欢迎访问我的网站交流技术 www.ithuzhu.com
这是www.sourcebeat.com上JPA101一书的样章,可以从TheServerSide.com上下载,作者blog是http://www.jroller.com/cmaki/entry/jpa_presentation,翻译问题请留言,修正之后会发布pdf版本。
摘要
本章探讨Java持久化查询语言(JPQL),在简单的向你介绍JPQL后,会直接切入创建查询这个话题。本章会涉及一个查询的方方面面,包括fetch join
操作。fetch join
操作会提前读取延时关联(lazy relationship),以消除LazyInitializationException
异常,这在使用ORM方案时,常常困扰很多应用程序的开发。接下来,会了解一下JPQL的打量操作支持。学习完本章时,你会对JPQL有一个全面的认识。
JPQL是一种与数据库无关的,基于实体(entity-based)的查询语言。它是EJB QL的一个扩展,并加入了很多EJB QL中所没有的新特性。JPQL支持projection(可以查询某个实体的字段而不需要查询整个实体),批量操作(update和delete),子查询,join,group by和having操作。所有的JPQL操作都在静态查询(命名查询,named query)和动态查询中得到支持。另外,JPQL支持多态,当读取Rank
会返回SpecialRank
和PostCountRank
实例(instance)(Rank
是一个抽象类,JPQL只返回具体类)。
JPQL在语法上与SQL相似:“ select from [where] [group by] [having] [order by]
”。如果你熟悉SQL,那么对大部分的JPQL语法也不会陌生。JPQL同样支持SQL类似的函数功能,如max和min,但是并不能取代SQL。不管如何相似,但两者之间有一个重要的区别,JPQL操作的是“抽象持久化模型(abstract persistence schema)”,而不是数据库定义的物理模型。抽象持久化模型通常是用JPA元数据(metadata)在ORM文件中或注释(annotation)中进行定义(有关注释和ORM的更多信息参见第2章Entities Part I,第3章Entities Part II,第4章Packaging)。抽象持久化模型包括实体,它们的字段及定义的所有关联关系。JPA持久化工具可以将JPQL转换成内置的SQL查询语句。这就意味着你可以在查询语句的SELECT
指定一个实体或是它的字段,或者在WHERE
语句中使用多个实体。你可以用点号(.)来访问查询语句中的SELECT
部分的关联关系。下面这个例子是访问agoraBB
对象模型中从PrivateMessage
到User
的关联。
SELECT p.toUser FROM PrivateMessage p
这个查询会得到PrivateMessage
所属的User
。
虽然可以说JPQL可以将所有的操作当成查询,但实际上只有三种不同的“查询”:SELECT
查询(真正可以算得上的一个查询),更新查询(不是查询而是更新语句),或者删除查询(也不是真正的查询而是删除语句)。JPA规范中将所有的这些查询当成查询,你可以用同样的语法定义这三种查询,它们之间的差别会反应到数据库上。
本章包含以下话题:
-
JPQL概述
-
Join
-
Where
,Group By
,Having
-
SELECT
语句 -
批量操作支持
-
实例
JPQL操作的就是抽象持久化模型,即一个持久化单元(persistence unit
)中实体所表示的模型。使用SQL,你可以直接查询数据库中表的字段;使用JPQL,你可以查找实体的属性。一个JPQL语句的各个组成部分都是针对实体或者实体的属性进行操作。你根本不会用到一个实体所映射的数据库表和字段。下面是你可以创建紧最基本的JPQL语句:
SELECT u FROM User
查询从数据库返回所有的User
实例。
注意
除实体名称和实体字段外,JPQL查询不区分大小写。所以在查询的其它部分你可以使用任意形式。本章中,所有JPA保留标志符我们使用大写,在你的应用程序中并不要求要这么做。
JPQL是一种强类型语言,一个JPQL语句中每个表达式都有类型。
注意
强类型语言中规定某种表达式对某种类型是合法的。如果在Java中编译"123"+3,你会得到一个类型不相容的编译器错误。
例如,有下面这样一个查询语句:
SELECT u.username FROM User u
在这个查询语句中,u.username
一个表达式的结果是一个String
类型。这个查询会返回一个String
对象的list。list中的各个对象代表系统中一个不同的用户名。JPQL规范将一个实体(属性)中所饮食的各种类型称为抽象模型类型
(abstract schema type
)。
一个实体可以包含一个或多个下面的抽象模型类型:
-
状态字段(state-field):这种类型包括一个实体不代表关联关系的任何字段或属性。字段的类型或者一个属性get方法的返回结果决定了状态字段的抽象模型类型。
-
关联字段(association-field):这种类型包括了一个实体中任何表示关联关系的字段或属性。关联字段的抽象模型类型就是目标实体的抽象模型类型。
JPQL语句的作用范围是由你执行查询语句中持久化单元(persistence unit)的实体决定的。
在一个查询中,实体是通过实体名称来引用的。实体的名称就是你使用@Entity
注释时指定的名称,或者是实体的限定词(unqualified name)。假如有这样一个实体。
package com.sourcebeat.jpa.model; @Entity(name="message") public class PrivateMessage extends ModelBase { // fields/methods removed for readability }
实体名称是“ message
”,因为你在使用@Entity
时候指定了名称(在一个持久化单元内实体名称必须是唯一的)。在下面的代码中,实体的限定词是User
(以一个大写的“U”开头)。实体的全名是com.sourcebeat.jpa.model.User
,但它的限定词 是User
。
package com.sourcebeat.jpa.model; @Entity public class User extends ModelBase { // fields/methods removed for readability }
当我们在JPQL中引用实体时,必须使用@Entity
注释指定的名称或是实体的限定词 。一些JPA实现(如,Hibernate)允许你使用全名,但有的(如,Toplink)则抛出异常。为了保证移植性,在查询中最好使用限定词 。
一个标识变量(identification variable)是一个在FROM
语句中指定的一个标志符号。在下面的查询语句中,u
就是一个标识变量。u
类型就是能够识别名称User
的实体的抽象模型类型。在本例中,实体是User
(前面已经定义),所以u
的类型是User
。
SELECT u FROM User u
标识变量也可以使用JOIN
关键字定义,例如。
SELECT r.name FROM User u JOIN u.roles r
这里,标识变量有u
和r
。r
表示任何能够从一个User
实例中直接访问的Role
。
JPA定义了以下保留标志符(虽然下面列表中显示是大写形式,保留标志符并不区分大小写,所以SELECT
和select
或SelECt
等同。)
SELECT, FROM, WHERE, UPDATE, DELETE, JOIN, OUTER, INNER, LEFT, GROUP, BY, HAVING, FETCH, DISTINCT, OBJECT, NULL, TRUE, FALSE, NOT, AND, OR, BETWEEN, LIKE, IN, AS, UNKNOWN, EMPTY, MEMBER, OF, IS, AVG, MAX, MIN, SUM, COUNT, ORDER, BY, ASC, DESC, MOD, UPPER, LOWER, TRIM, POSITION, CHARACTER_LENGTH, CHAR_LENGTH, BIT_LENGTH, CURRENT_TIME, CURRENT_DATE, CURRENT_TIMESTAMP, NEW, EXISTS, ALL, ANY, SOME.
UNKNOWN
目前在JPQL中还没有用到。
路径表达式就是一个标识符号紧跟一个访问操作符(.)再紧跟一个状态字段或是关联字段。只要它们不是集合(collections),就可以遍历关系。下图是agoraBB
项目对象模型的一部分,用于解释路径表达式。
PrivateMessage
所属于的User
的关系是由PrivateMessage
的关联关系字段toUser
表示的。User
有一个关联到Role
的集合关联字段roles
,还有一个关联到UserIpAddress
的字段userIpAddress
。
由于在在这些关系,你可以进行以下操作。
-
PrivateMessage
所属用户User
的IP地址UserIpAddresses
SELECT p.toUser.userIPAddresses from PrivateMessage p
-
PrivateMessage
所属用户User
的角色Role
SELECT p.toUser.roles from PrivateMessage p
-
User
的角色Role
,获取所有的无重复的角色名称。SELECT DISTINCT r.name FROM User u JOIN u.roles r
-
User
的IP地址UserIpAddresses
SELECT DISTINCT u.userIPAddresses FROM User u
下图关系略有不同。
图中,论坛Forum
与其主题Topics
的关系由Forum
的字段topics
表示,从Topics
到Post
的关系由Topic
的字段topics
表示。从对象关系图可以看出哪些JPQL是合法的,哪些不合法。
-
查询论坛
Forum
的主题Topic
,要获取标题(subject):路径表达是不合法的,不能直接通过集合访问。SELECT f.topics.subject FROM Forum f – ILLEGAL
-
查询论坛
Forum
主题Topic
的帖子Post
,使用JOIN
操作符,表达式是合法的。SELECT t.subject FROM Forum f JOIN f.topics AS t
-
查询论坛
Forum
主题Topic
的帖子Post
,,路径表达是不合法的,不能直接通过集合访问。SELECT f.topics.posts FROM Forum f – ILLEGAL
-
查询论坛
Forum
主题Topic
的帖子Post
,使用JOIN
操作符,表达式是合法的。SELECT p FROM Forum f JOIN f.topics t JOIN t.posts p
注意
虽然JPA规范中明确表示无法访问一个集合关系字段,据我在Hibernate和Toplink上的测试,在一个目标实体中可以访问集合关联字段或状态字段。在JPA的BNF范式针对路径访问指明了从Forum
访问Topic
,所以读取主题就是非法的。如果你要访问一个集合字段的目标,使用第二个例子如示JOIN
语法。
总之,你可以使用导航操作符(.)来遍历实体对象关系图,查询的类型是由SELECT
语句决定的。SELECT
语句可以包含标识变量和路径表达式。路径表达式可以遍历整个对象关系图,只要你从左到右访问的是单值关联字段,你无法访问一个集合字段或是一个状态关联字段。
界定变量使用与&SQL相似的语法,将实体名绑定到一个标志符上。界定变量声明如下(在一个查询语句的SELECT
语句):
entityName [AS] identification_variable
你可以通过使用多个界定变量来使用同一实体,下面的例子来自JPA规范。
SELECT DISTINCT o1 FROM Order o1, Order o2 WHERE o1.quantity > o2.quantity AND o2.customer.lastname = ‘Smith’ AND o2.customer.firstname= ‘John’
此查询返回大于John Smith的定单的所有的订单。
JOINS
join出现在两个或多个实体联合查询产生一个JPQL查询结果。JPQL中join与SQL中的SQL相似。最后,要说明的是,所有的JPQL会转换成SQL查询。出现以下情况时,就可以用上join。
-
访问集合关联字段的路径表达式出现在
SELECT
语句 -
join保留字出现在
WHERE
语句中 -
定义两个或多个界定变量
如果你在一个查询中使用了一个以上的实体,你会获取所有实体的实例。这一结果称为Cartesian product(卡笛尔产品)。假使你的系统中有8个角色和4个用户,下面的查询会得到32个对象。
SELECT r, u FROM Role r, User u
你可能想用某种join来减少查询结果的数量。如果你想通过字段而不是通过主键来关联实体,你可以使用theta-join。例如:
SELECT t FROM Topic t, Forum f WHERE t.postCount = f.forumPostCount
这条查询语句返回的是与论坛回复(Forum)数相同的主题(Topic)。theta-join允许你关联那些没有显式关联关系或者关联到没有关系但是等同的信息的实体。
JPQL中inner join也是用于关系联合。其语法如下:
[INNER] JOIN join_association_path_expression [AS] identification_variable
INNER
和AS
都是可选的。虽然它们对查询没有什么影响,你还是可以使用它们来更加清晰的表达你的意图。
join_assocation_path_expression
的意思是你可以访问一个关联实体,不管是单值关联还是一个集合。下图演示了两个inner join查询:
因为你不能在SELECT
语句中使用一个集合关联字段,JPQL为你提供了INNER JOIN
操作符。如果你想访问Forum-Topic-Post
的关联关系(如图2所示),获得所有Post的标题,你可以使用这样一条查询语句:
SELECT p.title FROM Forum f JOIN f.topics AS t JOIN t.posts AS p
这条查询会返回0个或多个String
对象,封装的是Forum-Topic-Post
联合查询的回复的标题。
一个outer join会返回一个实体的所有实例和其它与join criteria匹配的其它实体的实例。一个left join的语法如下:
LEFT [OUTER] JOIN join_association_path_expression [AS] identification_variable
[OUTER]是可选的,因为在JPQL中LEFT JOIN
和LEFT OUTER JOIN
以认为是等同的。使用上图中所示的Forum/Post
实体关系,下面的left join操作可以读取所有的Forum
和任何与Forum
关联的Topic
。如果找不到Topic
,Object
数组的第2项的值就是null。
SELECT f, t FROM Forum f LEFT JOIN f.topics t
使用范例数据库,上面的查询会返回以下结果:
[ Object: [Forum] Object: [Topic] ] [ Object: [Forum] Object: [null] ] [ Object: [Forum] Object: [null] ] [ Object: [Forum] Object: [null] ] [ Object: [Forum] Object: [null] ]
这个查询返回了所有的Forum
实例和一个唯一的Topic
,因为仅有一个Forum
有一个Topic
(看起来留言板需要更多的用户参与)。
由于LEFT JOIN
操作符是预读取的一种有效方式,JPA还提供了FETCH JOIN
操作。FETCH JOIN
操作符在下一节介绍。
fetch join允许你创建查询来预读取另外一种lazy关联关系。如果你了解到在所有的实体读取之后并可能处于脱管状态时就有必要使用LAZY
关联关系,你就可以使用FETCH JOIN
来读取这种关系。FETCH JOIN
如下:
[LEFT [OUTER] INNER] JOIN FETCH join_association_path_expression
和前面定义不同的是,FETCH JOIN
没有范围变量,因为你不能在查询语句中使用隐式引用的实体。下面的查询会读取任何有主题(Topic
)的论坛(Forum
)实体,仅仅那有主题(Topic
)的论坛(Forum
)实体才会被读取。下面代码演示了如何定义论坛(Forum
)实体。注意与主题(Topic
)的关联关系是lazy。
@Entity public class Forum extends ModelBase implements Serializable { @OneToMany( fetch=FetchType.LAZY, cascade={CascadeType.PERSIST, CascadeType.MERGE}, mappedBy="forum") @OrderBy("type asc, dateUpdated desc") Set<Topic> topics = new HashSet<Topic>(); // ... }
这里有一条查询语句:
SELECT DISTINCT f FROM Forum f JOIN FETCH f.topics
因为数据库中有5个Forum实例,但仅有一个有Topic。上面的查询语句会返回一个Forum实例,Topic的关联关系也立即读取。如果你不使用DISTINCT
,持久化实现方案(persistence provider)会为系统中每个返回一个Forum实例。使用了DISTINCT
,重复的实例会被删除。
要读取所有的论坛(Forum
)实例,并立即读取它们的主题(Topic
),如下在的查询语句所示,
SELECT DISTINCT f FROM Forum f LEFT JOIN FETCH f.topics
这条返回所有唯一的论坛(Forum
)实例,并且所有的主题(topics
)已经预先读取了。使用JOIN FETCH
的一个缺点是需要知道对象模型。一旦你知道关联关系的类型,就可以用JOIN FETCH
优化你的查询。
查询中的WHERE
语句是由条件表达式组成,由它决定返回的实体。只要你将使用GROUP BY
的字段放到SELECT
语句中,你可以用GROUP BY
来统计查询结果。你还可以用HAVING
过滤查询结果。JPA并没有要求持久化实现在不使用GROUP BY
支持HAVING
,可以保持可移植性,你最好不要在GROUP BY
外使用HAVING
。
现在假设你想知道创建的agoraBB
系统中每个用户的发帖数量。从上图中可以看,没有从User
到Post
的关联关系。因为User
和Post
继承ModelBase MappedSuperclass
的,我们知道每个Post
对象有一个createdByUser
和updatedByUser
字段。
使用inner join的话,你可以写查询语句:
SELECT count(p) From Post p JOIN p.createdByUser u
问题是,这条查询会返回所有的由createdByUser
字段指定用户创建的所有帖子的数量。如果你想知道每个用户创建了多少帖子,你必须使用GROUP BY
操作符:
SELECT u, count(p) From Post p JOIN p.createdByUser u GROUP BY u
WHERE , GROUP BY , HAVING
这条查询语句会返回一个
User
实例和他所创建帖子的数量。为了进一步缩小结果范围,你可以使用HAVING
过滤分组信息(由GROUP BY
标志)。下面的查询会返回User
实例及他所发的帖子数量,前提是用户的密码是8个以上的字符。
SELECT u, count(p) From Post p JOIN p.createdByUser u GROUP BY u HAVING length(u.password) > 8条件表达式可以用在一个JPQL查询的
WHERE
和HAVING
语句中。你必须了解使用条件表达式时的一些约束。
条件表达式中包括
LOB
状态字段可能无法在数据库之间进行移植。字符串使用单引号字符串包裹,如'this'。如果要在查询中使用单引号,就要两个一起使用。你无法在查询中使用Java转义方法(例如,/'代表单引号)。布尔(Boolean)字符用
TRUE
和FALSE
表示(不区分大小写),数字字符遵从Java规范,不支持日期字符串。同样可以支持Enum
,但是你必须使用Enum,
全路径名称,如,com.sourcebeat.jpa.model.FTPType
。标识变量必须出现在一个
SELECT
或DELETE
查询的FROM
语句中。如果使用UPDATE
,那么标识变量就必须在UPDATE
语句中。标识变量往往表示他们所定义的实体类型,而不能表示一个集合中的实体。你可以使用位置或者命名形式的输入参数,但不能在某一查询混合使用这两种形式。输入参数可以出现一个查询的
WHERE
语句和(或者)HAVING
语句中。
位置形式参数的格式是以一个问号(?)打头紧跟一个以1开始的正整数。例如,?1。你可以在一个查询中多个使用同一位置参数,如下所示。
SELECT u FROM User u WHERE u.dateCreated = ?1 OR u.dateUpdated = ?1命名形式的参数用一个冒号(:)加一个Java标志符如Java变量名来表示。命名参数表示如下:
SELECT u FROM User u WHERE u.dateCreated = :aDate OR u.dateUpdated = :aDateJPQL支持函数功能,多种的
IN
,LIKE
和BETWEEN
样式表达式,及面向集合(collection)的条件表达式。这一节详细讨论写查询可用的各种选择。一个查询语句中操作符的优先级为:
导航操作符(.)
一元符号(+,-)
乘(*),除(/)
加(+),减(-)
比较操作符,=, >, >=, <, <=,<> (不等), [NOT] BETWEEN, [NOT] LIKE, [NOT] IN, IS [NOT] NULL, IS [NOT] EMPTY, [NOT] MEMBER [OF]
逻辑操作符,NOT, AND, OR
你可以用
BETWEEN
操作符指定一个实体字段的范围。BETWEEN
的语法是:
expression [NOT] BETWEEN expression AND expression这里
expression
可以是一个字符串,算术或日期时间表达式。这里有几个使用BETWEEN
操作符的实例。
SELECT u FROM User u WHERE u.dateCreated between :startDate AND :endDate SELECT t FROM Topic t WHERE t.postCount NOT BETWEEN ?1 AND ?2你可以利用
IN
比较操作符指定为一个状态字段指定一系列的值。你可以列出一个或多个字符串或参数值(基于位置或命名的),或者利用子查询动态的生成一系列的值。字符型,数字型,枚举型的状态字段可以用在IN
操作符上。状态字段的类型必须与列表中的值的类型一致。IN
操作符的语法为:
state-field [NOT] in (item {, item2}* | subquery).这里有几个例子。
SELECT f FROM Forum f WHERE f.type IN (?1, ?2) SELECT f FROM Forum f WHERE f.type IN (1, 2)
LIKE
允许你根据部分值搜索字符串字段。JPQL用一个下划线(_)表示你搜索字符字符串中任一字符。在查询语句中可以用百分号(%)表示一系列字符,其它的字符代表他们本身。LIKE
的一般格式为:
string-expression [NOT] LIKE pattern [ESCAPE escape-char]如果你必须在查询语句中使用下划线或百分号作为字面字符,使用
ESCAPE
格式。例如,你可以用forum.description like ‘QA/_%’ ESCAPE ‘/’
。你必须在下划线或百分号前加入反反斜线符号人,并且在搜索字符串后面加入ESCAPE ‘/’
语法。这里列出几个例子:
‘tr_ck’可以匹配‘truck’和‘trick’, 但不能匹配‘trucker’。
‘tr%’可以匹配‘truck’, ‘tractor’, ‘trick’, 等等。
‘tr_ck%’可以匹配‘truck’, ‘trick’, 和‘trucker’。
如果你想搜索字符串_hello,你的查询语句应该是这样的:
‘/_hello’ ESCAPE ‘/’
ESCAPE ‘/’
告诉数据库,“我正在一个转义字符('/')上使用反斜线”。下面用代码表示:
em.createQuery("SELECT f FROM Forum f " + "WHERE f.description LIKE '//_%' ESCAPE '//'");在这段代码中,你使用了两个反斜线。第一个是为了Java编译器,第二个是由于JPQL解析器。
如果在MySQL数据库上执行上面的
ESCAPE
查询语句,你可能会得到一个数据库异常。默认情况下,MySQL会将反斜线识别成一个转义符号,所以它告诉你像处理转义符号那样处理反斜线是错误的。为了使你的查询能在不同数据库之间进行移植。你必须在JDBC连接中对所有MySQL关闭所有MySQL数据库实例上的反斜线转义功能。要在你的JDBC连接上禁用反斜线转义,将下面的URL中的sessionVariables
部分添加到你的JDBC连接中。
jdbc:mysql://localhost:3306/db?sessionVariables=sql_mode=NO_BACKSLASH_ESCAPES更多信息,请参考MySQL文档。
IS NULL
比较操作符能够让检测NULL
字段,不管是单值路径表达式还是输入参数。你可以使用IS NOT NULL
来确保一个单值路径表达式有非空值,或者使用IS NULL
来检测NULL
值。
SELECT p FROM PrivateMessage p WHERE p.dateRead IS NOT NULL // toUser references a many-to-one relationship, so you can use IS [NOT] NULL SELECT p FROM PrivateMessage p WHERE p.toUser IS NOT NULL // this query does not work because we are using a // collection-value path-expression SELECT f FROM Forum f WHERE f.topics IS NULL
IS [NOT] EMPTY
操作用于空或者非空的集合值表达式。
// the above query rewritten to use IS EMPTY SELECT f FROM Forum f WHERE f.topics IS EMPTY // this query will find all forum entities with topics // (i.e. the collection is not empty) SELECT f FROM Forum f WHERE f.topics IS NOT EMPTY可以用
[NOT] MEMBER [OF]
来判断一个实体是否是一个集合的一部分。[OF]
是可选的,不影响MEMBER
比较操作符。你可用可不用。可以用
NOT MEMBER
来判断一个实体不是一个集合的组成部分。MEMBER
的语法如下:
Expression [NOT] MEMBER [OF] collection-valued path-expression // find the forum instance that contains Topic t Query q2 = em.createQuery("SELECT f FROM Forum f " + "WHERE :topic MEMBER f.topics"); q2.setParameter("topic", t); List results2 = q2.getResultList();JPQL查询的
WHERE
或HAVING
语句中支持字符串函数作为函数表达式。
CONCAT(string 1, string 2)
:将字符串2追加到字符串1。
SUBSTRING(string, starting position, length)
:从字符串string
开始位置starting position
截取长度为length
字符。
LOWER(string)
:将一个字符串string
转换成小写形式。
UPPER(string)
:将一个字符串string
转换成大写形式。
LENGTH(string)
:返回字符串string
的长度,为整数。
TRIM([[LEADING|TRAILING|BOTH] [char] FROM] string)
:去掉字符串string
头,尾或两者的字符char
。最简形式是TRIM(string)
,可以去掉字符串string
头尾的空格字符。
LOCATE(string1, string2 [,start])
:返回string2
在string1
的位置。定位函数有一个可选的起始位置start
。JPQL查询的
WHERE
或HAVING
语句中支持数学函数作为函数表达式。
ABS(arithmetic expression)
:返回算术表达式的绝对值。
SQRT(arithmetic expression)
:求算术表达式的方根,返回一个Double。
MOD(arithmetic expression 1, arithmetic expression 2)
:求参数1与参数2的模,返回一个整数。
SIZE(collection-valued path-expression)
:计算一个集合中元素的数量,并返回一个整数。如果集合为空,返回0。支持以下时间函数。
CURRENT_DATE
:当时日期,由数据库决定。
CURRENT_TIME
:当时时间,由数据库决定。
CURRENT_TIMESTAMP
:当时日期和时间,由数据库决定
SECLECT函数
SELECT
语句标识查询结果。SELECT
语句包含一个或多个下列元素。
一个路径表达式或是标识变量:表明返回一个实体。
一个单值路径表达式:指定返回一个字段或实体。
一个统计SELECT表达式:表明返回计算结果(如,
COUNT(*)
)。一个构造器表达式:允许你从选择的条目中返回一个对象。
SELECT
语句允许查询各种实体,计算结果,投影值,非实体类。你可以在SELECT
语句中使用集合值的路径表达式,然而,下面表达是非法的。
SELECT f.topics FROM Forum f前面已经提供,一些JPA的实现允许这类的查询。为了保持移植性,你应该使用下面的语句替换(参看
Joins
一节)。
SELECT t FROM Forum f JOIN f.topics t查询的结果可以是一个抽象模型类型,一个状态字段(实体的字段或属性),一个统计函数的结果,由
NEW
操作符创建的对象,或任何它们可能的组合。如果你查询一个抽象模型类型或是构建一个新的对象,查询结果会是实体类型的对象组成的列表或一个新对象。如果你使用了统计函数,查询状态字段,或是不同的类型,返回结果是一个数组(Object[])实例的列表。数组中对象的位置与你在查询语句中指定的位置一致。例如。
SELECT t.subject, t.content FROM Topic t这个查询返回一个数组(Object[])实例组成的列表。这个列表中每个项目包含两个
String
对象,第一项目(index 0)是标题,第二个(index 1)是主题的内容。你可以创建一个新的对象作为查询结果。这个对象不要求是实体,但要有一个构造器,它的顺序与类型与
SELECT
语句一致。下面是一个封装用户统计数据的临时对象。public class UserStatistics { private String username; private Integer userId; private long postCount; public UserStatistics(String username, Integer userId, long postCount) { super(); this.username = username; this.userId = userId; this.postCount = postCount; } // getter methods removed for readability }下面的查询语句会计算系统中每个用户的发帖数量,并将结果,用户名,用户ID保存到一个临时对象
UserStatistics
中。Query q = em.createQuery("SELECT NEW com.sourcebeat.jpa.model.UserStatistics(" +"u.username, u.id, COUNT(p)) “ + “FROM Post p JOIN p.createdByUser u " + "WHERE p.parent IS NOT NULL GROUP BY u"); List results = q.getResultList();
SELECT
语句中可以使用以下统计函数(在一个路径表达式上应用)。计算查询结果作为参数的最大值,返回类型与参数类型一致。
MAX
函数可以应用到任何可排序的状态字段上,包括数字类型,字符串,字符类型,或日期。SELECT MAX(f.forumPostCount) FROM Forum f计算查询结果作为参数的最小值,返回类型与参数类型一致。
MIN
函数可以应用到任何可排序的状态字段上,包括数字类型,字符串,字符类型,或日期。SELECT MIN(f.dateCreated) FROM Forum f除
COUNT
外,这些函数必须用在以状态字段结尾的路径表达式上。你可以用一个状态字段,关联字段,或是一个标志符号变量作为参数用在COUNT
函数。如果SUM, AVG, MIN,和MAX计算的值不存在时,返回一个
NULL
。为了避免使用统计函数查询结果的重复,请使用
DISTINCT
操作符。但是在MAX 和MIN使用DISTINCT
是非法的。另外,在函数计算结果之前已经剔除了NULL
值,不管你是否使用了DISTINCT
。当使用一个构造器表达式组成的
SELECT
语句,函数的返回类型应该注意。UserAverages
对象的postCount
属性是一个Long
对象。最初,使用一个int
,查询语句运行时,Hibernate会一个IllegalArgumentException
异常,表明UserStatistics
与构造器不相符。我意识到COUNT
返回的是一个Long
类型对象,更新一下UserStatistics
,查询语句就可以正确的运行。
排序(ORDER BY)
ORDER BY
允许你对查询结果进行排序。数据库排序比应用程序更有效。要依据集合的顺序,可以使用ORDER BY
操作符。(更多信息参见第三章Entities Part II中@OrderBy
注释)这是一个JPQL查询语句中
Order By
操作符的所在位置。select from [where] [group by] [having] [order by]
Order By
的语法为:ORDER BY expression [ASC | DESC] {, expression [ASC | DESC]}*下面是一些合法的
Order By
实例。
SELECT u FROM User u ORDER BY u
SELECT u FROM User u ORDER BY u.address
SELECT u.username, u.id FROM User u ORDER BY u.username DESC
expression
可以是一个标识符号变量,一个单值关联路径,一个状态字段路径表达式。这些查询语句一一演示了Order By
的这些类型。你必须注意使用
Order By
操作符的一些限制。
当使用一个标识符号变量,一个单值关联字段时,你用来对查询进行排序的项目必须是一个可排序的类型,例如数字类型,字符串,字符类型,或是时间日期。
如果你使用一个状态字段路径表达式,同时它也必须出现在
SELECT
语句中。
ASC
表示升序(由小到大),是默认的排序方式。DESC
表示降序排列(由大到小),使用时显式在查询的Order By
语句上添加它。排序的优先级是按罗列的字段从左到右的顺序进行。不难理解,下面的查询是不合法的,因为它对一个字段进行排序但它却不在
SELECT
语句中。SELECT u.username, u.id FROM User u ORDER BY u.passwordJPQL提供了一种替代方案,用一条语句更新或删除一个或多个实体。JPQL中批处理支持一次可以处理一个实体类型(和它的子类)。也就是说,你只能在
FROM
或UPDATE
指定唯一实体。这里是一个UPDATE
查询的语法。UPDATE abstract_persistence_scheama_name [AS] identification_variable SET state_field | single_value_association_field = value {,state_field | single_value_association_field = value }*
value
引用必须与你要更新的状态字段或是单值关联字段的类型相同。你可以将下列任意值用于value
。
一个算术表达式
字符串
时间日期(datetime)
布尔值(boolean)
枚举类型(enum)
简单实体表达式
NULL
DELETE
语句看起来如下。DELETE FROM abstract_persistence_scheama_name [[AS] identification_variable] [WHERE clause]
WHERE
语句的语法与SELECT
相同(更多信息参见SELECT
语句一节)。DELETE
操作符只影响FROM
语句指定的实体及其子类。此操作不会级联到任何相关的实体。此外,UPDATE
操作符不更新实体的version一列。批处理操作最终转换成SQL并在数据库中执行,绕过了持久化环境(persistence context)。当使用一个事务作用域(transaction-scoped)的持久化环境,会在它们所在事务内执行或在一个事务开始时执行。
批量操作和扩展的持久化环境(extended persistence context)的组合在管理上有所不同。因为一个扩展持久化环境不会与数据库同步,直到参与到一个事务中去,你可以有一些实体已经删除了,但仍可能存在于持久化环境中。
持久化实现通常会在执行批处理操作前禁用一些缓存功能。基于不同的实现,部分或全部缓存功能会被禁用。频繁的使用批处理操作会影响应用程序的性能。基于这些原因,你应该在它们自己的事务内或是一个事务开始时执行批处理操作。
当你使用一个
UPDATE
或是DELETE
查询时,你一定会用到Query API提供的方法--executeUpdate()
,来执行更新或是删除操作。如果你使用getResultList()
或getSingleResult()
,持久化实现会抛出一个IllegalStateException
异常。同样,你用executeUpdate()
来执行一个SELECT
查询,持久化实现会抛出一个IllegalStateException
异常。下面是一些批量更新的例子。
一个论坛帖子数量的两倍。
Query q2 = em.createQuery("UPDATE Forum AS f " +"SET f.forumPostCount = f.forumPostCount * 2"); q2.executeUpdate();将所有的
Role
实体的dateUpdated
字段设置为当前日期和时间。Query q = em.createQuery("UPDATE Role AS r " + "SET r.dateUpdated = CURRENT_TIMESTAMP"); q.executeUpdate();将布尔字(
pruningEnabled)
)段值设置为true
。在agoraBB
应用程序中,用EntityListener
类来设置dateUpdated
和updatedByUser
字段。version字段是由持久化实现进行管理。避开这些弯弯角角,当你执行批量操作时,你的查询写成更新字段。还有,要注意的是,executeUpdate
返回实体更新的数目(删除时返回删除的数量)。// Assume we already fetched the correct User identified by // the variable adminUser Query forumUpdate = em.createQuery("UPDATE Forum AS f " + "SET f.pruningEnabled = TRUE, f.dateUpdated = CURRENT_TIMESTAMP, " + "f.version = f.version + 1, f.updatedByUser = :user"); forumUpdate.setParameter("user", adminUser); int entitiesUpdated = forumUpdate.executeUpdate();你可以将
pruningEnabled
的值设置为null
,将其进行重位。如下所示。Query pruningReset = em.createQuery("UPDATE Forum AS f " + "SET f.pruningEnabled = NULL"); pruningReset.executeUpdate();这条查询设置了
enum
类型的字段值。你必须使用enum
类的长限定词,因为enum
不是实体,持久化实现不法获得Status的信息,但它可以识别com.sourcebeat.jpa.model.Status
。Query enumUpdate = em.createQuery("UPDATE Forum AS f " + "SET f.status = com.sourcebeat.jpa.model.Status.LOCKED " + "WHERE f.type = com.sourcebeat.jpa.model.FTPType.ANNONUCEMENT"); int enumUpdateCount = enumUpdate.executeUpdate();这里有几个
DELETE
操作的例子。
删除系统中所有无密码的用户(
Users
)。Query removeRoles = em.createQuery("DELETE FROM User u " + "WHERE u.password = NULL"); int rolesRemoved = removeRoles.executeUpdate();删除没有主题(
Topics
)的论坛(Forum
)。Query removeForums = em.createQuery("DELETE FROM Forum f " + "WHERE f.topics IS EMPTY"); int forumsRemoved = removeForums.executeUpdate();JPQL提供了大量实体查询,分组,排序和总结的功能。包括丰富的关联(join)操作支持和即时读取延迟关联关系(eagerly fetch lazy relationship)的能力。你已经学习所支持的各种函数和表达式,以及
SELECT
语句所能提供的各种选项。本章最后探讨了批量处理操作,它具备使用单条查询语句(实际是指UPDATE
或DELETE
语句)影响一个或多个实体的能力。现在你应该很熟悉了JPQL所能提供的丰富的功能,利用它,从简单到复杂一步步地创建自己的查询。
欢迎访问我的网站交流技术 www.ithuzhu.com