1 背景
枚举类型的字段,如果想支持多选,大家往往会把枚举值设为1、2、4、8、16等,就是2的n次方。这样多选时,多个值加一起存储到数据库中。如当选了1、4、16,则存到数据库中的值为21。这些和是可以唯一确定一套枚举组合的。(这种存储方式应该有个专有名词的,我给忘了,三年前我还记得,越来越回旋了,知道的麻烦在评论区告诉我一下)。
为什么涉及到位运算了呢?多选字段,我们在查询时往往只需要某个状态的数据,那所有包含这个枚举值的数据,都需要被查询到。比如这样一个二进制数:00011111,它就表示16+8+4+2+1=31,也就是说,如果我想要状态是2的所有数据,那我倒数第二位是1就行了,它就表示我枚举状态的组合。1就是选了,0就是没选。
java和mysql都提供了与运算符:&,以解决这种查询问题。比如 (i & 2)>0 ,表示只要i里包含2,那么它就会大于0,否则等于0。
更详细的解释,这里就不赘述了。我也讲不太明白。
言归正传,集团最近要求一套代码在多种数据库环境下都可以正常跑。oracle数据库虽然也支持位运算,但它是用自己的函数实现的,不是用的操作符,mysql的语法到这里就报错了。
各种sql数据库基本都是支持in的,所以考虑用in替换。使用in的话,就需要列举出来查询枚举的所有组合,这是个排列组合,当枚举中的值比较多时,这个组合也会增多,所以写死值不是很合适。
2 解决方案
直接上代码:
枚举里先添加一个获取所有枚举值的静态方法,不然以后增加新枚举值后,如果in列表忘记改了就拉了库了。
public enum ProductStatus { //待提交 TOSUBMIT(1), //待确认 TOCONFIRM(2), //已确认 CONFIRMED(4), //已驳回 REJECTED(8), //失效 INVALID(16); private int code; ProductStatus(int code) { this.code = code; } public int getCode() { return code; } public static List<Integer> getEnumValues(){ List<Integer> result = new ArrayList<>(); for(ProductStatus status:values()){ result.add(status.getCode()); } return result; } }
提供一个基于枚举列表和目标枚举值自动生成所有符合条件组合的方法,正向去列全所有可能的话,算法很复杂。所以这里换个方向,由于这些枚举值加和就是它所能生成的最大值,这些二进制组合就能生成从1到最大值间所有数,所以遍历一下逐个判断即可。
public class EnumUtils { /** * * @param enumValues 必须是二进制值,如1,2,4,8 * @param targetEnumValue 必须在enumValues中 * @return 基于提供的枚举值,计算出所有可能的加和,并返回所有包含2的结果 */ public static List<Integer> getBitwiseOperationsValues(List<Integer> enumValues, Integer targetEnumValue) { List<Integer> result = new ArrayList<>(); if (CollectionUtils.isEmpty(enumValues) || targetEnumValue == null || !enumValues.contains(targetEnumValue)) return result; Integer sum = enumValues.stream().mapToInt(Integer::intValue).sum(); for (int i = 1; i <= sum; i++) { if ((i & targetEnumValue) > 0) { result.add(i); } } return result; } }
代码里调用时,把枚举列表和目标枚举值传进去即可获取到所有组合的加和结果。
List<Integer> statuses=EnumUtils.getBitwiseOperationsValues(ProductStatus.getEnumValues(),2);