jpa - niubods/playframework-notes GitHub Wiki

JPA 持久化

Play提供了一系列非常有用的辅助类来简化对你JPA实体的管理。

注意 任何时候你都可以再回到纯 JPA API,只要你愿意。

启动JPA实体管理

当play找到一个或多个以@javax.persistence.Entity注解的类时,它会自动启动Hibernate实体管理器。不过,你得保证你已经正确配置了正确的JDBC数据源,否则它会失败。

获取JPA实体管理器

当JPA实体管理器启动后,你可以从应用代码中取得它,使用JPA辅助类。例如:

public static index() {
    Query query = JPA.em().createQuery("select * from Article");
    List<Article> articles = query.getResultList();
    render(articles);
}

事务控制

Play会自动为你管理事务。它将在每次HTTP请求时开启一个事务然,然后当HTTP响应发出后提交它。如果你的代码抛出了异常,事务会自动回滚。

如果你需要在应用代码中强制事务回滚,你可以使用 JPA.setRollbackOnly() 方法,它会告诉JPA不要提交当前事务。

你也可以使用注解来指定事务应该怎样被处理。

如果你用@play.db.jpa.Transactional(readOnly=true)给控制器方法加注解,事务将会是只读的。

如果你想阻止Play开启任何事务,你可以使用@play.db.jpa.NoTransaction

要阻止所有方法开启事务,你可以使用@play.db.jpa.NoTransaction给控制器类加注解。

当使用@play.db.jpa.NoTransaction时,Play不会从连接池获取任何连接,而从连接池获取连接是可以改善速度的。

play.db.jpa.Model 支持类

这是JPA的主要辅助类。如果你让你的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会自动识别Post类的 公有 成员为 属性 。所以我们不需要为这个对象写所有的gettter/setter方法。

用GenericModel自定义id映射

没什么强制你让你的实体基于 play.db.jpa.Model 。你的JPA实体也可以继承 play.db.jpa.GenericModel 类。如果你不想使用一个 Long id 做你实体的主键你就得这么做。

例如,这里有一个简单的 User 实体的映射。 id 是一个UUID, nameemail 属性是必须的,那么我们使用Play Validation来实施简单的业务规则。

@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;
}

查找对象

play.db.jpa.Model 提供给你几种查找数据的方式。例如:

通过ID查找

这是找到一个对象最简单的方式。

Post aPost = Post.findById(5L);

查找所有

List<Post> posts = Post.findAll();

这是取回 所有 post最简单的方式,不过你同样也可以这样做:

List<Post> posts = Post.all().fetch();

这个可以让你给查找结果分页:

// 100 max posts
List<Post> posts = Post.all().fetch(100);

或者甚至,

// 100 max posts start at 50
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();

简单查询语句遵循下列语法[Property][Comparator]And?,可选的比较符号如下:

  • LessThan – 小于给定的值
  • LessThanEquals – 小于或等于给定的值
  • GreaterThan – 大于给定的值
  • GreaterThanEquals – 大于或等于给定值
  • Like – 等价于SQL的like表达式,除了这里的属性总是会转换成小写形式。
  • Ilike – 等价于Like,除了大小写不敏感,也就是说你的参数也会被转成小写形式。
  • Elike – 等价于SQL的like表达式,不会转换
  • NotEqual – 不等于
  • Between – 在两个值之间(需要两个参数)
  • IsNotNull – 不为空(不需要参数)
  • IsNull – 为空值(不需要参数)

使用JPQL查询语句查找

你可以使用一个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();

计算对象数量

你可以很容易地计算对象数量。

long postCount = Post.count();

或者甚至通过一个查询语句计算:

long userPostCount = Post.count("author = ?", connectedUser);

使用play.db.jpa.Blob保存上传文件

你可以使用 play.db.jpa.Blob 类型把上传文件保存在文件系统中(不是到数据库中)。在服务器端,Play把上传的文件保存在应用目录的 attachments 目录下。文件名(一个 UUID )和MINE类型保存在数据库中,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 属性:

#{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自动处理的。首先,在动作方法执行之前,上传文件保存到 tmp/uploads 子目录下。然后,当实体保存之后,文件被复制到 attachments/ 目录下,使用一个UUID作为文件名。最后,当动作方法执行完毕后,临时文件会被删除。

如果你为相同的用户上传了另一个文件,这会在服务器上保存一个新文件,名字是一个新UUID,这意味着原始的文件现在变成了孤儿。如果你不是有一块无限容量的硬盘,你需要实现你自己的清理方案,也许使用一个 异步作业.

如果HTTP请求没有指定正确的文件MIME类型,你可以使用文件后缀名映射,参见 控制器 文件部分的描述。

要把附件保存到另外的目录中,可以配置 “attachments.path”。

要对保存的文件提供服务,需要传递 Blob.get() 给控制器的 renderBinary 方法,参见 控制器 二进制部分的描述。

显式保存

Hibernate维护着一个从数据库查询出对象的缓存。只要用来获取它们的实体管理器仍是活动的,这些对象就一直被作为持久化对象引用。这意味着在一个事务的边界内,对这些对象的任何修改在事务提交的时候都会被自动持久化。在标准JPA中,这些更新是在事务边界内实现的,你不需要显示地调用任何方法来持久化这些值。

主要的缺点是我们必须手动管理我们的对象。我们必须告诉实体管理器哪些对象是我们不需要更新的,而不是告诉实体管理器去更新一个对象(这样会直观得多)。我们通过调用 refresh() 方法来做到这一点,这本质上是回滚了一个实体。我们在事务上调用commit前做这些,或者当我们意识到这个对象不应该更新。

这里是一个常见的用例,当表单提交后编辑一个持久化对象:

public static void save(Long id) {
    User user = User.findById(id);
    user.edit("user", params.all());
    validation.valid(user);
    if(validation.hasErrors()) {
        // Here we have to explicitly discard the user modifications...
        user.refresh();
        edit(id);
    }
    show(id);
}

从中我们可以看到,多数开发者没有意识到这一点,因而在发生错误时忘记抛弃对象的状态,以为对象不会在显示调用 save() 的时候保存。

所以这正是我们在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(); // explicit save here
       show(id);
    }
}

这样会直观得多。另外,因为在一个大型的对象图中显式调用 save() 很麻烦,所以当使用 cascade=CascadeType.ALL 属性对关联对象注解时 save() 会自动级联调用。

更多关于泛型类型的问题

play.db.jpa.Model 定义了一组泛型方法。这些泛型方法使用一个类型参数来指定方法的返回类型。当使用这些方法时,具体的返回类型会从调用上下文通过类型推断得到。

例如, findAll 方法定义如下:

<T> List<T> findAll();

而你像这样使用:

List<Post> posts = Post.findAll();

这里Java编译器会解决 T 的实际类型,通过你实际指定方法的结果为一个 List<Post>。所以 T 会解析成一个Post类型。

不幸的是,如果泛型方法的结果值直接作为另一个方法的参数来调用或者用在一个循环中就行不通了。所以下列代码会在编译的时候失败,编译错误为:"Type mismatch: cannot convert from element type Object to Post":

for(Post p : Post.findAll()) {
    p.delete();
}

当然,你可以使用一个临时的局部变量来解决这个问题,如:

List<Post> posts = Post.findAll(); // type inference works here!
for(Post p : posts) {
    p.delete();
}

但是等等,还有一个更好的方法。你可以使用一个很实用但是有点不为人所知的Java语言特性,这能使代码更短,同时可读性更强:

for(Post p : Post.<Post>findAll()) {
    p.delete();
}

有件很重要的事需要注意,Play不支持XA(两段提交)。如果你在一个请求中使用了多个不同的JPA配置, Play会尝试尽可能多的提交事务 。如果提交到第一个数据库成功了,而第二个提交到另一个数据库的事务失败了,第一个已经提交的事务不会回滚。当在同一个请求中使用多个JPA配置时要记住这一点。

⚠️ **GitHub.com Fallback** ⚠️