关于或逻辑的思考
本篇文章我们来探讨如何使用 flying 的方式来描述带有 ”or” 关键字的 sql 语句(如果您对 flying 还不了解,请参见 https://www.oschina.net/p/flying)。一直以来,flying力求做到的就是,把每一次与数据库交互都变为对象交互,而不是字符串交互,因为对象相比字符串至少有以下好处:
- 对象是完全解析的。比如我有一个子容器向父容器发送查询请求,然后按照业务,父容器要在这个查询请求上修改一个条件再追加一个条件。使用查询对象可以轻松做到这一点,因为它能被完全解析;但解析字符串就很麻烦了(试想一下拆分一个充满了 and 和 or 的 sql),估计只有数据库厂商才能提供可靠的工具。
-
不同数据库的 sql 语法有区别,但它们的查询对象相同。
-
对象可以跨语言,可以以 json 方式传输保存。
为了做到以上这些点,作为条件的查询对象必须具有以下特点:
- 各条件变量的赋值顺序无关。
- 易于理解和修改。
- 各条件变量是“且”的关系。
第一个特性很好理解,例如 personCondition.setNameLike(“张”); 和 personCondition.setAge(30); 就是顺序无关的,flying查询对象目前所有的条件赋值语句(包括判断条件、分页条件、排序条件)都是顺序无关的。
第二个特性是,用户一眼看到某个变量赋值语句就知道它的作用是什么,并可以按需要进行修改。
第三个特性是,所有的条件变量其实都是用与逻辑“and”相连的。
看到这里,大家会发现,其实 flying 查询对象只解决了一半的问题,因为对于或操作 “or”,以前根本就没有提及,而没提及的原因是,在满足以上三点的基础上解决或逻辑比较困难。而本文则尝试解决这一问题。
首先,我们抛出一个足够复杂的sql语句:
select person.id, person.name, person.age, person.level from person
where (person.name like ‘张%’ and person.age = 25)
or (person.age = 27 and person.level = ‘B’)
or (person.name like ‘李%’ and person.level = ‘A’)
这个复杂的sql语句如何用一个查询对象表示呢?这里我们需要使用一些数学工具,首先我们用逻辑变量来代替条件表达式:
A = "person.name like '张%'"
B = "person.age = '25'"
C = "person.age = '27'"
D = "person.level = 'B'"
E = "person.name like '李%'"
F = "person.level = 'A'"
这样一来以上这个逻辑表达式就简化为:(A∩B)∪( C∩D)∪( E∩F)
可是这样无法解决问题,因为 flying 擅长解决的是以“且”关系连接的条件,例如 X∩Y∩Z 这样,而以上表达式明显不是这样。
但是布尔逻辑运算具有以下性质:交换律、结合律与分配律。
交换律:A∩B = B∩A
同理 A∪B = B∪A
结合律:A∩(B∩C) = A∩B∩C
同理 A∪(B∪C)= A∪B∪C
分配律:(A∩B)∪C = (A∪C)∩(B∪C)
同理(A∪B)∩C = (A∩C)∪(B∩C)
有了这三个定律之后,我们就可以把(A∩B)∪( C∩D)∪( E∩F)变形为一连串布尔变量以“∩”相连的形式:
(A∩B)∪(C∩D)∪( E∩F)
= (((A∩B)∪C)∩((A∩B)∪D)))∪( E∩F)
= (((A∪C)∩(B∪C))∩((A∪D)∩(B∪D)))∪( E∩F)
= ((A∪C)∩(B∪C)∩(A∪D)∩(B∪D))∪(E∩F)
= (((A∪C)∩(B∪C)∩(A∪D)∩(B∪D))∪E)∩(((A∪C)∩(B∪C)∩(A∪D)∩(B∪D))∪F)
= (A∪C∪E)∩(B∪C∪E)∩(A∪D∪E)∩(B∪D∪E)∩(A∪C∪F)∩(B∪C∪F)∩(A∪D∪F)∩(B∪D∪F)
最后的这个形式看起来是我们用 flying 能描述的了的。实际上,对于布尔运算式有以下定理:
任何一个布尔表达式都能被转换为一个等价的合取范式(CNF),合取范式格式为:C1∩C2∩……Cn;其中,Ck(1<=k<=n)称为合取项,每个合取项是不包含∩的表达式。
这个归并是关系型数据库自己也会做的,因为它具有以下好处:
- 合取项只要有一个为假,整个表达式就为假,故代码中可以在发现一个合取项为假时,即停止其他合取项的判断,加快判断速度,如:WHERE(0 > 1 AND s1 = 5)
- 因为AND操作符是可交换的,所以优化器可以按照先易后难的顺序计算表达式,一旦发现一个合取项为假时,即停止其他合取项的判断,加快判断速度。
-
如果一个合取项上存在索引,则先判断索引是否可用,如能利用索引快速得出合取项的值,则能加快判断速度。如:WHERE (A.a> 100 AND A.b = 5 AND... )
情况1:A表的a列上存在索引,b列无索引,则利用a上的索引找出元组,“A.b = 5” 作为过滤条件使用;情况2:A表的a列上不存在索引,b列有索引,则利用b上的索引找出元组,“A.a> 100” 作为过滤条件使用。
所以,相对于(A∩B)∪( C∩D)∪( E∩F),我们将(A∪C∪E)∩(B∪C∪E)∩(A∪D∪E)∩(B∪D∪E)∩(A∪C∪F)∩(B∪C∪F)∩(A∪D∪F)∩(B∪D∪F)传给数据库,并不会增加它的查询时间,因为它原本也需要归并。
那么接下来的问题就变成,我们如何用代码描述(A∪C∪E),或者更具体地说,如何用代码描述:"person.name like '张%' or person.age = 27 or person.name like '李%'" 这样一个查询条件,这个解决了其它查询条件同理也就都解决了。
在这里,flying新增了Or标签类(见https://gitee.com/limeng32/mybatis.flying/blob/master/src/main/java/indi/mybatis/flying/annotations/Or.java),这个标签的内容是ConditionMapperAnnotation标签的数组,所以在查询条件类中可以有如下标签代码:
@Or({
@ConditionMapperAnnotation(dbFieldName = "name", conditionType = ConditionType.HeadLike),
@ConditionMapperAnnotation(dbFieldName = "age", conditionType = ConditionType.Equal),
@ConditionMapperAnnotation(dbFieldName = "name", conditionType = ConditionType.HeadLike)
})
同时为了赋值方便,我们强烈建议采用不定参数的Object[]作为变量,于是整个代码变成了:
@Or({
@ConditionMapperAnnotation(dbFieldName = "name", conditionType = ConditionType.HeadLike),
@ConditionMapperAnnotation(dbFieldName = "age", conditionType = ConditionType.Equal),
@ConditionMapperAnnotation(dbFieldName = "name", conditionType = ConditionType.HeadLike)
})
private Object[] condition1;
public Object[] getCondition1 () {
return condition1;
}
public void setCondition1 (Object... condition1) {
this. condition1 = condition1;
}
我们描述 "person.name like '张%' or person.age = 27 or person.name like '李%' "的代码变为:
personCondition.setCondition1("张", 27, "李");
/* 注意参数顺序和 condition1 上 @ConditionMapperOrAnnotation 的内部顺序一致 */
于是问题就全解决了。您也许会觉得这个解决方案过于复杂,但对于(A∩B)∪( C∩D)∪( E∩F)来说,用其它代码方式描述一样复杂(纯sql除外,但我们知道使用查询对象代替 sql 的好处)。
接下来我们再给出一些常见一点的使用或逻辑的场景,例如我想选择所有30岁以下或50岁以上的人员:
@Or({
@ConditionMapperAnnotation(dbFieldName = "age", conditionType = ConditionType.LessThan),
@ConditionMapperAnnotation(dbFieldName = "age", conditionType = ConditionType.GreaterThan)
})
private Object[] ageFilter;
public Object[] getAgeFilter () {
return ageFilter;
}
public void setAgeFilter (Object... ageFilter) {
this. ageFilter = ageFilter;
}
personCondition.setAgeFilter(30,50);
或者我们找所有姓张或者姓李的人:
@Or({
@ConditionMapperAnnotation(dbFieldName = "name", conditionType = ConditionType.HeadLike),
@ConditionMapperAnnotation(dbFieldName = "name", conditionType = ConditionType.HeadLike)})
private Object[] nameFilter;
/* 相关getter和setter请自行添加 */
personCondition.setAgeFilter("张", "李");
或者我们找年龄在 40 以上或者 level 为 A 的人:
@Or({
@ConditionMapperAnnotation(dbFieldName = "age", conditionType = ConditionType.GreaterThan),
@ConditionMapperAnnotation(dbFieldName = "level", conditionType = ConditionType.Equals)})
private Object[] filter;
/* 相关getter和setter请自行添加 */
personCondition.setFilter(40, "A");
是不是使用起来还是挺简单的?flying的设计哲学是“使您写下的每一行代码的回报率最大化”,当您的项目变得越来越庞大时,您会越来越明显感受到这一点。
自定义主键生成器
flying-初雪另一个特色是增加了自定义主键生成器,为此我们在flying:insert语句中新增了括号元素,比如:
flying:insert(uuid) 使用标准uuid作主键
flying:insert(uuid_no_line) 使用无下横线的uuid作主键
flying:insert(millisecond) 使用毫秒数作主键(需保证每秒并发在1000以下)
以上这些可以在 https://gitee.com/limeng32/mybatis.flying/blob/master/src/main/java/indi/mybatis/flying/statics/KeyGeneratorType.java 看到,当然更多的情况是您会自定义自己的主键生成器,只要您的主键生成器实现了 flying 中的 indi.mybatis.flying.type.KeyHandler 接口即可,比如这样调用一个自定义的主键生成器类:
flying:insert(indi.mybatis.flying.handlers.MySnowFlakeKeyHandler)
(上面的 indi.mybatis.flying.handlers.MySnowFlakeKeyHandler 是一个雪花主键生成器的 java 版本实现。雪花主键生成器由 tweeter 发明用于处理大规模并行写入,主键采用 float 类型存储以节省资源,自带递增无需 order by,单台主机每秒可产生 400 万个不同主键,最多可 1024 台主机集群同时工作)
或者您有某几个表的主键要共享一个连续数列的需求(比如工作流),就可以开发自己的主键生成器。
总结
- 本文是本人开源项目 flying (地址见 https://www.oschina.net/p/flying)在开发最新版本时需要用到的理论基础之二。
- flying 解决或逻辑问题的代码实现还没有写完,但理论已经完全梳理清楚,可以说完成了90%。
- 这段时间除了flying外同时进行两个大项目,整个人感觉像脱了一层皮,我不能说太多,只能透露其中一个和熬夜洗尿布有关。
- 最后请您静待 flying-初雪 降临。