简介
前面介绍了mysql bit位基于sql上的使用技巧,但是在复杂的业务系统中,如果有很多表都存在多状态管理,那么每个表的状态管理都去手工单独定义基于位操作的管理SQL,这样开发起来就比较繁琐了。
在java程序中,是否能像mybatis plus的的枚举自动映射、sql构造器一样,几行代码就轻而易举的管理bit位多状态数据,开发人员不用去关心底层位的运算和sql操作呢?
要做到以上要求,分两部份在框架层面进行封装即可:
- 对bit位结果自动映射
- 对bit位自定义sql构造器
同样我们的封装基于mybatis plus框架只做增强,不做改变
,下面我们大概讲讲实现原理:
1. 准备测试表
CREATE TABLE `bit_demo` (
`id` int NOT NULL,
`name` varchar(100) DEFAULT NULL,
`flag` bit(3) DEFAULT NULL COMMENT '从低到高位依次为移动,电信,联通',
PRIMARY KEY (`id`),
KEY `idx_bit` (`flag`)
)
说明:为了将来可扩展,我们一般先从低位开始使用,此处我们设计三个位,从低到高位依次为移动,电信,联通短信渠道是否完成发送,各位值上1表示true,0表示false.
3个位,一共8种情况,二进制值范围:000->111,十进制范围:0->7
二进制 十进制 说明 000 0 所有渠道都未发送 001 1 移动渠道已发送 010 2 电信渠道已发送 011 3 移动、电信渠道已发送 100 4 联通渠道已发送 101 5 移动、联通渠道已发送 110 6 联通、电信渠道已发送 111 7 移动、电信、联通渠道已发送
2. bit位结果自动映射
原理:自定义mybatis类型转换器,编写代码处理bit位值到对象属性的双向映射转换。
mybatis内置的枚举转换器一样.
2.1 先看使用效果
// 写入bit数据
MessageBitStatusDTO bitStatus = new MessageBitStatusDTO();
bitStatus.setHasYidong(true);
bitStatus.setHasDianxin(false);
bitStatus.setHasLiantong(true);
bitDemoMapper.insert(new BitDemo().setId(4).setName("阿虎").setFlag(bitStatus));
//修改bit数据
MessageBitStatusDTO bitStatus = new MessageBitStatusDTO();
bitStatus.setHasYidong(false);
bitStatus.setHasDianxin(false);
bitStatus.setHasLiantong(true);
bitDemoMapper.updateById(new BitDemo().setId(4).setFlag(bitStatus));
//列表查询
List<BitDemo> list = bitDemoMapper.selectList(new LambdaQueryWrapper<BitDemo>());
list.forEach(c->{
Boolean hasYidong = c.getFlag().getHasYidong();
Boolean hasDianxin = c.getFlag().getHasDianxin();
Boolean hasLiantong = c.getFlag().getHasLiantong();
});
// 按状态查询
MessageBitStatusDTO bitStatus = new MessageBitStatusDTO();
bitStatus.setHasYidong(false);
bitStatus.setHasDianxin(false);
bitStatus.setHasLiantong(true);
//手工转换对象到bit位值
int value = 0;
if(bitStatus.getHasYidong()) {
value|=1<<0;
}
if(bitStatus.getHasDianxin()) {
value|=1<<1;
}
if(bitStatus.getHasLiantong()) {
value|=1<<2;
}
// SELECT * FROM bit_demo WHERE (flag = b'100') or SELECT * FROM bit_demo WHERE (flag = 4)
List<BitDemo> bitDemoList =
bitDemoMapper.selectList(new LambdaQueryWrapper<BitDemo>().eq(BitDemo::getFlag, value));
log.debug(JSONObject.toJSONString(bitDemoList));
查看数据:
MySQL [test]> select id,name,flag+0,bin(flag) from bit_demo;
+----+--------+--------+-----------+
| id | name | flag+0 | bin(flag) |
+----+--------+--------+-----------+
| 1 | 张三 | 3 | 11 |
| 2 | 李四 | 3 | 11 |
| 3 | 三五 | 3 | 11 |
| 4 | 阿虎 | 4 | 100 |
+----+--------+--------+-----------+
4 rows in set (0.00 sec)
4 rows in set (0.03 sec)
以上是基于mybatis plus内置的默认的数据库操作API,通过自定义转换器,已自动将MessageBitStatusDTO实体和flag字段bit位值双向进行了状态转换提取。
问题:
- 无法一次db操作根据某一位或几位状态进行查询?
- 无法一次db操作单独更新某一位或几位状态?
参考以下第三章节解决问题
2.2 定义映射配置注解
- 定义状态位映射注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface BitField {
/**
* position属性常量
*/
static String POSITION_KEY = "position";
/**
* 位置
* 按低位从1开始映射
* @author hushow
* @return String
* @date 2020年7月31日 上午10:54:41
*/
int position() default 1;
}
- 定义多状态管理基类
/**
* 基于数据库bit类型多状态位管理基类
*
* @author hushow
* @date 2020年8月7日 上午10:46:49
*/
public interface BitBase {
}
- 继承BitBase,完成具体业务多状态实体映射
@Setter
@Getter
@JsonInclude(JsonInclude.Include.NON_NULL)
@ApiModel(value="bit状态", description="消息渠道多状态实体")
public class MessageBitStatusDTO implements Serializable , BitBase{
private static final long serialVersionUID=1L;
/**
* 移动
*/
@BitField(position = 1)
private Boolean hasYidong;
/**
* 电信
*/
@BitField(position = 2)
private Boolean hasDianxin;
/**
* 联通
*/
@BitField(position = 3)
private Boolean hasLiantong;
}
3.3 转换器核心逻辑
在此只提供转换思路代码,至于mybatis类型转换器如何配置开发,后续开章节详解.
/**
* 读取注解缓存多状态字段映射关系
* @author hushow
* @param type void
* @date 2020年8月10日 上午11:42:20
*/
private void initPosition2Field(Class<E> type) {
System.out.println(this.getRawType());
ReflectionUtils.doWithFields(type, fc -> {
Annotation an = fc.getAnnotation(BitField.class);
if (null != an && fc.getType() == Boolean.class) {
Map<String, Object> map = AnnotationUtils.getAnnotationAttributes(an);
Integer position = (Integer)map.get(BitField.POSITION_KEY);
position2FieldMap.put(position, fc);
}
});
}
/**
* 提取对象状态属性到bit值
* @author hushow
* @param obj void
* @date 2020年8月3日 上午10:11:10
*/
@SuppressWarnings("unchecked")
private Integer extractEntityToBit(E obj) {
try {
if(position2FieldMap.size()<=0) {
initPosition2Field((Class<E>)obj.getClass());
}
//000|001 1
//001|010 3
//011|100
Integer v = 0;
for (Entry<Integer, Field> entry : position2FieldMap.entrySet()) {
Integer position = entry.getKey();
Field field = entry.getValue();
field.setAccessible(true);
Boolean booleanValue = (Boolean)field.get(obj);
if(booleanValue) {
Integer moveStep = position <= 0 ? 0 : position - 1;
//基位
Integer bitBase = 1 << moveStep;
v|=bitBase;
}
}
return v;
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new RuntimeException("setBitField 异常:" + e.getMessage(), e);
}
}
/**
* 提取bit位值到对象属性中
* @author hushow
* @param value
* @return E
* @date 2020年8月3日 上午10:11:00
*/
private E extractBitToEntity(Integer value) {
try {
E obj = this.type.newInstance();
for (Entry<Integer, Field> entry : position2FieldMap.entrySet()) {
Integer position = entry.getKey();
Field field = entry.getValue();
Integer moveStep = position <= 0 ? 0 : position - 1;
//基位
Integer bitBase = 1 << moveStep;
field.setAccessible(true);
Boolean extractValue = false;
if ((value & bitBase) > 0) {
extractValue = true;
}
field.set(obj, extractValue);
}
return obj;
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new RuntimeException("extractBitField 异常:" + e.getMessage(), e);
}
}
3. bit位自定义sql构造器
参考mybatis plus QueryWrapper 相关实现,通过继承扩展我们自己的bit字段构造器,这样就可以达到灵活的状态更新及查找。
3.1 使用效果
//只修改指定两个状态
int updateCount = bitDemoMapper.update(null,
new BitLambdaUpdateWrapper<BitDemo, MessageBitStatusDTO>()
.setBit(BitDemo::getFlag, MessageBitStatusDTO::getHasDianxin, true)
.setBit(BitDemo::getFlag, MessageBitStatusDTO::getHasLiantong, false).eq(BitDemo::getId, 1));
log.debug("updateSingleBitTest.updateCount :{}", updateCount);
//将所有未发送移动短信渠道的数据,所有状态置为已发送
int updateCount = bitDemoMapper.update(null,
new BitLambdaUpdateWrapper<BitDemo, MessageBitStatusDTO>()
.setBit(BitDemo::getFlag, MessageBitStatusDTO::getHasYidong, true)
.setBit(BitDemo::getFlag, MessageBitStatusDTO::getHasDianxin, true)
.setBit(BitDemo::getFlag, MessageBitStatusDTO::getHasLiantong, true)
.eqBit(BitDemo::getFlag, MessageBitStatusDTO::getHasYidong, false));
//搜索出所有未发送电信且已发送联通短信渠道的数据
List<BitDemo> bitDemoList = bitDemoMapper.selectList(new BitLambdaQueryWrapper<BitDemo, MessageBitStatusDTO>()
.eqBit(BitDemo::getFlag, MessageBitStatusDTO::getHasDianxin, false)
.eqBit(BitDemo::getFlag, MessageBitStatusDTO::getHasLiantong, true)
);
3.2 构造器实现代码
继承LambdaUpdateWrapper类,实现setBit方法,构造灵活的bit状态位写入
/**
* 支持Bit类型字段单个状态位设置构造器
*
* @author hushow
* @date 2020年8月6日 下午5:24:16
*/
@SuppressWarnings("serial")
public class BitLambdaUpdateWrapper<T, B extends BitBase> extends LambdaUpdateWrapper<T>{
/**
* 状态字段映射缓存
*/
static final Map<String, PositionField> NAME_POSITION_FIELD_MAP = new ConcurrentHashMap<>();
/**
* 设置要修改的bit状态位值
*
* @author hushow
* @param column 位类型字段
* @param bitStatusField 位类型字段中某状态字段
* @param val 要设置的某状态字段值
* @return BitLambdaUpdateWrapper<T, B>
* @date 2020年8月6日 下午5:22:14
*/
public BitLambdaUpdateWrapper<T, B> setBit(SFunction<T, ?> column, SFunction<B, ?> bitStatusField, Boolean val) {
Assert.notNull(val, "column not null", column);
Assert.notNull(val, "bitStatusColumn not null", bitStatusField);
Assert.notNull(val, "val not null", val);
//获取位字段数据库列名
String columnName = columnToString(column);
//获取状态字段注解位置
SerializedLambda bitStatusLambda = LambdaUtils.resolve(bitStatusField);
String bitStatusfieldName = PropertyNamer.methodToProperty(bitStatusLambda.getImplMethodName());
PositionField positionField = getBitStatusfieldName(bitStatusLambda.getInstantiatedType(), bitStatusfieldName);
if (null == positionField) {
Assert.notNull(positionField, "can not find lambda cache for this annotationed BitField fieldName [%s]",
bitStatusfieldName);
}
//计算基位
Integer moveStep = positionField.getPosition() <= 0 ? 0 : positionField.getPosition() - 1;
Integer bitValue = 1 << moveStep;
//使用mybatis内置函数组装自定义sql
if (val) {
// 100|010=110
this.setSql(String.format("%s=%s|%d", columnName, columnName, bitValue));
} else {
// 100&101=100
this.setSql(String.format("%s=%s&%d", columnName, columnName, ~bitValue));
}
return this;
}
/**
* 获取位字段配置信息
*
* @author hushow
* @param clazz
* @param bitStatusfieldName
* @return PositionField
* @date 2020年8月7日 上午10:31:33
*/
protected PositionField getBitStatusfieldName(Class<?> clazz, String bitStatusfieldName) {
return Optional.ofNullable(NAME_POSITION_FIELD_MAP.get(bitStatusfieldName)).orElseGet(() -> {
ReflectionUtils.doWithFields(clazz, fc -> {
Annotation an = fc.getAnnotation(BitField.class);
if (null != an && fc.getType() == Boolean.class) {
Map<String, Object> map = AnnotationUtils.getAnnotationAttributes(an);
Integer position = (Integer)map.get(BitField.POSITION_KEY);
NAME_POSITION_FIELD_MAP.put(fc.getName(), new PositionField(position, fc));
}
});
return NAME_POSITION_FIELD_MAP.get(bitStatusfieldName);
});
}
@Getter
@Setter
@AllArgsConstructor
public static class PositionField {
private Integer position;
private Field field;
}
}