Hibernate 2.1的一个重大改进是,我们终于拥有了一个成熟的Criteria查询API。在很长的一段时间里,我都让这个特性停滞不前,因为我并不确定它应该是什么样子。我看过每一个QBC API,它们的实现方式都不同,而且根本没有什么标准API可以参考。我见过从这样
new Criteria(Project.class) .addEq("name", "Hibernate") .addLike("description", "%ORM%") .execute();
到这样
Criteria crit = new Criteria(Project.class) crit.getProperty("name").eq("Hibernate); crit.getProperty("description).like("%ORM%"); crit.execute();
我不喜欢这两种方法,因为添加新的条件类型会导致单个中心接口(在第一种情况下是Criteria;在第二种情况下是Property)的无序增长。
我甚至更不喜欢第二种方法,因为它很难串联方法调用。eq()方法应该返回什么?好吧,似乎最合理的是它应该返回接收对象(即属性)。但是,将多个条件应用于同一属性是非常不寻常的!所以,如果我们想串联方法调用,我们确实更喜欢它返回Criteria。好吧,我不知道你,但我觉得任何返回两个调用之前的接收者的API可能被认为不“直观”。所以我们就陷入了那个邪恶的临时变量。
我认真考虑过改进第二种方法,使其看起来像这样
new Criteria(Project.class) .getProperty("name").eq("Hibernate) .and() .getProperty("description).like("%ORM%"); .execute();
这实际上非常干净。不幸的是,接口本身相当奇特:and()是由... Criterion定义的操作?and()方法返回... Criteria?这感觉不像是一种非常自然的OO设计。而且我认为这会令新用户感到困惑。我稍后会回到一个理由,为什么“and”和“or”根本不应该成为操作。
作为第一种方法的变体,我还见过以下情况
new Criteria(Project.class) .add( new Equals("name", "Hibernate") ) .add( new Like("description", "%ORM%") ) .execute();
这避免了Criteria接口失去控制的问题。但我对Java构造函数的厌恶程度几乎和临时变量一样!Java中构造函数的问题在于它们不能被赋予有意义的名称。如果我们有一个名为Equals的类,我们不能将其构造函数命名为EqualsIgnoreCase()。其次,一旦我们开始使用构造函数,我们就基本上永久地确定了Criterion类层次结构。我们将客户端代码直接绑定到具体类。我不能事后改变主意,决定Equals和EqualsIgnoreCase应该是不同的类。
最终,我被Cayenne查询API(我认为它是受Apple的WebObjects影响的)影响最大。Cayenne使用一个具有静态工厂方法的类来创建Criterion实例。实际上,Cayenne错误地将criterion类命名为Expression,我在我们的(Hibernate)工厂类中愚蠢地继承了这种错误命名。所以,我们最终有了
session.createCriteria(Project.class) .add( Expression.eq("name", "Hibernate") ) .add( Expression.like("description", "%ORM%") ) .list();
请注意,这段代码没有使用任何具体类,除了静态工厂类——它全是接口!
这种设计的缺点是,在“add(Expression.eq())”中比在“add(new Eq())”或“addEq()”中使用的字符更多。所以它确实更冗长。它也很嘈杂。上面的代码中突出的两处是“Expression”。但它们在代码中是最不重要的。
幸运的是,JDK1.5将出现,并给我们带来静态导入。静态导入在过去一直被不公正地诽谤,所以让我尝试纠正记录。如果我添加import net.sf.hibernate.expression.Expression.*,上面的代码示例就变成了
session.createCriteria(Project.class) .add( eq("name", "Hibernate") ) .add( like("description", "%ORM%") ) .list();
现在比使用构造函数的版本更简洁、更易读。我完成了一半。
第二个问题是criterion的逻辑组合。和和或都是结合的,但一串和和或显然不是结合的。因此,我认为逻辑运算符的优先级从代码结构中清晰可见至关重要。我讨厌以下内容
session.createCriteria(Project.class) .addAnd( eq("name", "Hibernate") ) .addAnd( like("description", "%ORM%") ) .addOr( like("description", "%object/relational%") ) .list();
我已经看到很多这样的API,但我仍然不清楚前一段代码的意图是如何读取的。同样的问题也适用于这种变化
new Criteria(Project.class) .getProperty("name").eq("Hibernate) .and() .getProperty("description).like("%ORM%") .or() .getProperty("description).like("%object/relational%") .execute();
好吧,好吧,我实际上知道连接通常比分离具有更高的优先级——但我永远不会编写依赖于这种优先级的代码。这根本不可读。我们当然不能总是依赖于运算符优先级——我们需要某种方式来表达分组。无论如何,我认为这个问题会影响任何提供and()和or()作为方法的API。所以,我们不要让and()和or()成为操作。
顺便说一下,最糟糕的是这个
new Criteria(Project.class) .getProperty("name").eq("Hibernate) .and( crit.getProperty("description).like("%ORM%") ) .execute();
和是一个对称操作!这种对称性应该是显而易见的。
解决方案是将连接和分离以完全相同的方式处理为原子criterion。使它们成为criterion,而不是操作。
session.createCriteria(Project.class) .add( Expression.disjunction() .add( eq("name", "Hibernate") ) .add( like("description", "%ORM%") ) ) .list();
嗯,这对我来说太多了几个括号。我正在考虑在Hibernate中支持以下内容
session.createCriteria(Project.class) .createDisjunction() .add( eq("name", "Hibernate") ) .add( like("description", "%ORM%") ) .list();
我这里的最大问题是createDisjunction()需要返回一个Criteria的新实例(包装Disjunction),这样我们就可以调用list()而不需要新的临时变量。我不确定是否喜欢这个。目前Expression.disjunction()直接返回一个Disjunction的实例——而Disjunction只实现Criterion接口。我想我们仍在追求完美...