開始使用 Spring Data JPA

英文原文:Getting started with Spring Data JPA

在我們剛剛發佈項目Spring Data JPA的第一個里程碑時,我想給你一個關於它的簡要介紹.正如你所知道的,Spring framework 對於基於JPA的數據存取層提供了支持.那麼 Spring Data JPA 是如何添加到Spring中的呢?回答這個問題,我想從一個數據存取組件開始.這個組件提供了一個簡單的域(domain),它是用純JPA和Spring實現的,而且可以擴展和改進.在我們實現之後,我將用Spring Data JPA 來重構它. 你在以在 GitHub上找到這個小項目的每一次重構的詳細指導.

域(The domain)

為了保持簡單,我從最簡單常用的域開始:客戶(Customer)和帳號(Account)

@Entity
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String firstname;
private String lastname;
// … methods omitted
}
@Entity
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@ManyToOne
private Customer customer;
@Temporal(TemporalType.DATE)
private Date expiryDate;
// … methods omitted
}

帳戶只有一個到期日(expriyDate).再無其他. - 它使用JPA註解.現在我們來看看管理帳號的組件.

@Repository
@Transactional(readOnly = true)
class AccountServiceImpl implements AccountService {
@PersistenceContext
private EntityManager em;
@Override
@Transactional
public Account save(Account account) {
if (account.getId() == null) {
em.persist(account);
return account;
} else {
return em.merge(account);
}
}
@Override

public List<account> findByCustomer(Customer customer) {
TypedQuery query = em.createQuery("select a from Account a where a.customer = ?1", Account.class);
query.setParameter(1, customer);
return query.getResultList();
}
}

為了在後面重構引入存儲層(repository layer)時不引起名稱衝突,我特意命名為 class*Service.但是在概念上,這個類是一個存儲對象,而不是服務.事實上我們有嗎?

這個被@Repository 註釋的類拋出的異常可以被Spring的DataAccessException捕獲。另外我們還用到了@Transactional 來保證 save(...) 操作的事務性和該類其他方法(這裡是findByCustomer(...))的事務只讀標識。這樣會對數據庫性能和我們持久化提供器的性能有一定的優化。

由於我們不想讓程序員去決定什麼時候調用EntityManager的merge(...)或者persist(...)方法。我們用了實體類主鍵字段內容來判斷到底調用哪一個。這是判斷邏輯是全局通用的所以不用對每一個域對象都去聲明實現。查詢方法也是很直觀的,我們寫一個查詢,綁定一個參數然後執行這條查詢並取得結果。這個方法名其實已經很直觀了,其實我們可以根據方法名就推出查詢語句,所以這裡是可以提升一下的。(在後面可以直接按照這樣的模式寫方法名和參數而不需要去寫實現,SPRING DATA JPA會根據你方法的命名自動轉成SQL。(如果配合良好的數據庫設計和視圖設計,省了不少事情))

Spring Data 存儲庫支持

在我們開始重構該實現之前,看一下示例項目中包含的那些在重構課程中能夠運行的測試用例,確保那些代碼現在依舊能用。下面我們就來看如何改善我們的實現類。

Spring Data JPA 提供了一個存儲庫編程模型,首先,每一個受管理的領域對象都要有一個接口:

public interface AccountRepository extends JpaRepository<account> { … }

定義這個接口是出於兩個目的:首先,通過繼承JpaRepository我們獲得了一組泛型的CRUD方法,使我們能保存Accounts,刪除它們等等。其次,這使得Spring Data JPA存儲庫框架在classpath中掃描該接口,併為它創建一個Spring bean。

為了讓Spring創建一個實現該接口的bean,你僅需要使用Sping JPA"命名空間"並用適當元素激活其對repository 的支持。

<repositories> 

這將掃描包含在包(package)com.acme.repositories下所有的繼承於JpaRepository的接口,併為該接口創建實現了SimpleJpaRepository的Sping bean。下面讓我們邁出第一步,稍微重構我們的AccountService實現以便使用我們新介紹的repository接口。

@Repository 

@Transactional(readOnly = true)
class AccountServiceImpl implements AccountService {
@PersistenceContext
private EntityManager em;
@Autowired
private AccountRepository repository;
@Override
@Transactional
public Account save(Account account) {
return repository.save(account);
}
@Override
public List<account> findByCustomer(Customer customer) {
TypedQuery query = em.createQuery("select a from Account a where a.customer = ?1", Account.class);
query.setParameter(1, customer);
return query.getResultList();
}
}

重構之後,我們將save(...)實際委派給repository。在默認情況下,如果一個實體的主鍵屬性為null,那麼repository實現會將其作為新建實體,正如你在前面的例子中所看到的一樣(注意,如果有必要,你可以對其進行更為詳細的控制) 。除此之外,Spring Data JPA repository實現類已經被@Transactional標註的CRUD等方法可以摒棄@Transactional聲明。

下一步,我們將重構查詢方法。並且使查詢方法與保存方法遵循相同的委派策略。我們在存儲庫接口裡面引入查詢方法並且將我們原有方法委託給新引進的方法:

@Transactional(readOnly = true) 
public interface AccountRepository extends JpaRepository<account> {
List<account> findByCustomer(Customer customer);
}
@Repository
@Transactional(readOnly = true)
class AccountServiceImpl implements AccountService {

@Autowired
private AccountRepository repository;
@Override
@Transactional
public Account save(Account account) {
return repository.save(account);
}
@Override
public List<account> findByCustomer(Customer customer) {
return repository.findByCustomer(Customer customer);
}
}

我們快速補充點事務處理的知識。在這種非常簡單的情況下我們可以從AccountServiceImpl實現類中移除@Transaction註解,因為在存儲庫的CRUD方法已經進行了事務管理,查詢方法在存儲庫接口中也已經用@Transactional(readOnly = true)進行了標註。當前設置——在service層次標記為事務性的方法是一個最佳實踐(儘管在這裡並不需要),因為當你在service層次查看方法時它可以顯式清楚的表明操作是在同一個事務中處理的。除此之外,如果一個service層次的方法修改了,需要進行多次的存儲庫方法調用,這種情況下所有的代碼任然將會在同一個事務中執行,因為在存儲庫內部的事務將會簡單的加入到外部service層次的事務中。存儲庫事務行為和調整方式在 參考文檔中有詳細說明。

嘗試再次運行測試案例,看看測試是否正常運行。打住,我們還沒有實現forfindByCustomer()方法,是不?那它是如何工作的呢?

查詢方法

當Spring Data JPA為創建AccountRepository接口創建Spring實例的時候,它會檢查接口裡面定義的所有查詢方法並且為它們每個都派生一個查詢。默認情況下,Spring Data JPA 將自動解析方法名並以此創建一個查詢,查詢用標準JPA的API實現。在本例中findByCustomer(...)方法在邏輯上等同於JPQL 查詢“select a from Account a where a.customer = ?1”。解析方法名稱的解析器支持大量的關鍵字比如And,Or,GreaterThan,LessThan,Like,IsNull,Notand等等,如果您喜歡,您還可以添加ORDER BY子句。有關詳情請參閱文檔。這種機制給我們提供了一個查詢方法編程模型就像你在Grails 或 Spring Roo中用到的一樣。

現在,假設你想要顯式的使用指定的查詢。要做到這點你可以按照如下命名規約(本例中為Account.findByCustomer)在實體上通過註解或在orm.xml中聲明一個JPA命名的查詢來實現,另外一個選擇就是你可以在存儲庫方法中使用@Query註解:

@Transactional(readOnly = true)
public interface AccountRepository extends JpaRepository<account> {
@Query("")
List<account> findByCustomer(Customer customer);
}

現在我們對CustomerServiceImpl用我們已經看到的特性做一個前後對比:

@Repository
@Transactional(readOnly = true)

public class CustomerServiceImpl implements CustomerService {
@PersistenceContext
private EntityManager em;
@Override
public Customer findById(Long id) {
return em.find(Customer.class, id);
}
@Override
public List<customer> findAll() {
return em.createQuery("select c from Customer c", Customer.class).getResultList();
}
@Override
public List<customer> findAll(int page, int pageSize) {
TypedQuery query = em.createQuery("select c from Customer c", Customer.class);
query.setFirstResult(page * pageSize);
query.setMaxResults(pageSize);
return query.getResultList();
}
@Override
@Transactional
public Customer save(Customer customer) {
// Is new?
if (customer.getId() == null) {
em.persist(customer);
return customer;
} else {
return em.merge(customer);
}
}
@Override
public List<customer> findByLastname(String lastname, int page, int pageSize) {
TypedQuery query = em.createQuery("select c from Customer c where c.lastname = ?1", Customer.class);
query.setParameter(1, lastname);
query.setFirstResult(page * pageSize);
query.setMaxResults(pageSize);
return query.getResultList();
}
}

好吧,首先創建CustomerRepository並消除CRUD方法:

@Transactional(readOnly = true)
public interface CustomerRepository extends JpaRepository<customer> { … }
@Repository
@Transactional(readOnly = true)

public class CustomerServiceImpl implements CustomerService {
@PersistenceContext
private EntityManager em;
@Autowired
private CustomerRepository repository;
@Override
public Customer findById(Long id) {
return repository.findById(id);
}
@Override
public List<customer> findAll() {
return repository.findAll();
}
@Override
public List<customer> findAll(int page, int pageSize) {
TypedQuery query = em.createQuery("select c from Customer c", Customer.class);
query.setFirstResult(page * pageSize);
query.setMaxResults(pageSize);
return query.getResultList();
}
@Override
@Transactional
public Customer save(Customer customer) {
return repository.save(customer);
}
@Override
public List<customer> findByLastname(String lastname, int page, int pageSize) {
TypedQuery query = em.createQuery("select c from Customer c where c.lastname = ?1", Customer.class);
query.setParameter(1, lastname);
query.setFirstResult(page * pageSize);
query.setMaxResults(pageSize);
return query.getResultList();
}
}

到目前為止,一切都很好。以下兩種方法都可以處理常見的場景:你希望給定查詢能夠分頁而不是返回全部實體(比如一頁10條記錄)。眼下我們通過兩個整數適當限制查詢來實現,但這樣做有兩個問題,兩個整數實際上只代表了一個方面,這裡並沒有明確這一點。除此之外,結果我們只返回了一個簡單的list,所以我們丟掉了實際頁面的元數據信息:這是第一頁嗎?這是最後一頁嗎?總共有多少頁?Spring Data提供了一個抽象包括兩個接口:Pageable(捕捉分頁請求)和Page(捕獲結果以及元信息)。讓我們按照如下方式嘗試添加findByLastname(…)到存儲庫接口中並重寫findAll(…)和findByLastname(…)方法:

@Transactional(readOnly = true) 
public interface CustomerRepository extends JpaRepository<customer> {
Page<customer> findByLastname(String lastname, Pageable pageable);
}
@Override
public Page<customer> findAll(Pageable pageable) {
return repository.findAll(pageable);
}
@Override
public Page<customer> findByLastname(String lastname, Pageable pageable) {
return repository.findByLastname(lastname, pageable);
}

請確保按照名字的變化對測試用例進行了適配,這樣它應該能正常運行。歸結下來為兩件事:我們有CRUD方法支持分頁並且查詢執行機制知道分頁的參數。在這個階段,我們的包裝類實際上已經過時了,因為客戶端也可以直接調用我們的存儲庫接口,這樣我們擺脫了整個的實現代碼。

總結

在本文的課程中,我們把為存儲庫編寫的代碼減少到2個接口,包含3個方法一行XML:

@Transactional(readOnly = true) 
public interface CustomerRepository extends JpaRepository<customer> {
Page<customer> findByLastname(String lastname, Pageable pageable);
}
@Transactional(readOnly = true)
public interface AccountRepository extends JpaRepository<account> {
List<account> findByCustomer(Customer customer);
}
<repositories>

我們擁有類型安全的CRUD方法、查詢執行方法和內置的分頁功能。這不僅適用於基於JPA的存儲庫,而且也適用於非關係型數據庫,這很酷。第一個支持這些功能的非關係型數據庫是MongoDB ,它是幾天後發版的Spring Data Document中的一部分。你將會獲得Mongo DB版的所有這些特性,而且我們還將支持其它一些數據庫。另外,還有其它一些特性等待我們去探秘(比如,實體審計,自定義數據訪問代碼集成) ,我們會在後續的博文中涉及。


分享到:


相關文章: