Mysql Bit类型多状态位在Java中的妙用

2020-08-07 615 0

简介

前面介绍了mysql bit位基于sql上的使用技巧,但是在复杂的业务系统中,如果有很多表都存在多状态管理,那么每个表的状态管理都去手工单独定义基于位操作的管理SQL,这样开发起来就比较繁琐了。

http://www.hushowly.com/articles/1369

在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;
    }
}

相关文章

快速实现通用的办公文档在线预览方案
Spring Feign大文件上传踩坑记
MinIO分布式存储方案预研
Dubbo+Grpc+Spring Boot初体验
vagrant+virtualBox快速部署集群节点
Spring Boot下grpc最佳实践

发布评论