再谈关于约束子
在持久层中实现O/R Mapping的难点不在如何更新数据,因为Insert/Update/Delete的SQL语句非常简单并且变化很小。所以一上午的时间就可以写一个支持增删改的小型框架。真正的难点在于数据访问方面。原因是面向对象的查询不能仅仅完成SQL语句的简单翻译,而必须实现条件运算、条件简化和类型检查三大功能。因此,所有基于脚本的查询都将被否决,例如Hibernate的HQL。
否决脚本式语言的更深层次的原因在于:
1.一个查询条件在很多情况下并不是由一个应用现场决定的,很可能包括多个不同的场景来共同决定。例如,在功能设计方面决定一个条件,在权限限制方面又决定另一个条件,实际条件就成为两个条件之叉。因为脚本在运行时只是一个字符串,并没有携带更丰富的元数据信息,所以如果用脚本实现就无法避免多个脚本子句的拼接,这和SQL语句的拼接没有什么本质差别,其缺陷是明显的。
2.不能检测子类型的查询条件。如果一个持久层框架不能实现继承关系当然不能算是真正的持久层框架。
3.脚本很难实现条件冲突检查。当一个条件与另外一个条件冲突时,无法在将查询请求提交到数据库驱动之前就终止查询过程。
4.不能等到某个脚本式查询无法解析的时候才抛出一些低级的错误来,例如不存在的类型或属性、不匹配的查询类型等等。
5.当然,脚本是不支持重构和扩展的。不支持扩展也许还可以勉强接受,但对于面向对象语言来说,不支持重构不仅仅是一种缺陷,更说明与语言本身存在较大的冲突。
Kanas.net Framework的第一个版本就支持条件运算、简化和类型检查,但是真正完善是在
在Kanas.net Framework中,查询条件被统称为“约束子”(Constraint)。下表描述了所有的约束子及其版本沿袭:
|
类别 |
约束子类型 |
约束子名称 |
首次支持版本 |
|
特殊类 |
FullConstraint |
完全约束子 |
1.2 |
|
EmptyConstraint |
空约束子 |
1.2 |
|
|
属性约束类 |
AssignedConstraint |
赋值状态约束子 |
1.1 |
|
BoolConstraint |
布尔约束子 |
1.0 |
|
|
IntegerConstraint |
整数类型约束子 |
1.0 |
|
|
StringConstraint |
字符串类型约束子 |
1.0 |
|
|
FloatConstraint |
浮点类型约束子 |
1.0 |
|
|
DateConstraint |
日期类型约束子 |
1.0 |
|
|
EnumConstraint |
枚举类型约束子 |
1.0 |
|
|
GuidConstraint |
Guid类型约束子 |
1.0 |
|
|
BinaryConstraint |
二进制类型约束子 |
1.2 |
|
|
类型约束类 |
KeyConstraint |
关键值约束子① |
1.0 |
|
TypeConstraint |
子类型约束子② |
1.3 |
|
|
ReferenceConstraint |
引用约束子③ |
1.0 |
|
|
RecurseConstrant |
递归约束子④ |
1.0 |
|
|
MasteredConstraint |
反引用约束子⑤ |
1.1 |
|
|
运算约束类 |
NegativeConstraint |
反约束子 |
1.0 |
|
UnionConstraint |
合并约束子 |
1.0 |
|
|
IntersectionConstraint |
交叉约束子 |
1.0 |
① 关键值约束子是通过一组或多组关键值进行查询。例如查询ID分别为3、6、9的订单:
context.Select(KeyConstraint.By(Entity.Order, 3, 6, 9)
② 子类型约束子是指查询结果是指定一个子类型或多个子类型的实例。例如:许可证包括十类许可证,查询类型可以仅包括进口许可证和出口许可证:
context.Select(Entity.Authorization.IsTypeOf(Entity.ImportAuthorization, Entity.ExportAuthorization))
③ 引用约束子是指查询结果共同引用同一个或一组对象实例。例如查询三个订单实例(o1, o2, o3)所有的订单项:
context.Select(Entity.OrderItem.Order.Contains(o1, o2, o3))
如果是单个引用实例可以更简单:
context.Select(Entity.OrderItem.Order == o1)
④ 递归约束子是指查询结果共同引用满足某一约束子的一组对象。例如查询2006年5月的所有订单明细项:
context.Select(Entity.OrderItem.Order == (Entity.Order.OrderDate >= new DateTime(2006, 5, 1) & (Entity.Order.OrderDate < new DateTime(2006, 6, 1)))
⑤ 反引用约束子是指查询结果共同被满足某一约束子的一组对象所引用。例如查询其订单明细项中包括某个产品(p1)的所有订单:
context.Select(Entity.Order.Within(Entity.OrderItem.Order, Entity.OrderItem.Product == p1))
约束子的设计满足上文所指的三大设计要求:运算、简化及类型检查。首先是通过运算符重载使所有的约束子可以进行取交(“&”)、取并(“|”)和取反(“!”)三种运算。当然也支持直接通过表达式来构建关系表达式,例如IntersectionConstraint.By(Entity.Order).Add(……)。
其次是简化,通过Predigistion属性获取等值的简式。其实实现非常简单,每个约束子覆写GetPredigistion()方法就可以对冲突进行检测并求简,并通过该虚方法将体系内的给子传递给下一层约束子。例如,合并约束子的简式其实就是将每个子约束子求简后所获得的新的约束子。如果其中有一个子项为完全约束子则简式为完全约束子;如果有一项为空约束子则可以删该子项;如果只有一个子项则简式为该子项。而递归约束子的简式就是将引用的约束子求简后的表达式。
最后是类型检查。每种约束子对类型的要求都是固定的。至于属性与约束间的类型联系则是通过Entity辅助类来实现的,并不是约束子的功劳。
约束子如何转化为SQL语句,在1.1版中是由约束子自己解决的,而在1.2版中作了较大的修改,约束子被设计成完全独立于数据驱动之外的对象体系。因为不同的数据驱动对同一个约束子的解释是不同的,所以提供了三种层面的约束子扩展。第一个层面是最底层,自定义约束子并自定义实现。如果没有提供对应的查询语句的实现则通过内部选择器来实现查询。内部选择器是指约束子已经实现了一种结构的过滤能力,可以在实例化前将包含一组数据的结构进行筛选以确定是否对其实例化。第二个层面是适配器层面,不同的适配器对约束子的解释可以完全不同,例如SQL之于XPath。自定义数据驱动IAdapter接口一定会实现ILoadService接口,该接口解决对象的加载问题。第三个层面是数据库一级的,Kanas.net Framework通过实现DbAdapter作为IAdapter接口的标准实现已经按照ANSI92的SQL标准实现了所有的约束子SQL翻译。DbAdapter需要一个IDbProvider来为DbAdapter提供自定义功能。IDbProvider提供了一个OnInit方法,其中有一个参数就是ICriteriaBuilderManager,通过自己实现ICriteriaBuilder来实现自定义查询(例如可以实现存贮过程和视图的访问,当然访问存贮过程和视图并不是被广泛推荐的一种好方式)。
浙公网安备 33010602011771号