開始使用 Spring Data JPA

工程 | Oliver Drotbohm | 2011年2月10日 | ...

在我們剛剛釋出了 Spring Data JPA 專案的第一個里程碑之際,我想向您快速介紹一下它的功能。您可能知道,Spring 框架提供了支援構建基於 JPA 的資料訪問層。那麼 Spring Data JPA 在此基礎支援上又增加了什麼呢?為了回答這個問題,我想從一個使用純 JPA + Spring 實現的示例域的資料訪問元件開始,並指出有改進空間的地方。在我們完成這一切之後,我將重構這些實現,以使用 Spring Data JPA 的功能來解決這些問題。示例專案以及重構步驟的逐步指南可以在 Github 上找到。

為了保持簡單,我們從一個微小而熟悉的域開始:我們有 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
}

Account 有一個有效期,我們將在後續階段使用它。除此之外,類或對映本身並沒有什麼特別之處——它們使用純 JPA 註釋。現在,讓我們來看看管理 Account 物件的元件。

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

我特意將類命名為 *Service,以避免命名衝突,因為我們在開始重構時將引入一個倉庫層。但從概念上講,這裡的類更像是一個倉庫,而不是一個服務。那麼,我們這裡實際上有什麼呢?

該類用 @Repository 註釋,以支援將 JPA 異常轉換為 Spring 的 DataAccessException 層次結構。除此之外,我們使用 @Transactional 來確保 save(…) 操作在事務中執行,並允許設定(在類級別)findByCustomer(…)readOnly 標誌。這會在持久化提供者內部以及資料庫級別帶來一些效能最佳化。

由於我們希望將客戶端從決定是在 EntityManager 上呼叫 merge(…) 還是 persist(…) 的決策中解放出來,因此我們使用 Accountid 欄位來決定我們是否將 Account 物件視為新的。當然,這個邏輯可以提取到一個通用的超類中,因為我們可能不希望為每個特定域物件的倉庫實現重複這段程式碼。查詢方法也相當直接:我們建立一個查詢,繫結一個引數,然後執行查詢以獲取結果。它幾乎是如此直接,以至於有人可能會認為實現程式碼是樣板程式碼,因為只需一點想象力,它就可以從方法簽名中推匯出來:我們期望一個 List of Account,查詢非常接近方法名,我們只需將方法引數繫結到它。所以,正如您所看到的,還有改進的空間。

Spring Data 倉庫支援

在開始重構實現之前,請注意示例專案包含可以在重構過程中執行的測試用例,以驗證程式碼仍然有效。現在讓我們看看如何改進實現。

Spring Data JPA 提供了一個倉庫程式設計模型,它從每個託管域物件的介面開始

public interface AccountRepository extends JpaRepository<Account, Long> { … }

定義這個介面有兩個目的:首先,透過擴充套件 JpaRepository,我們在型別中獲得了一系列通用的 CRUD 方法,這些方法允許儲存 Account、刪除它們等等。其次,這將允許 Spring Data JPA 倉庫基礎設施掃描類路徑以查詢此介面,併為其建立一個 Spring bean。

為了讓 Spring 建立一個實現此介面的 bean,您只需要使用 Spring JPA 名稱空間並透過適當的元素啟用倉庫支援

<jpa:repositories base-package="com.acme.repositories" />

這會掃描 com.acme.repositories 下的所有包,查詢擴充套件 JpaRepository 的介面,併為其建立一個 Spring bean,該 bean 由 SimpleJpaRepository 的實現支援。讓我們邁出第一步,稍微重構我們的 AccountService 實現,以使用我們新引入的倉庫介面

@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(…) 呼叫委託給倉庫。預設情況下,倉庫實現會將一個實體視為新的,如果它的 id 屬性為 null,就像您在前面的示例中看到的那樣(請注意,如果需要,您可以對該決策獲得更詳細的控制)。此外,我們可以擺脫該方法的 @Transactional 註釋,因為 Spring Data JPA 倉庫實現的 CRUD 方法已經用 @Transactional 註釋了。

接下來我們將重構查詢方法。讓我們對查詢方法採取與儲存方法相同的委託策略。我們在倉庫介面中引入一個查詢方法,並讓我們的原始方法委託給這個新引入的方法

@Transactional(readOnly = true) 
public interface AccountRepository extends JpaRepository<Account, Long> {

  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 類中的 @Transactional 註釋,因為倉庫的 CRUD 方法是事務性的,並且查詢方法已經在倉庫介面上標記了 @Transactional(readOnly = true)。當前的設定,即服務層的類被標記為事務性(即使在此情況下不需要),是最好的,因為它在檢視服務層時清楚地表明操作是在事務中進行的。此外,如果一個服務層方法被修改為對多個倉庫方法進行呼叫,所有程式碼仍將在單個事務中執行,因為倉庫的內部事務將簡單地加入在服務層啟動的外部事務。倉庫的事務行為以及調整它的可能性在 參考文件 中有詳細記錄。

嘗試再次執行測試用例,看看它是否有效。等等,我們沒有為 findByCustomer(…) 提供任何實現,對吧?這是如何工作的?

查詢方法

當 Spring Data JPA 為 AccountRepository 介面建立 Spring bean 例項時,它會檢查其中定義的所有查詢方法,併為每個方法派生一個查詢。預設情況下,Spring Data JPA 會自動解析方法名並從中建立一個查詢。該查詢使用 JPA 規範 API 實現。在這種情況下,findByCustomer(…) 方法在邏輯上等同於 JPQL 查詢 select a from Account a where a.customer = ?1。分析方法名的解析器支援相當多的關鍵字,如 AndOrGreaterThanLessThanLikeIsNullNot 等等。如果您願意,還可以新增 OrderBy 子句。有關詳細概述,請參閱 參考文件。此機制為我們提供了與您從 Grails 或 Spring Roo 中熟悉的查詢方法程式設計模型。

現在,假設您想明確指定要使用的查詢。為此,您可以在實體上的註釋或 orm.xml 中宣告一個遵循命名約定的 JPA 命名查詢(在本例中為 Account.findByCustomer)。或者,您可以使用 @Query 註釋您的倉庫方法

@Transactional(readOnly = true)
public interface AccountRepository extends JpaRepository<Account, Long> {

  @Query("<JPQ statement here>")
  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, Long> { … }
@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();
  }
}

到目前為止一切順利。現在剩下的是兩個處理常見場景的方法:您不想訪問給定查詢的所有實體,而是隻想訪問其中的一頁(例如,第 1 頁,頁面大小為 10)。目前,這透過兩個整數來解決,這兩個整數會適當限制查詢。這裡有兩個問題。這兩個整數組合在一起實際上代表了一個概念,但在此並未明確說明。此外,我們返回一個簡單的 List,因此我們丟失了關於實際資料頁的元資料資訊:這是第一頁嗎?是最後一頁嗎?總共有多少頁?Spring Data 提供了一個由兩個介面組成的抽象:Pageable(用於捕獲分頁請求資訊)以及 Page(用於捕獲結果以及元資訊)。因此,讓我們嘗試將 findByLastname(…) 新增到倉庫介面,並像這樣重寫 findAll(…)findByLastname(…)

@Transactional(readOnly = true) 
public interface CustomerRepository extends JpaRepository<Customer, Long> {

  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 方法,並且查詢執行機制也知道 Pageable 引數。在這個階段,我們的包裝類實際上變得多餘了,因為客戶端可以直接使用我們的倉庫介面。我們消除了所有的實現程式碼。

總結

在這篇博文中,我們將倉庫的程式碼量減少到兩個介面,包含 3 個方法,以及一行 XML

@Transactional(readOnly = true) 
public interface CustomerRepository extends JpaRepository<Customer, Long> {

    Page<Customer> findByLastname(String lastname, Pageable pageable); 
}
@Transactional(readOnly = true)
public interface AccountRepository extends JpaRepository<Account, Long> {

    List<Account> findByCustomer(Customer customer); 
}
<jpa:repositories base-package="com.acme.repositories" />

我們內建了型別安全的 CRUD 方法、查詢執行和分頁。很酷的是,這不僅適用於基於 JPA 的倉庫,也適用於非關係型資料庫。第一個支援此方法的非關係型資料庫將是 MongoDB,作為 Spring Data Document 版本釋出的一部分,將在幾天內推出。您將獲得與 Mongo DB 完全相同的功能,並且我們還在開發對其他資料庫的支援。還有其他要探索的功能(例如,實體審計、自定義資料訪問程式碼的整合),我們將在接下來的博文中進行介紹。

獲取 Spring 新聞通訊

透過 Spring 新聞通訊保持聯絡

訂閱

領先一步

VMware 提供培訓和認證,助您加速進步。

瞭解更多

獲得支援

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位制檔案,只需一份簡單的訂閱。

瞭解更多

即將舉行的活動

檢視 Spring 社群所有即將舉行的活動。

檢視所有