07.JPA持久化

play提供了一些非常有用的帮助类来简单管理jpa实体。

注意:如果需要,你仍旧可以继续使用原始的JPA API。

启动JPA实体管理器

当play找到至少一个注释了@javax.persistence.Entity标识的类时,play将自动启动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请求到达,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不会从连接池中获取连接,以提高运行速度。

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类自动提供了一个自增长的Longid域。采用自增长的Long id主键对jpa模型来说是个好主意。

注意,我们事实上已经使用了play中的一特性,也就是说play会自动把Post类中的public成员认作属性。因此,我们不需要为这些成员书写setter/getter。

为GenreicModel定制id映射

play并不强制使用play.db.jpa.Model。你的JPA实体也可以继承play.db.jpa.GenericModel,如果不打算使用Long类型的id作为主键,就必须这样做。

比如,下面是一个非常简单的User实体类。它的id是UUID, namemail属性都是非空值,我们使用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; 
} 

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();

Counting统计对象

统计对象非常容易:

long postCount = Post.count();

或使用查询进行统计:

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

用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()参数。

强制保存

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()方法。

更多公共类型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配置时一定要牢记这一点。

发布了8 篇原创文章 · 获赞 3 · 访问量 1万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章