在我们刚刚发布项目Spring Data JPA的第一个里程碑时,我想给你一个关于它的简要介绍.正如你所知道的,Spring framework 对于基于JPA的数据存取层提供了支持。那么 Spring Data JPA 是如何添加到Spring中的呢?回答这个问题,我想从一个数据存取组件开始。这个组件提供了一个简单的域(domain),它是用纯JPA和 Spring实现的,而且可以扩展和改进。在我们实现之后,我将用Spring Data JPA 来重构它。你在以在 GitHub上找到这个小项目的每一次重构的详细指导。
域(The domain)
为了保持简单,我从最简单常用的域开始:客户(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
}
帐户只有一个到期日(expriyDate).再无其他. - 它使用JPA注解.现在我们来看看管理帐号的组件.
@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();
}
}
为了在后面重构引入存储层(repository layer)时不引起名称冲突,我特意命名为 class*Service.但是在概念上,这个类是一个存储对象,而不是服务.事实上我们有吗?
这个被@Repository 注释的类抛出的异常可以被Spring的DataAccessException捕获。另外我们还用到了@Transactional 来保证 save(...) 操作的事务性和该类其他方法(这里是findByCustomer(...))的事务只读标识。这样会对数据库性能和我们持久化提供器的性能有一定的优化。
由于我们不想让程序员去决定什么时候调用EntityManager的merge(...)或者persist(...)方法。我们用了实体类主键字段内容来判断到底调用哪一个。这是判断逻辑是全局通用的所以不用对每一个域对象都去声明实现。查询方法也是很直观的,我们写一个查询,绑定一个参数然后执行这条查询并取得结果。这个方法名其实已经很直观了,其实我们可以根据方法名就推出查询语句,所以这里是可以提升一下的。(在后面可以直接按照这样的模式写方法名和参数而不需要去写实现,SPRING DATA JPA会根据你方法的命名自动转成SQL。(如果配合良好的数据库设计和视图设计,省了不少事情))
Spring Data 存储库支持
在我们开始重构该实现之前,看一下示例项目中包含的那些在重构课程中能够运行的测试用例,确保那些代码现在依旧能用。下面我们就来看如何改善我们的实现类。
Spring Data JPA 提供了一个存储库编程模型,首先,每一个受管理的领域对象都要有一个接口:
public interface AccountRepository extends JpaRepository<Account, Long> { … }
定义这个接口是出于两个目的:首先,通过继承JpaRepository我们获得了一组泛型的CRUD方法,使我们能保存Accounts,删除它们等等。其次,这使得Spring Data JPA存储库框架在classpath中扫描该接口,并为它创建一个Spring bean。
为了让Spring创建一个实现该接口的bean,你仅需要使用Sping JPA"命名空间"并用适当元素激活其对repository 的支持。
<jpa:repositories base-package="com.acme.repositories" />
这将扫描包含在包(package)com.acme.repositories下所有的继承于JpaRepository的接口,并为该接口创建实现了SimpleJpaRepository的Sping bean。下面让我们迈出第一步,稍微重构我们的AccountService实现以便使用我们新介绍的repository接口。
@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(...)实际委派给repository。在默认情况下,如果一个实体的主键属性为null,那么repository实现会将其作为新建实体,正如你在前面的例子中所看到的一样(注意,如果有必要,你可以对其进行更为详细的控制) 。除此之外,Spring Data JPA repository实现类已经被@Transactional标注的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实现类中移除@Transaction注解,因为在存储库的CRUD方法已经进行了事务管理,查询方法在存储库接口中也已经用@Transactional(readOnly = true)进行了标注。当前设置——在service层次标记为事务性的方法是一个最佳实践(尽管在这里并不需要),因为当你在service层次查看方法时它可以显式清楚的表明操作是在同一个事务中处理的。除此之外,如果一个service层次的方法修改了,需要进行多次的存储库方法调用,这种情况下所有的代码任然将会在同一个事务中执行,因为在存储库内部的事务将会简单的加入到外部service层次的事务中。存储库事务行为和调整方式在 参考文档中有详细说明。
尝试再次运行测试案例,看看测试是否正常运行。打住,我们还没有实现forfindByCustomer()方法,是不?那它是如何工作的呢?