取得领先
VMware 提供培训和认证,助您加速前进。
了解更多我们刚刚发布了 Spring Data JPA 项目的第一个里程碑版本,我想快速介绍一下它的功能。正如您可能知道的,Spring 框架提供了构建基于 JPA 的数据访问层的支持。那么 Spring Data JPA 在此基础支持上增加了什么呢?为了回答这个问题,我想先从使用普通 JPA + Spring 实现的示例领域的数据访问组件入手,并指出有待改进的领域。之后,我将重构实现以使用 Spring Data JPA 的特性来解决这些问题领域。示例项目以及重构步骤的分步指南可以在 Github 上找到。
Account
的 Customer
。@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
列表,查询与方法名称非常接近,我们只需将方法参数绑定到它。因此,正如您所见,这里还有改进的空间。
在我们开始重构实现之前,请注意示例项目包含测试用例,可以在重构过程中运行这些测试用例以验证代码是否仍然有效。现在我们来看一下如何改进实现。
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
的接口,并为其创建一个由 SimpleJpaRepository
实现支持的 Spring Bean。让我们迈出第一步,稍微重构一下 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();
}
}
目前为止一切顺利。现在剩下的是处理一个常见场景的两个方法:您不想访问给定查询的所有实体,而只访问其中的一页(例如,页大小为 10 的第 1 页)。目前这是通过两个整数来适当地限制查询来解决的。这有两个问题。这两个整数共同代表了一个概念,但在这里没有明确说明。此外,我们返回的是一个简单的 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 完全相同的功能,我们也在努力支持其他数据库。还有一些额外的功能有待探索(例如,实体审计、自定义数据访问代码的集成),我们将在后续的博客文章中介绍这些内容。