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

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

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

了解更多

获得支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一份简单的订阅。

了解更多

即将举行的活动

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

查看所有