Spring Data JPA 入门

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

我们刚刚发布了 Spring Data JPA 项目的第一个里程碑版本,我想快速介绍一下它的功能。正如您可能知道的,Spring 框架提供了构建基于 JPA 的数据访问层的支持。那么 Spring Data JPA 在此基础支持上增加了什么呢?为了回答这个问题,我想先从使用普通 JPA + Spring 实现的示例领域的数据访问组件入手,并指出有待改进的领域。之后,我将重构实现以使用 Spring Data JPA 的特性来解决这些问题领域。示例项目以及重构步骤的分步指南可以在 Github 上找到。

领域模型

为了简单起见,我们从一个很小的、众所周知的领域模型开始:我们有拥有 AccountCustomer
@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 对象是否为新对象。这段逻辑当然可以提取到一个公共超类中,因为我们可能不想为每个特定领域对象的仓库实现重复这段代码。查询方法也非常直观:我们创建一个查询,绑定参数,然后执行查询以获取结果。它几乎是如此直观,以至于可以说实现代码是样板代码,只需稍加想象,就可以从方法签名中推导出来:我们期望一个 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 的接口,并为其创建一个由 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。分析方法名称的解析器支持相当多的关键字,例如 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();
  }
}

目前为止一切顺利。现在剩下的是处理一个常见场景的两个方法:您不想访问给定查询的所有实体,而只访问其中的一页(例如,页大小为 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 完全相同的功能,我们也在努力支持其他数据库。还有一些额外的功能有待探索(例如,实体审计、自定义数据访问代码的集成),我们将在后续的博客文章中介绍这些内容。

订阅 Spring 时事通讯

通过 Spring 时事通讯保持联系

订阅

取得领先

VMware 提供培训和认证,助您加速前进。

了解更多

获取支持

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

了解更多

即将举行的活动

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

查看全部