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萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章