领先一步
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 对象视为新的。当然,这个逻辑可以提取到一个通用的超类中,因为我们可能不希望为每个特定域对象的仓库实现重复这段代码。查询方法也相当直接:我们创建一个查询,绑定一个参数,然后执行查询以获取结果。它几乎是如此直接,以至于有人可能会认为实现代码是样板代码,因为只需一点想象力,它就可以从方法签名中推导出来:我们期望一个 List of 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 的接口,并为其创建一个 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。分析方法名的解析器支持相当多的关键字,如 And、Or、GreaterThan、LessThan、Like、IsNull、Not 等等。如果您愿意,还可以添加 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 完全相同的功能,并且我们还在开发对其他数据库的支持。还有其他要探索的功能(例如,实体审计、自定义数据访问代码的集成),我们将在接下来的博文中进行介绍。