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 对象视为新的。当然,可以将此逻辑提取到一个通用的超类中,因为我们可能不希望为每个域对象特定的存储库实现重复此代码。查询方法也很简单:我们创建一个查询,绑定一个参数并执行查询以获取结果。它几乎简单到可以将实现代码视为样板代码,因为只要稍加想象,就可以从方法签名中推导出它:我们期望一个 AccountList,查询非常接近方法名称,我们只需将方法参数绑定到它即可。因此,正如您所看到的,有改进的空间。

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 Criteria API 实现。在这种情况下,findByCustomer(…) 方法在逻辑上等效于 JPQL 查询 select a from Account a where a.customer = ?1。分析方法名称的解析器支持相当多的关键字,例如 AndOrGreaterThanLessThanLikeIsNullNot 等。如果需要,您还可以添加 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完全相同的功能,我们也在努力支持其他数据库。此外,还有一些其他功能可以探索(例如实体审计、自定义数据访问代码集成),我们将在以后的博文中详细介绍。

获取Spring通讯

通过Spring通讯保持联系

订阅

抢先一步

VMware提供培训和认证,助您快速提升。

了解更多

获取支持

Tanzu Spring在一个简单的订阅中提供OpenJDK™、Spring和Apache Tomcat®的支持和二进制文件。

了解更多

即将举行的活动

查看Spring社区中所有即将举行的活动。

查看全部