抢先一步
VMware提供培训和认证,助您快速提升。
了解更多由于我们刚刚发布了 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(…)
,因此我们使用 Account
的 id
字段来决定是否将 Account
对象视为新的。当然,可以将此逻辑提取到一个通用的超类中,因为我们可能不希望为每个域对象特定的存储库实现重复此代码。查询方法也很简单:我们创建一个查询,绑定一个参数并执行查询以获取结果。它几乎简单到可以将实现代码视为样板代码,因为只要稍加想象,就可以从方法签名中推导出它:我们期望一个 Account
的 List
,查询非常接近方法名称,我们只需将方法参数绑定到它即可。因此,正如您所看到的,有改进的空间。
在开始重构实现之前,请注意,示例项目包含可以在重构过程中运行的测试用例,以验证代码仍然有效。现在让我们看看如何改进实现。
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 Criteria API 实现。在这种情况下,findByCustomer(…)
方法在逻辑上等效于 JPQL 查询 select a from Account a where a.customer = ?1
。分析方法名称的解析器支持相当多的关键字,例如 And
、Or
、GreaterThan
、LessThan
、Like
、IsNull
、Not
等。如果需要,您还可以添加 OrderBy
子句。有关详细概述,请查看 参考文档。此机制为我们提供了类似于 Grails 或 Spring Roo 中习惯使用的查询方法编程模型。
现在假设您希望明确要使用的查询。为此,您可以在实体上的注释或您的 orm.xml
中声明一个遵循命名约定(在本例中为 Account.findByCustomer
)的 JPA 命名查询。或者,您可以使用 @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版本中发布。您将获得与MongoDB完全相同的功能,我们也在努力支持其他数据库。此外,还有一些其他功能可以探索(例如实体审计、自定义数据访问代码集成),我们将在以后的博文中详细介绍。