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

«


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章