play 提供了一些非常有用的帮助类来简单管理 jpa 实体。
注意:如果需要,你仍旧可以继续使用原始的 JPA API。
7.1. 启动 JPA 实体管理器
当 play 找到至少一个注释了@javax.persistence.Entity 标识的类时,play 将 自动启动 hibernate 实体管理器。前提是已经有一个正确的 JDBC 数据源配置, 否则会导致失败。
7.2. 获取 JPA 实体管理器
当 JPA 实体管理器启动后,就可以在应用程序代码中得到管理器,并使用 JPA 帮助类了,比如:
public static index() {
Query query = JPA.em().createQuery("select * from Article");
List<Article> articles = query.getResultList();
render(articles);
}
7.3. 事务管理
play 会自动管理事务。当 http 请求到达,play 就会为每个 http 请求启动一个 事务。当 http response 发送的时候,就会把事务提交。如果代码抛出异常,事 务将会自动回滚。
如果需要在代码中强制回滚事务,可以使用 JPA.setRollbackOnly()方法,以告 诉 JPA 不要提交当前事务。
也可使用注释明确哪些事务要进行处理。
如果在控制器里用@play.db.jpa.Transactional(readOnly=true)注释了控制器 的某个方法,那么这个事务是只读的。
如果不想让 play 启动事务,可以使用以下注释@play.db.jpa.NoTransaction。
如果不想让类的所有方法执行事务,可以对控制器类进行注释:
@play.db.jpa.NoTransaction.
当使用@play.db.jpa.NoTransaction 注释时,Play 不会从连接池中获取连接, 以提高运行速度。
7.4. play.db.jpa.Model 支持类
在 play 中, 这是最主要的帮助类, 如果你的 jpa 实体继承了 play.db.jpa.Model 类,那么这个实体类将得到许多非常有用的方法来管理 jpa 访问。
比如下面的 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 类自动提供了一个自增长的 Long id 域。采用自增长的 Long id 主键对 jpa 模型来说是个好主意。
注意,我们事实上已经使用了 play 中的一特性,也就是说 play 会自动把 Post 类中的public 成员认作属性。 因此, 我们不需要为这些成员书写 setter/getter。
7.5. 为 GenreicModel 定制 id 映射
play 并不强制使用 play.db.jpa.Model。你的 JPA 实体也可以继承 play.db.jpa.GenericModel,如果不打算使用 Long 类型的 id 作为主键,就必须 这样做。
比如,下面是一个非常简单的 User 实体类。它的 id 是 UUID, 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;
}
7.6. Finding 对象
play.db.jpa.Model 提供了几种方式来查找数据,比如:
Find by ID
这是查找对象最简单的方式:
Post aPost = Post.findById(5L);
Find all
List<Post> posts = Post.findAll();
这是获取所有 posts 对象最简单的方式,类似的应用还有:
List<Post> posts = Post.all().fetch();
下面对结果进行分页:
// 最多 100 条
List<Post> posts = Post.all().fetch(100);
或
// 50 至 100 条
List<Post> posts = Post.all().from(50).fetch(100);
使用简单查询进行查找
以下方式允许你创建一些非常有用的查询,但仅限于简单查询:
Post.find("byTitle", "My first post").fetch();
Post.find("byTitleLike", "%hello%").fetch();
Post.find("byAuthorIsNull").fetch();
Post.find("byTitleLikeAndAuthor", "%hello%", connectedUser).fetch();
简单查询遵循以下语法[属性][比较]And?,比较可取以下值:
- LessThan –小于给定值
- LessThanEquals – 小于等于给定值
- GreaterThan – 大于给定值
- GreaterThanEquals – 大于等于给定值
- Like –等价于 SQL 的 like 表达式,但属性要为小写。
- Ilike – 和 Like 相似,大写不敏感,也就是说参数要转换成小写。
- Elike -等价于 SQL 的 like 表达式,不进行转换。
- NotEqual – 不等于
- Between – 两个值之间(必须带 2 个参数)
- IsNotNull – 非空值(不需要任何参数)
- IsNull – 空值(不需要任何参数)
使用 JPQL 查询进行查找
如:
Post.find(
"select p from Post p, Comment c " +
"where c.post = p and c.subject like ?", "%hop%"
);
或仅查询某部分:
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();
7.7. Counting 统计对象
统计对象非常容易:
long postCount = Post.count();
或使用查询进行统计:
long userPostCount = Post.count("author = ?", connectedUser);
7.8. 用 play.db.jpa.Blob 存储上传文件
使用 play.db.jpa.Blob 类型可以存储上传的文件到文件系统里(不是数据库)。
在服务器端, Play 以文件方式存储上传的图片到应用程序目录下的 attachments 文件夹下。文件名(一个 UUID 是指在一台机器上生成的数字,通用唯一识别码, 用来唯一标识不同的文件)和 MIME 类型将存储到数据库的属性中(SQL 数据类型 为 VARCHAR)。
在 play 里上传、存储、下载文件非常容易。这是因为框架自动对 html 窗体到 jpa 模型进行了文件上传绑定,而且 play 还提供了便利的方法来操作二进制数 据,就像操作普通文本一样简单。为了在模型里存储上传文件,需要增加一个 play.db.jpa.Blob 类型的属性:
import play.db.jpa.Blob;
@Entity
public class User extends Model {
public String name;
public Blob photo;
}
为了上传文件,需要在视图模板里添加一个窗体,在窗体里使用文件上传组件, 组件名称应为模型的 Blob 属性,如 user.photo:
#{form @addUser(), enctype:'multipart/form-data'}
<input type="file" name="user.photo">
<input type="submit" name="submit" value="Upload">
#{/form}
之后,在控制器里增加一个方法用于存储上传的文件:
public static void addUser(User user) {
user.save();
index();
}
这些代码除了 jpa 实体的存储操作外,好像什么都没做,这是因为 play 自动对 上传文件进行了处理。首先,在启动 action 方法之前,上传的文件已经存储到 应用程序的 tmp/uploads/文件夹下,接着,当实体存储完成后,上传的文件会 被复制到应用程序的 attachments/目录,文件的名称为 UUID。最后,当 action 完成后,临时文件将被删除。
如果同一用户上传另外一个文件,服务器将把上传的文件当作新文件进行存储, 并为新文件生成一个新的 UUID 文件名,也就是说之前上传的文件无效。要实现 多文件上传,就必须自行去实现,比如采用异步 job 方式。
如果 http 请求没有指定文件的 MIME 类型,你可以使用文件名称扩展。
要想把文件存储到不同的目录,需要配置 attachments.path。
要想下载存储的文件,需要给控制器的 renderBinary()方法传递 Blob.get() 参 数。
7.9. 强制保存
Hibernate 负责维护从数据库查询出来的对象缓存,这些对象将被当作持久化对 象进行对待,其时限和 EntityManager 生命周期一样长。也就是说所有绑定了事 务的对象的任何改变都会在事务提交时自动进行持久化。在标准的 JPA 里,更新 操作属于事务范围,也就不需要强制调用任何方法来持久化值。
负面影响就是你必须手工管理所有的对象, 而不是告诉 EntityManager 去更新对 象(哪种更直观)。我们必须告诉 EntityManager 哪个对象不需要更新,这个操 作是通过调用 refresh()来实现的,本质上是回滚一个单实体。我们在提交事务 之前调用 refresh()方法的目的就是为了让某些对象不被更新。
下面是一个通用情况,在窗体已经提交后,对一个持久化对象进行编辑:
public static void save(Long id) {
User user = User.findById(id);
user.edit("user", params.all());
validation.valid(user);
if(validation.hasErrors()) {
//这里我们必须丢弃用户的编辑
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 属性来自动 调用 save()方法。
7.10. 更多公共类型 generic typing 问题
play.db.jpa.Model 定义了许多公共方法。这些方法使用一种类型参数来指定方 法的返回值类型。在使用这些方法的时候,返回值的具体类型由调用的上下文类 型接口确定。
比如,findAll 定义如下:
<T> List<T> findAll();
使用情况为:
List<Post> posts = Post.findAll();
在这里,java 编译器使用你分配给结果方法 List<Post>的类型作为 T 的实际类 型。因此,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();
}
很重要的一点就是 play 不支持 XA(两阶段提交)。如果你在同一请求里使用多个 不同的 jpa 配置,play 将试着提交更多的事务。如果在第一个数据库成功提交, 而在第二个数据库提交失败,那么第一个数据库提交的数据将不会回滚。当在同 一个请求里使用多个 jpa 配置时一定要牢记这一点。