Hibernate查询语言(Query Language), 即HQL
Hibernate装备了一种极为有力的查询语言,(有意地)看上去很像SQL。但是别被语法蒙蔽,HQL是完全面向对象的,具备继承、多态和关联等特性。
10.1.大小写敏感性(CaseSensitivity)
除了Java类和属性名称外,查询都是大小写不敏感的。所以,SeLeCT和sELEct以及SELECT相同的,但是net.sf.hibernate.eg.FOO和net.sf.hibernate.eg.Foo是不同的,foo.barSet和foo.BARSET也是不同的。
本手册使用小写的HQL关键词。有些用户认为在查询中使用大写的关键字更加易读,但是我们认为嵌入在Java代码中这样很难看。
10.2.from子句
可能最简单的Hibernate查询是这样的形式:
fromeg.Cat它简单的返回所有eg.Cat类的实例。
大部分情况下,你需要赋予它一个别名(alias),因为你在查询的其他地方也会引用这个Cat。
fromeg.Catascat上面的语句为Cat赋予了一个别名cat。所以后面的查询可以用这个简单的别名了。as关键字是可以省略的,我们也可以写成这样:
fromeg.Catcat可以出现多个类,结果是它们的笛卡尔积,或者称为“交叉”连接。
fromFormula,ParameterfromFormulaasform,Parameterasparam让查询中的别名服从首字母小写的规则,我们认为这是一个好习惯。这和Java对局部变量的命名规范是一致的。(比如,domesticCat).
10.3.联合(Associations)和连接(joins)
你可以使用join定义两个实体的连接,同时指明别名。
fromeg.Catascatinnerjoincat.mateasmateleftouterjoincat.kittensaskittenfromeg.Catascatleftjoincat.mate.kittensaskittensfromFormulaformfulljoinform.parameterparam支持的连接类型是从ANSISQL借用的:
内连接,innerjoin
左外连接,leftouterjoin
右外连接,rightouterjoin
全连接,fulljoin(不常使用)
innerjoin,leftouterjoin和rightouterjoin都可以简写。
fromeg.Catascatjoincat.mateasmateleftjoincat.kittensaskitten并且,加上"fetch"后缀的抓取连接可以让联合的对象随着它们的父对象的初始化而初始化,只需要一个select语句。这在初始化一个集合的时候特别有用。
fromeg.Catascatinnerjoinfetchcat.mateleftjoinfetchcat.kittens抓取连接一般不需要赋予别名,因为被联合的对象应该不会在where子句(或者任何其它子句)中出现。并且,被联合的对象也不会在查询结果中直接出现。它们是通过父对象进行访问的。
请注意,目前的实现中,在一次查询中只会抓取一个集合(?原文为:onlyonecollectionrolemaybefetchedinaquery)。也请注意,在使用scroll()或者iterate()方式调用的查询中,是禁止使用fetch构造的。最后,请注意fulljoinfetch和rightjoinfetch是没有意义的。
10.4.select子句
select子句选择在结果集中返回哪些对象和属性。思考一下下面的例子:
selectmatefromeg.Catascatinnerjoincat.mateasmate这个查询会选择出作为其它猫(Cat)朋友(mate)的那些猫。当然,你可以更加直接的写成下面的形式:
selectcat.matefromeg.Catcat你甚至可以选择集合元素,使用特殊的elements功能。下面的查询返回所有猫的小猫。
selectelements(cat.kittens)fromeg.Catcat查询可以返回任何值类型的属性,包括组件类型的属性:
selectcat.namefromeg.DomesticCatcatwherecat.namelike'fri%'selectcust.name.firstNamefromCustomerascust查询可以用元素类型是Object[]的一个数组返回多个对象和/或多个属性。
selectmother,offspr,mate.namefromeg.DomesticCatasmotherinnerjoinmother.mateasmateleftouterjoinmother.kittensasoffspr或者实际上是类型安全的Java对象
selectnewFamily(mother,mate,offspr)fromeg.DomesticCatasmotherjoinmother.mateasmateleftjoinmother.kittensasoffspr上面的代码假定Family有一个合适的构造函数。
10.5.统计函数(Aggregatefunctions)
查询可以返回属性的统计函数。
selectavg(cat.weight),sum(cat.weight),max(cat.weight),count(cat)fromeg.Catcat在select子句中,统计函数的变量也可以是集合。
selectcat,count(elements(cat.kittens))fromeg.Catcatgroupbycat下面是支持的统计函数列表:
avg(...),sum(...),min(...),max(...)
count(*)
count(...),count(distinct...),count(all...)
distinct和all关键字的用法和语义与SQL相同。
selectdistinctcat.namefromeg.Catcatselectcount(distinctcat.name),count(cat)fromeg.Catcat10.6.多态(polymorphism)
类似下面的查询:
fromeg.Catascat返回的实例不仅仅是Cat,也有可能是子类的实例,比如DomesticCat。Hibernate查询可以在from子句中使用任何Java类或者接口的名字。查询可能返回所有继承自这个类或者实现这个接口的持久化类的实例。下列查询会返回所有的持久化对象:
fromjava.lang.Objecto可能有多个持久化类都实现了Named接口:
fromeg.Namedn,eg.Namedmwheren.name=m.name请注意,上面两个查询都使用了超过一个SQL的SELECT。这意味着orderby子句将不会正确排序。(这也意味着你不能对这些查询使用Query.scroll()。)
10.7.where子句
where子句让你缩小你要返回的实例的列表范围。
fromeg.Catascatwherecat.name='Fritz'返回所有名字为'Fritz'的Cat的实例。
selectfoofromeg.Foofoo,eg.Barbarwherefoo.startDate=bar.date会返回所有的满足下列条件的Foo实例,它们存在一个对应的bar实例,其date属性与Foo的startDate属性相等。复合路径表达式令where子句变得极为有力。思考下面的例子:
fromeg.Catcatwherecat.mate.nameisnotnull这个查询会被翻译为带有一个表间(inner)join的SQL查询。如果你写下类似这样的语句:
fromeg.Foofoowherefoo.bar.baz.customer.address.cityisnotnull你最终会得到的查询,其对应的SQL需要4个表间连接。
=操作符不仅仅用于判断属性是否相等,也可以用于实例:
fromeg.Catcat,eg.Catrivalwherecat.mate=rival.mateselectcat,matefromeg.Catcat,eg.Catmatewherecat.mate=mate特别的,小写的id可以用来表示一个对象的惟一标识。(你可以使用它的属性名。)
fromeg.Catascatwherecat.id=123fromeg.Catascatwherecat.mate.id=69第二个查询是很高效的。不需要进行表间连接!
组合的标示符也可以使用。假设Person有一个组合标示符,是由country和medicareNumber组合而成的。
frombank.Personpersonwhereperson.id.country='AU'andperson.id.medicareNumber=123456frombank.Accountaccountwhereaccount.owner.id.country='AU'andaccount.owner.id.medicareNumber=123456又一次,第二个查询不需要表间连接。
类似的,在存在多态持久化的情况下,特殊属性class用于获取某个实例的辨识值。在where子句中嵌入的Java类名将会转换为它的辨识值。
fromeg.Catcatwherecat.class=eg.DomesticCat你也可以指定组件(或者是组件的组件,依次类推)或者组合类型中的属性。但是在一个存在路径的表达式中,最后不能以一个组件类型的属性结尾。(这里不是指组件的属性)。比如,假若store.owner这个实体的的address是一个组件
store.owner.address.city//okaystore.owner.address//error!“任意(any)”类型也有特殊的id属性和class属性,这可以让我们用下面的形式来表达连接(这里AuditLog.item是一个对应到<ant>的属性)。
fromeg.AuditLoglog,eg.Paymentpaymentwherelog.item.class='eg.Payment'andlog.item.id=payment.id注意上面查询中,log.item.class和payment.class会指向两个值,代表完全不同的数据库字段。
10.8.表达式(Expressions)
where子句允许出现的表达式包括了你在SQL中可以使用的大多数情况:
数学操作+,-,*,/
真假比较操作=,>=,<=,<>,!=,like
逻辑操作and,or,not
字符串连接||
SQL标量(scalar)函数,例如upper()和lower()
没有前缀的()表示分组
in,between,isnull
JDBC传入参数?
命名参数:name,:start_date,1
SQL文字'foo',69,'1970-01-0110:00:01.0'
Java的publicstaticfinal常量比如Color.TABBY
in和between可以如下例一样使用:
fromeg.DomesticCatcatwherecat.namebetween'A'and'B'fromeg.DomesticCatcatwherecat.namein('Foo','Bar','Baz')其否定形式为
fromeg.DomesticCatcatwherecat.namenotbetween'A'and'B'fromeg.DomesticCatcatwherecat.namenotin('Foo','Bar','Baz')类似的,isnull和isnotnull可以用来测试null值。
通过在Hibernate配置中声明HQL查询的替换方式,Boolean也是很容易在表达式中使用的:
<propertyname="hibernate.query.substitutions">true1,false0</property>在从HQL翻译成SQL的时候,关键字true和false就会被替换成1和0。
fromeg.Catcatwherecat.alive=true你可以用特殊属性size来测试一个集合的长度,或者用特殊的size()函数也可以。
fromeg.Catcatwherecat.kittens.size>0fromeg.Catcatwheresize(cat.kittens)>0对于排序集合,你可以用minIndex和maxIndex来获取其最大索引值和最小索引值。类似的,minElement和maxElement可以用来获取集合中最小和最大的元素,前提是必须是基本类型的集合。
fromCalendarcalwherecal.holidays.maxElement>currentdate也有函数的形式(和上面的形式不同,函数形式是大小写不敏感的):
fromOrderorderwheremaxindex(order.items)>100fromOrderorderwhereminelement(order.items)>10000SQL中的any,some,all,exists,in功能也是支持的,前提是必须把集合的元素或者索引集作为它们的参数(使用element和indices函数),或者使用子查询的结果作为参数。
selectmotherfromeg.Catasmother,eg.Cataskitwherekitinelements(foo.kittens)selectpfromeg.NameListlist,eg.Personpwherep.name=someelements(list.names)fromeg.Catcatwhereexistselements(cat.kittens)fromeg.Playerpwhere3>allelements(p.scores)fromeg.Showshowwhere'fizard'inindices(show.acts)请注意这些设施:size,elements,indices,minIndex,maxIndex,minElement,maxElement都有一些使用限制:
在where子句中:只对支持子查询的数据库有效
在select子句中:只有elements和indices有效
有序的集合(数组、list、map)的元素可以用索引来进行引用(只限于在where子句中)
fromOrderorderwhereorder.items[0].id=1234selectpersonfromPersonperson,Calendarcalendarwherecalendar.holidays['nationalday']=person.birthDayandperson.nationality.calendar=calendarselectitemfromItemitem,Orderorderwhereorder.items[order.deliveredItemIndices[0]]=itemandorder.id=11selectitemfromItemitem,Orderorderwhereorder.items[maxindex(order.items)]=itemandorder.id=11[]中的表达式允许是另一个数学表达式。
selectitemfromItemitem,Orderorderwhereorder.items[size(order.items)-1]=itemHQL也对一对多关联或者值集合提供内置的index()函数。
selectitem,index(item)fromOrderorderjoinorder.itemsitemwhereindex(item)<5底层数据库支持的标量SQL函数也可以使用
fromeg.DomesticCatcatwhereupper(cat.name)like'FRI%'假如以上的这些还没有让你信服的话,请想象一下下面的查询假若用SQL来写,会变得多么长,多么不可读:
selectcustfromProductprod,Storestoreinnerjoinstore.customerscustwhereprod.name='widget'andstore.location.namein('Melbourne','Sydney')andprod=allelements(cust.currentOrder.lineItems)提示:对应的SQL语句可能是这样的
SELECTcust.name,cust.address,cust.phone,cust.id,cust.current_orderFROMcustomerscust,storesstore,locationsloc,store_customerssc,productprodWHEREprod.name='widget'ANDstore.loc_id=loc.idANDloc.nameIN('Melbourne','Sydney')ANDsc.store_id=store.idANDsc.cust_id=cust.idANDprod.id=ALL(SELECTitem.prod_idFROMline_itemsitem,ordersoWHEREitem.order_id=o.idANDcust.current_order=o.id)10.9.orderby子句
查询返回的列表可以按照任何返回的类或者组件的属性排序:
fromeg.DomesticCatcatorderbycat.nameasc,cat.weightdesc,cat.birthdateasc和desc是可选的,分别代表升序或者降序。
10.10.groupby子句
返回统计值的查询可以按照返回的类或者组件的任何属性排序:
selectcat.color,sum(cat.weight),count(cat)fromeg.Catcatgroupbycat.colorselectfoo.id,avg(elements(foo.names)),max(indices(foo.names))fromeg.Foofoogroupbyfoo.id请注意:你可以在select子句中使用elements和indices指令,即使你的数据库不支持子查询也可以。
having子句也是允许的。
selectcat.color,sum(cat.weight),count(cat)fromeg.Catcatgroupbycat.colorhavingcat.colorin(eg.Color.TABBY,eg.Color.BLACK)在having子句中允许出现SQL函数和统计函数,当然这需要底层数据库支持才行。(比如说,MySQL就不支持)
selectcatfromeg.Catcatjoincat.kittenskittengroupbycathavingavg(kitten.weight)>100orderbycount(kitten)asc,sum(kitten.weight)desc注意,groupby子句和orderby子句都不支持数学表达式。
10.11.子查询
对于支持子查询的数据库来说,Hibernate支持在查询中嵌套子查询。子查询必须由圆括号包围(常常是在一个SQL统计函数中)。也允许关联子查询(在外部查询中作为一个别名出现的子查询)。
fromeg.Catasfatcatwherefatcat.weight>(selectavg(cat.weight)fromeg.DomesticCatcat)fromeg.DomesticCatascatwherecat.name=some(selectname.nickNamefromeg.Nameasname)fromeg.Catascatwherenotexists(fromeg.Catasmatewheremate.mate=cat)fromeg.DomesticCatascatwherecat.namenotin(selectname.nickNamefromeg.Nameasname)10.12.示例
Hibernate查询可以非常强大复杂。实际上,强有力的查询语言是Hibernate的主要卖点之一。下面给出的示例与我在近期实际项目中使用的一些查询很类似。请注意你编写的查询大部分等都不会这么复杂!
下面的查询对特定的客户,根据给定的最小总计值(minAmount),查询出所有未付订单,返回其订单号、货品总数、订单总金额,结果按照总金额排序。在决定价格的时候,参考当前目录。产生的SQL查询,在ORDER,ORDER_LINE,PRODUCT,CATALOG和PRICE表之间有四个内部连接和一个没有产生关联的字查询。
selectorder.id,sum(price.amount),count(item)fromOrderasorderjoinorder.lineItemsasitemjoinitem.productasproduct,Catalogascatalogjoincatalog.pricesaspricewhereorder.paid=falseandorder.customer=:customerandprice.product=productandcatalog.effectiveDate<sysdateandcatalog.effectiveDate>=all(selectcat.effectiveDatefromCatalogascatwherecat.effectiveDate<sysdate)groupbyorderhavingsum(price.amount)>:minAmountorderbysum(price.amount)desc好家伙,真长!实际上,在现实生活中我并不是非常热衷于子查询,所以我的查询往往是这样的:
selectorder.id,sum(price.amount),count(item)fromOrderasorderjoinorder.lineItemsasitemjoinitem.productasproduct,Catalogascatalogjoincatalog.pricesaspricewhereorder.paid=falseandorder.customer=:customerandprice.product=productandcatalog=:currentCataloggroupbyorderhavingsum(price.amount)>:minAmountorderbysum(price.amount)desc下面的查询统计付款记录处于每种状态中的数量,要排除所有处于AWAITING_APPROVAL状态的,或者最近一次状态更改是由当前用户做出的。它翻译成SQL查询后,在PAYMENT,PAYMENT_STATUS和PAYMENT_STATUS_CHANGE表之间包含两个内部连接和一个用于关联的子查询。
selectcount(payment),status.namefromPaymentaspaymentjoinpayment.currentStatusasstatusjoinpayment.statusChangesasstatusChangewherepayment.status.name<>PaymentStatus.AWAITING_APPROVALor(statusChange.timeStamp=(selectmax(change.timeStamp)fromPaymentStatusChangechangewherechange.payment=payment)andstatusChange.user<>:currentUser)groupbystatus.name,status.sortOrderorderbystatus.sortOrder假若我已经把statusChange集合映射为一个列表而不是一个集合的话,查询写起来会简单很多。
selectcount(payment),status.namefromPaymentaspaymentjoinpayment.currentStatusasstatuswherepayment.status.name<>PaymentStatus.AWAITING_APPROVALorpayment.statusChanges[maxIndex(payment.statusChanges)].user<>:currentUsergroupbystatus.name,status.sortOrderorderbystatus.sortOrder下面的查询使用了MSSQLServer的isNull()函数,返回当前用户所属的组织所有账户和未付支出。翻译为SQL查询后,在ACCOUNT,PAYMENT,PAYMENT_STATUS,ACCOUNT_TYPE,ORGANIZATION和ORG_USER表之间有三个内部连接,一个外部连接和一个子查询。
selectaccount,paymentfromAccountasaccountleftouterjoinaccount.paymentsaspaymentwhere:currentUserinelements(account.holder.users)andPaymentStatus.UNPAID=isNull(payment.currentStatus.name,PaymentStatus.UNPAID)orderbyaccount.type.sortOrder,account.accountNumber,payment.dueDate对某些数据库而言,我们可能不能依赖(关联的)子查询。
selectaccount,paymentfromAccountasaccountjoinaccount.holder.usersasuserleftouterjoinaccount.paymentsaspaymentwhere:currentUser=userandPaymentStatus.UNPAID=isNull(payment.currentStatus.name,PaymentStatus.UNPAID)orderbyaccount.type.sortOrder,account.accountNumber,payment.dueDate10.13.提示和技巧(Tips&Tricks)
你不返回结果集也可以查询结果集的大小:
((Integer)session.iterate("selectcount(*)from....").next()).intValue()要依据一个集合的大小对结果集排序,可以用下面的查询来对付一对多或多对多的关联:
selectusrfromUserasusrleftjoinusr.messagesasmsggroupbyusrorderbycount(msg)如果你的数据库支持子查询,你可以在查询的where子句中对选择的大小进行条件限制:
fromUserusrwheresize(usr.messages)>=1如果你的数据库不支持子查询,可以使用下列查询:
selectusr.id,usr.namefromUserusr.namejoinusr.messagesmsggroupbyusr.id,usr.namehavingcount(msg)>=1因为使用了innerjoin,这个解决方法不能返回没有message的User.下面的方式就可以:
selectusrfromUserasusrleftjoinusr.messagesasmsggroupbyusrhavingcount(msg)=0JavaBean的属性可以直接作为命名的查询参数:
Queryq=s.createQuery("fromfooinclassFoowherefoo.name=:nameandfoo.size=:size");q.setProperties(fooBean);//fooBeanhasgetName()andgetSize()Listfoos=q.list();在Query接口中使用过滤器(filter),可以对集合分页:
Queryq=s.createFilter(collection,"");//thetrivialfilterq.setMaxResults(PAGE_SIZE);q.setFirstResult(PAGE_SIZE*pageNumber);Listpage=q.list();集合元素可以使用查询过滤器(queryfilter)进行排序或者分组:
ListorderedCollection=s.filter(collection,"orderbythis.amount");Listcounts=s.filter(collection,"selectthis.type,count(this)groupbythis.type");不用初始化集合就可以得到其大小:
((Integer)session.iterate("selectcount(*)from....").next()).intValue();