一、多值存储
mysql 条件位运算位运算实现多值存储,方法适合数据范围有限,且不会变更在业务上往往会出现多选的情况,例:选择 周一 至 周日 随意组合;
数据在设计时就会如何去储存?
- 一种是一般是在储存是以某种方式隔开,例如:1,2,3代表选择了 周一、 周二、周三;
- 另一种就是使用,mysql的位运算;字段类型为 int(3);
七个二进制分别代表 周一至周日,0-未选 1-选中,例: 选择了周日、周一、 周二。
周一 | 周二 | 周三 | 周四 | 周五 | 周六 | 周日 |
---|---|---|---|---|---|---|
1 | 1 | 0 | 0 | 0 | 1 |
对应二进制位:11000001,数据库储存十进制:67
注意:
mysql位运算,一个字段表示多选值;这种方式可以提高查询效率,减少like语句的应用;
这种方式,建议每次参数都全量传。比如张三,第一次勾选了周一,第二次想新增周三、周四,那么前端传值:1,8,16;
二、设计
星期码值表:
数据库存的数据:
三、相关sql
3.1 关键sql
-- 查询包含周四的
SELECT * FROM test_weeks WHERE weeks & 8;
-- 查询不包含周四的
SELECT * FROM test_weeks WHERE !(weeks & 8);
-- 查询包含周一or周四的
SELECT * FROM test_weeks WHERE weeks & (1+8);
SELECT * FROM test_weeks WHERE weeks & 1 or weeks & 8;
-- 查询包含周一and周四的
SELECT * FROM test_weeks WHERE weeks & 1 and weeks & 8;
-- 新增周四、周五;不建议,如果已经包含周四、周五了,这种方法计算出来的码值不对。
UPDATE test_weeks set weeks = weeks + 8 + 16 WHERE id = 3
-- 减少周四、周五;不建议,如果已经不含周四、周五了,这种方法计算出来的码值不对
UPDATE test_weeks set weeks = weeks - 8 - 16 WHERE id = 3
3.1.1 根据名字,更新数据,将某人值班信息增加周四、周五
sumNum=8+16;
<update id="addDutyAndBin2">
UPDATE mybatisx_duty_info
<![CDATA[ set weeks = weeks | #{sumNum},]]>
<!--LPAD函数是MySQL中用于实现对字符串的左侧填充操作的函数。它的语法格式为:LPAD(str,len,padstr) -->
weeks_bin=LPAD(bin( weeks | #{sumNum}),8,'0')
WHERE name = #{name}
</update>
3.1.2 根据名字,更新数据,将某人值班信息去掉周四、周五
- 如果该人有周四、周五的值班信息,可以用【3.1 关键sql】最后一条sql;
- 如果该人有周四、周五的值班信息,想实现【3.1.1】,这种情况写不了,没有对应的位运算符;
四、建表语句
星期枚举表:
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for mybatisx_week
-- ----------------------------
DROP TABLE IF EXISTS `mybatisx_week`;
CREATE TABLE `mybatisx_week` (
`id` int(0) NOT NULL,
`week_ten` int(0) NULL DEFAULT NULL,
`week_bin` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '星期枚举表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of mybatisx_week
-- ----------------------------
INSERT INTO `mybatisx_week` VALUES (1, 1, '0000001', '星期日');
INSERT INTO `mybatisx_week` VALUES (2, 2, '0000010', '星期一');
INSERT INTO `mybatisx_week` VALUES (3, 4, '0000100', '星期二');
INSERT INTO `mybatisx_week` VALUES (4, 8, '0001000', '星期三');
INSERT INTO `mybatisx_week` VALUES (5, 16, '0010000', '星期四');
INSERT INTO `mybatisx_week` VALUES (6, 32, '0100000', '星期五');
INSERT INTO `mybatisx_week` VALUES (7, 64, '1000000', '星期六');
SET FOREIGN_KEY_CHECKS = 1;
值班表:
CREATE TABLE `mybatisx_duty_info` (
`id` int NOT NULL AUTO_INCREMENT,
`weeks` int DEFAULT NULL,
`weeks_bin` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='值班表';
五、代码
5.1 查询周日、周三都值班的人
5.1.1 代码
/**
*
* ==> Preparing: SELECT * FROM mybatisx_duty_info WHERE weeks & ? and weeks & ?
* ==> Parameters: 1(Integer), 8(Integer)
*/
@Test
public void testAnd(){
//查一下周一和周四都值班的人;
//周一:1;周四:8
Integer[] weekArr={1,8};
List<Integer> weekList = Arrays.asList(weekArr);
List<MybatisxDutyInfo> mybatisxDutyInfos = mybatisxDutyInfoService.listByBinAnd(weekList);
System.out.println("周一和周四都值班的人:"+ JSON.toJSONString(mybatisxDutyInfos));
}
5.1.2 mybatis.xml
<select id="listByWeekOfAnd" resultType="com.qian.mybatisx.domain.MybatisxDutyInfo">
SELECT *
FROM mybatisx_duty_info
<where>
<if test="weekList!=null and weekList.size >0">
<foreach collection="weekList" item="week">
<![CDATA[ and weeks & #{week}]]>
</foreach>
</if>
</where>
</select>
5.2 查询包含周日or周三的
5.2.1 代码
/**
* ==> Preparing: SELECT * FROM mybatisx_duty_info WHERE weeks & ?
* ==> Parameters: 72(Integer)
*/
@Test
public void testOr() {
//查询包含周日or周三的
//周日:64;周四:8
Integer[] weekArr={64,8};
List<Integer> weekList = Arrays.asList(weekArr);
List<MybatisxDutyInfo> mybatisxDutyInfos = mybatisxDutyInfoService.listByBinOr(weekList);
System.out.println("周日or周三值班的人:"+ JSON.toJSONString(mybatisxDutyInfos));
}
@Override
public List<MybatisxDutyInfo> listByBinOr(List<Integer> weekList) {
if (CollectionUtils.isEmpty(weekList)){
return null;
}
//mybatis的<foreach>标签里,相加不好写,so,在业务层加上
int sum=0;
for (Integer integer : weekList) {
sum+=integer;
}
return mybatisxDutyInfoMapper.listByWeekOfOrSum(sum);
}
5.2.2 mybatis.xml
<select id="listByWeekOfOrSum" resultType="com.qian.mybatisx.domain.MybatisxDutyInfo">
SELECT *
FROM mybatisx_duty_info
<where>
<if test="sum >0">
<![CDATA[ and weeks & #{sum}]]>
</if>
</where>
</select>
5.3 查询不包含周二的
5.3.1 代码
/**
*
* ==> Preparing: SELECT * FROM mybatisx_duty_info WHERE !(weeks & ?)
* ==> Parameters: 2(Integer)
*/
@Test
public void testNotIs() {
//查询不包含周二的
//周二:2
Integer week=2;
List<MybatisxDutyInfo> mybatisxDutyInfos = mybatisxDutyInfoService.listByBinNotIs(week);
System.out.println("不包含周二值班的人:"+ JSON.toJSONString(mybatisxDutyInfos));
}
5.3.2 mybatis.xml
<select id="listByBinNotIs" resultType="com.qian.mybatisx.domain.MybatisxDutyInfo">
SELECT *
FROM mybatisx_duty_info
<where>
<if test="week !=null and week.tostring!='null'">
<![CDATA[ and !(weeks & #{week})]]>
</if>
</where>
</select>
5.4 修改值班日期为:周一、周三、周四;
5.4.1 代码
/**
* 【建议这样:参数传全量的】
* 张三值班改为周一、周三、周四;
* 计算二进制:select bin(3);
*
* 位数不足补0:SELECT LPAD(bin(3),8,'0')
*/
@Test
public void testUpdateDuty() {
//周一周三周四
Integer[] weekArr={1,8,16};
List<Integer> weekList = Arrays.asList(weekArr);
mybatisxDutyInfoService.updateDuty(weekList,"张三");
}
@Override
public void updateDuty(List<Integer> weekList, String name) {
//sum表示要添加的码值之和
Integer sum=0;
for (Integer week : weekList) {
sum+=week;
}
mybatisxDutyInfoMapper.updateDuty(sum,name);
}
5.4.2 mybatis.xml
<update id="updateDuty">
UPDATE mybatisx_duty_info set weeks = #{sumNum},
<!--LPAD函数是MySQL中用于实现对字符串的左侧填充操作的函数。它的语法格式为:LPAD(str,len,padstr) -->
weeks_bin=LPAD(bin(#{sumNum}),8,'0')
WHERE name = #{name}
</update>
六、Java 位运算的常用方法封装
package com.qian.mybatisx.util;
/**
* Java 位运算的常用方法封装<br>
*/
public class BitUtils {
/**
* 获取运算数指定位置的值<br>
* 例如: 0000 1011 获取其第 0 位的值为 1, 第 2 位 的值为 0<br>
*
* @param source
* 需要运算的数
* @param pos
* 指定位置 (0<=pos<=7)
* @return 指定位置的值(0 or 1)
*/
public static byte getBitValue(byte source, int pos) {
return (byte) ((source >> pos) & 1);
}
/**
* 将运算数指定位置的值置为指定值<br>
* 例: 0000 1011 需要更新为 0000 1111, 即第 2 位的值需要置为 1<br>
*
* @param source
* 需要运算的数
* @param pos
* 指定位置 (0<=pos<=7)
* @param value
* 只能取值为 0, 或 1, 所有大于0的值作为1处理, 所有小于0的值作为0处理
*
* @return 运算后的结果数
*/
public static byte setBitValue(byte source, int pos, byte value) {
byte mask = (byte) (1 << pos);
if (value > 0) {
source |= mask;
} else {
source &= (~mask);
}
return source;
}
/**
* 将运算数指定位置取反值<br>
* 例: 0000 1011 指定第 3 位取反, 结果为 0000 0011; 指定第2位取反, 结果为 0000 1111<br>
*
* @param source
*
* @param pos
* 指定位置 (0<=pos<=7)
*
* @return 运算后的结果数
*/
public static byte reverseBitValue(byte source, int pos) {
byte mask = (byte) (1 << pos);
return (byte) (source ^ mask);
}
/**
* 检查运算数的指定位置是否为1<br>
*
* @param source
* 需要运算的数
* @param pos
* 指定位置 (0<=pos<=7)
* @return true 表示指定位置值为1, false 表示指定位置值为 0
*/
public static boolean checkBitValue(long source, int pos) {
source = (long) (source >>> pos);
return (source & 1) == 1;
}
/**
* 入口函数做测试<br>
*
* @param args
*/
public static void main(String[] args) {
int param = 3;
System.out.println(getBitValue(Byte.parseByte(param+""),2));
// 取十进制 11 (二级制 0000 1011) 为例子
long source = 256;
// 取第2位值并输出, 结果应为 0000 1011
for (byte i = 7; i >= 0; i--) {
System.out.printf("%d ", getBitValue((byte) source, i));
}
// 将第6位置为1并输出 , 结果为 75 (0100 1011)
System.out.println("\n" + setBitValue((byte) source, 6, (byte) 1));
// 将第6位取反并输出, 结果应为75(0100 1011)
System.out.println(reverseBitValue((byte) source, 6));
// 检查第6位是否为1,结果应为false
System.out.println(checkBitValue(source, 6));
// 输出为1的位, 结果应为 0 1 3
for (byte i = 0; i < 8; i++) {
if (checkBitValue(source, i)) {
System.out.printf("%d ", i);
}
}
}
}
七、附MySQL的支持6种位运算
八、参考文章
教你巧用mysql位运算解决多选值存储的问题
mysql 条件位运算实现多值存储
Java 一个字段存多个选项 位运算工具类
【架构思想】告别繁琐的多选值存储,MySQL位运算带你飞!