单一字段存储多状态思路

最近在做一个类似百度知道类的系统,针对每个回答以及问题都有可能有多重组合状态:

  • 是否过期
  • 是否重复
  • 是否优质

  • 这些状态有个共同的特点,就是都是 Yes Or Not,如果拍平,每个状态都用一个独立的字段进行存储,字段存储信息过于简单先不说,将来如果增加状态的话,数据库结构也需要同步修改。

正在研究怎么处理的时候,感谢同事的提点,给我了一个单一字段存储这些值的思路。经过实践后,发觉用的挺好,于是总结一下。

基本原理

首先我们已知这些状态都是 Yes Or Not,且互斥的关系,不相交。因此我们可以将这些状态都看做是一个二进制数的一位,例如过期看做是 0001,重复是 0010,优质是 0100,这样,如果数据库中字段值是 3=(0b0011),就表示既过期,又重复。
这样如果我们将来状态不止这几种,只需要扩充二进制的位数即可。

计算

我们思路有了,如何提取结果就是目前的重要问题。
即:我给一个数据库值 3=(0b0011),如何反推出是优质和过期。
这就涉及到三个按位操作符——&、|、^。

&(按位与)

逐位进行与运算

A B 结果
0 0 0
0 1 0
1 0 0
1 1 1
例如:
0011 & 0001 = 0001
0010 & 0001 = 0000

|(按位或)

逐位进行与运算

A B 结果
0 0 0
0 1 1
1 0 1
1 1 1
例如:
0011 0001 = 0011
0010 0001 = 0011

^(按位异或)

逐位进行与运算,相同为 0,不相同为 1

A B 结果
0 0 0
0 1 1
1 0 1
1 1 0
例如:
0011 ^ 0001 = 0010
0010 & 0001 = 0000

状态值计算

有个这三个工具后,就可以进行相应值的计算了
首先**计算状态组合,用|运算**,
例如:
一个问题是重复,不过期,不优质,就用 0001 | 0000 | 0000 = 0001 = 1(int)
一个问题是重复,过期,不优质,就用 0001 | 0010 | 0000 = 0011 =3(int)
每一种组合都对应唯一的一个二进制结果,这样将结果保存后,就一个用一个字段存储所有组合了。

然后我们有了这个计算结果后,还可以进行**反推状态,这里用&操作**
例如,刚才我们的一个计算结果是 3,0b0011
我们首先用 0011 与“重复”对应的 0001 进行&操作
0011 & 0001 = 0001 ≠ 0,表示这个计算结果是包含“重复”状态的。
我们在用 0011 与“过期”对应的 0010 进行&操作
0011 & 0010 = 0010 ≠ 0,表示这个计算结果是包含“过期”状态的。
我们在用 0011 与“优质”对应的 0100 进行&操作
0011 & 0100 = 0000 = 0,表示这个计算结果是不包含“优质”状态的。
通过与相应二进制码进行&操作后结果是否等于 0,可以判断状态。

除了进行反推,还需要个清除状态,可以去掉某个状态值。这一用到^操作
例如已知 0011 是包含重复状态,去除重复的话
0011 ^ 0001 = 0010 ,0010 表示只过期状态。

一句话总结下:
添加状态用|,去掉状态用^(这里有个小坑,之后说),判断状态用&

具体代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public enum Status {

DEFAULT(0b00000000, "默认", "default"),
REPEAT(0b00000001, "重复", "repeat"),
EXPIRE(0b00000010, "过期", "expire");

private int code;
private String desc;
private String property;


public static int getMark(Status... status) {
int init = 0;
for (AnswerStatus s : status) {
init = init | (s.getCode());
}
return init;
}

public static boolean status(int mark, Status status) {
return (mark & status.getCode()) == status.getCode();//判断是否相等也行,判断是否不等于0也可以
}

public static void handle(Consumer<Status> consumer) {
Arrays.stream(values()).forEach(consumer::accept);
}
}

生成状态值时,代码如下:

1
2
3
4
5
6
7
8
//保存mark
setMark(Status.getMark(
answer.isRepeat() ? Status.REPEAT : Status.DEFAULT,
answer.isExpire() ? Status.EXPIRE : Status.DEFAULT
))
//获取状态
repeat(AnswerStatus.status(answer.getMark(), AnswerStatus.REPEAT))
expire(AnswerStatus.status(answer.getMark(), AnswerStatus.EXPIRE))

这里生成状态值时,只能给出所有组合,进行保存,如果想根据当前状态与给定状态,进行组合更新,需要像下面这样调用:
例如目前,已经是 0011 了,我们给定 expire=false,需要结果更新为 0001

  1. 0011 & 0010 = 0010 ≠0,表示目前是过期,去掉过期通过^处理,0011^0010 = 0001
  2. 0001 & 0010 = 0000 =0,表示原来就不是过期,所以不需要进行操作
    这种单独修改,需要判断状态的转化,所以一般分为两步:
    1、获取当前状态与目标状态
    2、是-》是,否-》否,不需要操作;是到否,异或方式去掉状态;否到是,用或操作,将状态加上。
1
2
3
4
5
6
7
8
9
10
11
12
//mark:{repeat:true/false,expire:true/false}
Status.handle(element -> {
if (mark.containsKey(element.getProperty())) {
//需要进行调整
if ((pojo.getMark() & element.getCode()) != 0 && !mark.getBoolean(element.getProperty())) {
pojo.setMark(pojo.getMark() ^ (element.getCode()));
} else if ((pojo.getMark() & element.getCode()) == 0 && mark
.getBoolean(element.getProperty())) {
pojo.setMark(pojo.getMark() | (element.getCode()));
}
}
});

后续

这样就可以用一个字段进行存储了。其实我目前的方法不是最优解,实际上安卓系统在处理这种多状态时,已经在用这种二进制方式了,后续在研读安卓后,会尝试写一篇后续文章,优化目前的代码


单一字段存储多状态思路
https://wangqianying.com/2019/11/01/2019-11-01-multi-binary/
作者
wang fei
发布于
2019年11月1日
许可协议