Play 框架手册(7) – JPA 持久化


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 配置时一定要牢记这一点。


前一篇:
后一篇:

发表评论