目前JPA的实现主要有:Hibernate,OpenJPA, Toplink,JDO等。Play默认使用了Hibernate的实现,JPA作为一种规范,因此也可以使用其他任意的JPA实现来替代Hibernate。
JPA的核心概念是通过注解或XML来描述对象与表的映射关系,并将运行期的实体对象持久化到数据库中。Play在JPA的基础上提供了一套非常实用的辅助类来简化对JPA实体的管理,使开发更加便捷。
开发过程中仍然可以通过原生JPA提供的API来管理实体。
10.2.1 启用JPA实体管理#
Play会自动查找标记为javax.persistence.Entity注解的类,@javax.persistence.Entity注解的作用是通知Play对当前实体类进行管理,默认情况下实体类通过Hibernate的Entity Manager进行管理。当然以上所有的前提是必须正确配置JDBC数据源,否则Play将无法进行持久化操作。
10.2.2 获得JPA实体管理器#
JPA实体管理器启动后,程序代码中就可以通过JPA辅助类来取得被管理的实体。例如:
public static void index(){ Query query = JPA.em().createQuery("from Article"); List<Article> articles = query.getResultList(); render(articles); }
10.2.3 事务管理#
事务(Transaction)是访问并可能更新数据库中各种数据项的程序执行单元。Play会自动进行事务管理,在每次发送HTTP请求时自动开启事务,发送HTTP响应完毕后提交事务。如果程序在request/response过程中抛出了异常,事务会自动回滚。当然也可以显式调用JPA.setRollbackOnly()方法通知JPA不要提交当前事务,而在代码中对事务进行强制回滚。
Play为事务管理控制提供了注解的支持。如果需要将Action声明为只读事务,可以在Action方法上标记@play.db.jpa.Transactional(readOnly=true)注解;如果执行当前方法时无需开启事务,可以在Action方法上标记@play.db.jpa.NoTransaction;如果当前控制器中所有方法都无需开启事务,可以直接在控制器上标明@play.db.jpa.NoTransaction。这样做的好处是,大大提升了应用的性能,因为添加@play.db.jpa.NoTransaction注解后Play不会从连接池中获取连接。
10.2.4 play.db.jpa.Model辅助类#
play.db.jpa.Model是Play中主要的JPA辅助类,它提供了大量的辅助方法,简化程序对JPA的访问。需要做的就是使JPA实体类继承play.db.jpa.Model。如下是继承Model的Post实体类:
@Entity public class Post extends Model { public String title; public String content; public Date postDate; @ManyToOne public Author author; @OneToMany public List<Comment> comments; }
play.db.jpa.Model类默认包含了一个自增长的长整型id字段,并将其作为主键。这通常是比较好的方案:让自增长的长整型id作为JPA实体的主键(技术主键),并指定另一字段作为实体的功能主键。
Play将实体的成员变量的访问权限定义为public,从而无需编写大量的setter/getter方法。
10.2.5 使用GenericModel自定义ID#
Play并不强制要求实体类都继承play.db.jpa.Model,某些情况下应用不需要自增长型的id字段作为实体类的主键,可以选择将JPA实体类继承自play.db.jpa.GenericModel。
下例是User实体的映射。其中实体的id属性是UUID(通用唯一识别码 Universally Unique Identifier),name和mail属性是必需的,并且使用Play验证器来验证简单的业务规则。
@Entity public class User extends GenericModel { @Id @GeneratedValue(generator = "system-uuid") @GenericGenerator(name = "system-uuid", strategy = "uuid") public String id; @Required public String name; @Required @MaxSize(value=255, message = "email.maxsize") @play.data.validation.Email public String mail; }
10.2.6 查找对象#
play.db.jpa.Model 类提供多种方法用于数据查找。
通过ID查找
findById()是最常用的查找对象方法,可以根据实体对象的Id进行查找:
Post aPost = Post.findById(5L);
查找所有对象
findAll()用来检索所有对象:
List<Post> posts = Post.findAll();
以下方法实现的效果与findAll()等价:
List<Post> posts = Post.all().fetch();
采用以下写法可以对查询结果进行分页:
// 最多匹配100篇 post List<Post> posts = Post.all().fetch(100); //从第50篇post开始查找,并且最多匹配到第100篇post List<Post> posts = Post.all().from(50).fetch(100);
使用精简语句查询
Play提供了非常语义化的查询表达式来创建查询语句,但是只支持一些简单的条件查询。
Post.find("byTitle", "My first post").fetch(); Post.find("byTitleLike", "%hello%").fetch(); Post.find("byAuthorIsNull").fetch(); Post.find("byTitleLikeAndAuthor", "%hello%", connectedUser).fetch();
简单的查询语句必须遵循固定语法规则:“[Property][Comparator]And?”。其中Comparator有以下几种形式:
- LessThan – 小于给定的值
- LessThanEquals – 小于等于给定的值
- GreaterThan – 大于给定的值
- GreaterThanEquals – 大于等于给定的值
- Like – 与SQL语法中like相似,但属性总是会被转换成小写
- Ilike – 与Like相似,但是大小写敏感,会将参数全部转换成小写
- Elike – 与SQL语法中like等价,不会进行大小写转换
- NotEqual – 不等于
- Between – 在两个数值之间(需要两个参数)
- IsNotNull – 不为空(不需要参数)
- IsNull – 为空(不需要参数)
使用JPQL查询
在Play中也可以书写完整的JPQL语句进行查询:
Post.find( "select p from Post p, Comment c " + "where c.post = p and c.subject like ?", "%hop%" );
也可以在JPQL语句中只写条件部分的查询:
Post.find("title", "My first post").fetch(); Post.find("title like ?", "%hello%").fetch(); Post.find("author is null").fetch(); Post.find("title like % and author is null", "%hello%").fetch(); Post.find("title like % and author is null order by postDate", "%hello%").fetch();
甚至可以只写order by的部分,对查询结果进行升降排序:
Post.find("order by postDate desc").fetch();
10.2.7 统计对象#
Play提供的count()方法具有统计查询对象结果的功能:long postCount = Post.count();
也可以在count()方法中指定条件来统计目标对象数量:
long userPostCount = Post.count("author = ?", connectedUser);
10.2.8 显式地持久化对象#
Hibernate会将数据库中查询到的结果以对象缓存的形式维护起来。当实体管理器在进行查询匹配等操作的过程中,数据一直作为持久对象存在。这意味着如果事务没有提交,对象的任何改变都将自动持久化到数据库中。JPA的规范是这样定义的:对象的修改默认与事务过程一致,所以无需显式地调用任何方法就可以持久化修改后的数据。
这种全自动的持久化管理,也有不足的地方,因为我们并不总是希望对象一旦修改就被持久化。所以与其通知实体管理器更新一个对象,还不如告诉它哪些对象不需要更新。refresh()方法可以回滚单个实体,在事务提交之前调用,对象将不被持久化。
下例是提交表单后,处理持久化对象的常规方式:
public static void save(Long id) { User user = User.findById(id); user.edit("user", params.all()); //修改持久化的对象 validation.valid(user); if(validation.hasErrors()) { // 需要显式抛弃user对象 user.refresh(); edit(id); } show(id); }
Play总结了这个问题,并做了处理过程的改进。所有继承JPASupport/JPAModel的对象需要显式地调用save()方法才能实现实体的持久化操作。重写上例中的代码:
public static void save(Long id) { User user = User.findById(id); user.edit("user", params.all()); validation.valid(user); if(validation.hasErrors()) { edit(id); }else{ user.save(); show(id); } }
显式地调用save()方法在对象级联的操作中显得非常繁琐,通过在注解中声明cascade=CascadeType.ALL属性可以很方便的解决这个问题。
注解中声明cascade=CascadeType.ALL后,save()方法会自动包含级联操作。
10.2.9 泛型问题#
play.db.jpa.Model中定义了大量的泛型方法,这些方法通过泛型参数来指定方法的返回类型。调用这些方法时,会根据执行上下文返回具体的类型。例如,findAll()方法的定义如下:
<T> List<T> findAll();
findAll()的使用方法如下:
List<Post> posts = Post.findAll();
方法返回结果中指定了List<Post>,通知Java编译器确切的类型,泛型T才被解析为Post类型。但是我们无法直接在循环参数中指定泛型方法返回值类型。所以下面的代码无法通过编译,会提示“Type mismatch: cannot convert from element type Object to Post”类型转换错误:
for(Post p : Post.findAll()) { p.delete(); }
针对这个问题,通常的解决方案是为其指定中间变量:
List<Post> posts = Post.findAll(); for(Post p : posts) { p.delete(); }
下面介绍一种实用却较少用到的Java语言特性,代码简洁并更具可读性:
for(Post p : Post.<Post>findAll()) { p.delete(); }
开发者在使用JPA的时候需要注意,Play并不支持XA(采用两阶段提交方式来管理分布式事务)。如果在同一个请求中配置了多个不同的JPA,Play的做法是尽最大可能commit事务。如第一个数据库的commit成功了,但是第二个数据库的commit失败了,第一个commit并不会回滚。所以,开发者在同个请求中使用多个JPA配置的时候,需要格外的当心。