play框架使用起来(13)

高级指南


1、文件上传

1.1 架构考虑#

      应用中通常有两种方式来保存二进制数据:将数据保存到服务器的文件系统中,或者直接保存到数据库中。当然这两种实现各有利弊,使用文件系统非常容易,而使用数据库则具有事务处理支持,但两者都有通病,那就是很难扩展。

      这一节需要向读者着重强调的是,Play中提供的play.db.jpa.Blob类型,与java.sql.Blob类型有很大的区别。在Play中声明为play.db.jpa.Blob类型的属性默认将数据存储在数据库之外的文件当中,并没有在数据库中使用带有BLOB的java.sql.Blob类型。在服务器端,Play默认将上传的文件存储到应用的data/attachments/目录下。文件名称(UUID)和MIME类型拼接的字符串被作为SQL中VARCHAR类型存储在数据库属性中。

      如果需要将数据存储到数据库表中,可以将@javax.persistence.Lob注解添加到模型属性前。带有@Lob注解的数据便会作为BLOB或者CLOB类型存储到数据表中。


1.2 上传文件并存储到服务器中#

      在Play中实现文件的上传,存储和共享都非常简单,因为框架自动将HTML表单中提交的文件绑定到JPA模型中,而且Play提供的便捷的方法使得共享文件如同显示文本一样简单。

      首先,在应用中定义User模型,该模型用于存储上传文件。将photo属性定义为play.db.jpa.Blob类型:

package models;
 
import play.db.jpa.Blob;
import play.db.jpa.Model;
 
import javax.persistence.Entity;
 
@Entity
public class User extends Model {
 
   
public Blob photo;
}

      模型定义完成后,在视图文件中添加上传文件的表单。为了绑定JPA模型,上传文件的name属性设置为user.photo:

#{form @addUser(), enctype:'multipart/form-data'}
   
<input type="file" name="user.photo">
   
<input type="submit" name="submit" value="Upload">
#{/form}

      在控制器中添加相应的Action方法,使用新建的模型对象来保存上传的文件:

public static void addUser(User user) {
   user
.save();
   index
();
}

      因为文件上传是由框架自动处理的,所以代码看起来除了保存JPA实体以外,并没有做其他事情。实际上,框架内部帮我们完成了如下一系列的操作:首先,在Action方法执行之前,上传的文件会被保存到应用的tmp/uploads子目录。当实体保存之后,上传的文件使用UUID命名并被复制到data/attachments目录下。最后当Action方法执行完毕时,删除临时文件。


 如果需要将文件保存到其他目录,可以在conf/application.conf文件中指定路径,可以是绝对路径,也可以是Play应用目录的相对路径:

attachments.path=photos

      为了使上传的图片更加直观,我们为模板文件中增加<img>标签,用于显示上传的图片:

#{list items:models.User.findAll(), as:'user'}
   
<img src="@{userPhoto(user.id)}">
#{/list}

      最后,在控制器中增加Action方法,用于加载模型并返回上传的图片:

public static void userPhoto(long id) {
   
final User user = User.findById(id);
   notFoundIfNull
(user);
   response
.setContentTypeIfNotSet(user.photo.type());
   renderBinary
(user.photo.get());
}
      以上就是Play中实现的简单的文件上传功能。读者会发现,我们并没有写很多的业务代码,也没有做过多的配置处理,只不过几句方法调用就完成了文件上传的功能。这就是Play提倡的设计哲学:简单并且高效。
文件上传


1.3 更新上传#

      如果读者希望更新上传的文件,也很容易,只需在请求参数中提供user.id,就可以使用save()方法来更新实体:

public static void updateUser(User user) {
   user
.save();
   index
();
}

      上传的更新文件会以新的UUID命名并保存,这意味着原始的文件会失去关联。如果服务器没有足够的空间,那么就必须实现清理策略。本节介绍以下几种可行的方案:

  • 比较蛮力的方式是使用异步Job定期查询所有当前使用文件类型的JPA模型,扫描attachments文件夹,然后删除没有关联的文件。这种方法很难在大规模的应用中使用。
  • 针对某些应用,在模型中维护所有文件之前版本的引用是非常有意义的。这样我们可以在用户界面显示这些旧版本的文件信息,就像wiki通常会保存每个页面之前的版本那样。清理工作可以是手动,也可以是新文件上传或者异步Job进行触发。清理的策略可以是保存一定数量的版本,或者删除指定日期之前的版本。使用@PreUpdate和@PreRemove JPA拦截器能够很好地处理以上问题。
  • 比较极端的做法是直接使用BinaryField字段来实现具有事务的上传更新。

1.4 文件删除#

      如果我们删除带有play.db.jpa.Blob属性的对象,attachments文件夹中的文件不会自动删除,我们需要通过引用java.io.File属性来手动进行删除,具体操作如下:

public static void deleteUser(long id) {
   
final User user = User.findById(id);
   user
.photo.getFile().delete();
   user
.delete();
   index
();
}

      我们也可以将文件删除封装到模型里,只要在User.java中覆盖_delete()方法,就可以在数据库实体成功删除后自动执行文件删除操作。

@Override
public void _delete() {
   
super._delete();
   photo
.getFile().delete();
}

1.5 上传文件并保存文件名#

      如果读者需要保存原始上传文件的名称,我们可以在服务器端将文件扩展名映射为MIME类型,这样就可以以原始文件名的方式保存文件了。

      我们需要将表单控制绑定到具有java.io.File类型的Action方法参数中以便得到文件名,这意味着我们需要在控制器中创建一个新的Action方法,将单独的表单参数构建为模型对象,而不是像第一个例子那样直接绑定模型对象。

      首先,在User模型中添加photoFileName属性:

@Entity
public class User extends Model {

   
public String photoFileName;
   
public Blob photo;
}

      新建Action方法addUserWithFileName(),实例化模型对象以及初始化Blob属性:

public static void addUserWithFileName(File photo) throws FileNotFoundException {
   
final User user = new User();
   user
.photoFileName = photo.getName();
   user
.photo = new Blob();
   user
.photo.set(new FileInputStream(photo),
   
MimeTypes.getContentType(photo.getName());
   user
.save();
   index
();
}

      接着修改视图层的模版,显示上传后的图片文件名称。由于使用新的控制器方法为addUserWithFileName(),文件上传控件的名称也需要做相应的改变(将user.photo改成photo):

#{list items:models.User.findAll(), as:'user'}
   
<img title="${user.photoFileName}" src="@{userPhoto(user.id)}">
#{/list}

#{form @addUserWithFileName(), enctype:'multipart/form-data'}
   
<input type="file" name="photo">
   
<input type="submit" name="submit" value="Upload">
#{/form}

1.6下载文件#

      当我们访问二进制数据(比如图片)时,可能会以正常的方式直接在浏览器中显示。比如之前通过URL访问的图片资源就是直接在浏览器中显示的。但是,我们可以设置HTTP头来通知浏览器将文件作为附件的形式下载到用户的计算机中,而不是直接显式地在浏览器中呈现。

      首先,创建实现下载功能的Action方法。假设文件名已经被正确设置,实现下载功能的Action方法与userPhoto()方法唯一不同的地方是需要将文件名作为renderBinary()方法的参数传递,这样Play就会设置Content-Disposition响应头来提供文件名。

public static void downloadUserPhoto(long id) {
   
final User user = User.findById(id);
   notFoundIfNull
(user);
   response
.setContentTypeIfNotSet(user.photo.type());
   renderBinary
(user.photo.get(), user.photoFileName);
}

      修改模版文件,增加链接指向图片下载的URL:

#{list items:models.User.findAll(), as:'user'}
   
<a href="@{downloadUserPhoto(user.id)}">
       
<img src="@{Application.userPhoto(user.id)}">
   
</a>
#{/list}

1.7 自定义content type#

      我们知道,浏览器响应的内容是在控制器的Action方法中设置的。框架的play.libs.MimeTypes类负责查找给定文件扩展名的MIME类型,详细类型列表参见$PLAY_HOME/framework/src/play/libs/mime-types.properties文件。

      从Play的1.2版本开始,我们可以在conf/application.conf配置文件中添加自定义类型。比如,增加以xcf为扩展名的GIMP图片的MIME类型:

mimetype.xcf=application/x-gimp-image


注意:

设置content type的例子只适用于addUserWithFileName()方法,因为方法会隐式查找基于原始文件名的MIME类型。最早介绍的addUser()例子使用的MIME类型是HTTP请求文件上传时发送的。



2、JPA支持

 JPA(Java Persistence API)是Java持久化的规范,但JPA本身并不是持久化的具体实现。这就好比规定了所有的手机都必须能打电话。针对以上这个比喻,JPA充当的角色只是手机能打电话这条约束,而不是电话本身。我们能说手机提供了通话功能,但是不能说通话功能就是一个手机,而且很显然手机提供了通话功能之外的其它很多附加的能力。同样的,JPA的实现者也可以根据自身的情况,提供基本功能之外的附加功能。

      目前JPA的实现主要有:Hibernate,OpenJPA, Toplink,JDO等。Play默认使用了Hibernate的实现,JPA作为一种规范,因此也可以使用其他任意的JPA实现来替代Hibernate。

      JPA的核心概念是通过注解或XML来描述对象与表的映射关系,并将运行期的实体对象持久化到数据库中。Play在JPA的基础上提供了一套非常实用的辅助类来简化对JPA实体的管理,使开发更加便捷。


提示:

开发过程中仍然可以通过原生JPA提供的API来管理实体。



2.1 启用JPA实体管理#

      Play会自动查找标记为javax.persistence.Entity注解的类,@javax.persistence.Entity注解的作用是通知Play对当前实体类进行管理,默认情况下实体类通过Hibernate的Entity Manager进行管理。当然以上所有的前提是必须正确配置JDBC数据源,否则Play将无法进行持久化操作。


2.2 获得JPA实体管理器#

      JPA实体管理器启动后,程序代码中就可以通过JPA辅助类来取得被管理的实体。例如:

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

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不会从连接池中获取连接。


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



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

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

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

2.7 统计对象#

      Play提供的count()方法具有统计查询对象结果的功能:

long postCount = Post.count();

      也可以在count()方法中指定条件来统计目标对象数量:

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

2.7 统计对象#

      Play提供的count()方法具有统计查询对象结果的功能:

long postCount = Post.count();

      也可以在count()方法中指定条件来统计目标对象数量:

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

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);
}
      可能大部分开发者都会犯这样的错误:当不希望对象被持久化时,忘记通知实体管理器抛弃当前对象,以为只要没有显式地调用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();
        show
(id);
   
}
}

      显式地调用save()方法在对象级联的操作中显得非常繁琐,通过在注解中声明cascade=CascadeType.ALL属性可以很方便的解决这个问题。


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配置的时候,需要格外的当心。

«


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值