工作日志4-28
了解Spring Data JPA,先从 JPA 开始,简单介绍一个 JPA 示例;接着重构该示例,并引入 Spring 框架。
示例主要涉及七个文件,但是很清晰:业务层包含一个接口和一个实现;持久层包含一个接口、一个实现、一个实体类;另外加上一个 JPA 配置文件和一个测试类。相关类 / 接口代码如下:
实体类 AccountInfo.java
@Entity @Table(name = "t_accountinfo") public class AccountInfo implements Serializable { private Long accountId; private Integer balance; // 此处省略 getter 和 setter 方法。 } |
业务层接口 UserService.java
public interface UserService { public AccountInfo createNewAccount(String user, String pwd, Integer init); } |
业务层的实现类 UserServiceImpl.java
public class UserServiceImpl implements UserService { private UserDao userDao = new UserDaoImpl(); public AccountInfo createNewAccount(String user, String pwd, Integer init){ // 封装域对象 AccountInfo accountInfo = new AccountInfo(); UserInfo userInfo = new UserInfo(); userInfo.setUsername(username); userInfo.setPassword(password); accountInfo.setBalance(initBalance); accountInfo.setUserInfo(userInfo); // 调用持久层,完成数据的保存 return userDao.save(accountInfo); } } |
持久层接口
public interface UserDao { public AccountInfo save(AccountInfo accountInfo); } |
持久层的实现类
public class UserDaoImpl implements UserDao { public AccountInfo save(AccountInfo accountInfo) { EntityManagerFactory emf = Persistence.createEntityManagerFactory("SimplePU"); EntityManager em = emf.createEntityManager(); em.getTransaction().begin(); em.persist(accountInfo); em.getTransaction().commit(); emf.close(); return accountInfo; } } |
JPA 标准配置文件 persistence.xml
<?xml version="1.0" encoding="UTF-8"?> <persistence xmlns="http://java.sun.com/xml/ns/persistence" version="2.0"> <persistence-unit name="SimplePU" transaction-type="RESOURCE_LOCAL"> <provider>org.hibernate.ejb.HibernatePersistence</provider> <class>footmark.springdata.jpa.domain.UserInfo</class> <class>footmark.springdata.jpa.domain.AccountInfo</class> <properties> <property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver"/> <property name="hibernate.connection.url" value="jdbc:mysql://10.40.74.197:3306/zhangjp"/> <property name="hibernate.connection.username" value="root"/> <property name="hibernate.connection.password" value="root"/> <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5Dialect"/> <property name="hibernate.show_sql" value="true"/> <property name="hibernate.format_sql" value="true"/> <property name="hibernate.use_sql_comments" value="false"/> <property name="hibernate.hbm2ddl.auto" value="update"/> </properties> </persistence-unit> </persistence> |
测试
public class SimpleSpringJpaDemo { public static void main(String[] args) { new UserServiceImpl().createNewAccount("aa", "123456", 1); } } |
接下来我们引入 Spring,以展示 Spring 框架对 JPA 的支持。业务层接口 UserService 保持不变,UserServiceImpl 中增加了三个注解,以让 Spring 完成依赖注入,因此不再需要使用 new 操作符创建 UserDaoImpl 对象了。同时我们还使用了 Spring 的声明式事务:
配置为 Spring Bean 的业务层实现
@Service("userService") public class UserServiceImpl implements UserService { @Autowired private UserDao userDao; @Transactional public AccountInfo createNewAccount( String name, String pwd, Integer init) { …… } } |
对于持久层,UserDao 接口也不需要修改,只需修改 UserDaoImpl 实现,修改后的代码如下:
配置为 Spring Bean 的持久层实现
@Repository("userDao") public class UserDaoImpl implements UserDao { @PersistenceContext private EntityManager em; @Transactional public Long save(AccountInfo accountInfo) { em.persist(accountInfo); return accountInfo.getAccountId(); } } |
Spring 配置文件
<?xml version="1.0" encoding="UTF-8"?> <beans...> <context:component-scan base-package="footmark.springdata.jpa"/> <tx:annotation-driven transaction-manager="transactionManager"/> <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"> <property name="entityManagerFactory" ref="entityManagerFactory"/> </bean> <bean id="entityManagerFactory" class= "org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> </bean> </beans> |
改后的测试代码
public class SimpleSpringJpaDemo{ public static void main(String[] args){ ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("spring-demo-cfg.xml"); UserDao userDao = ctx.getBean("userDao", UserDao.class); userDao.createNewAccount("aa", "123456", 1); } } |
通过对比重构前后的代码,可以发现 Spring 对 JPA 的简化已经非常出色了,我们可以大致总结一下 Spring 框架对 JPA 提供的支持主要体现在如下几个方面:
- 首先,它使得 JPA 配置变得更加灵活。JPA 规范要求,配置文件必须命名为 persistence.xml,并存在于类路径下的 META-INF 目录中。该文件通常包含了初始化 JPA 引擎所需的全部信息。Spring 提供的 LocalContainerEntityManagerFactoryBean 提供了非常灵活的配置,persistence.xml 中的信息都可以在此以属性注入的方式提供。
- 其次,Spring 实现了部分在 EJB 容器环境下才具有的功能,比如对 @PersistenceContext、@PersistenceUnit 的容器注入支持。
- 第三,也是最具意义的,Spring 将 EntityManager 的创建与销毁、事务管理等代码抽取出来,并由其统一管理,开发者不需要关心这些,如前面的代码所示,业务方法中只剩下操作领域对象的代码,事务管理和 EntityManager 创建、销毁的代码都不再需要开发者关心了。
通过前面的分析可以看出,Spring 对 JPA 的支持已经非常强大,我们只需关心核心业务逻辑的实现代码,无需过多关注 EntityManager 的创建、事务处理等 JPA 相关的处理,这基本上也是作为一个开发框架而言所能做到的极限了。
Spring Data JPA 框架,主要针对的就是 Spring 唯一没有简化到的业务逻辑代码,至此,连仅剩的实现持久层业务逻辑的工作都省了,唯一要做的,就只是声明持久层的接口,其他都交给 Spring Data JPA 来帮你完成!Spring Data JPA 做的是规范方法的名字,根据符合规范的名字来确定方法需要实现什么样的逻辑。
接下来我们针对前面的例子进行改造,让 Spring Data JPA 来帮助我们完成业务逻辑。在着手写代码之前,需要先下载Spring Data JPA 的发布包(需要同时下载 Spring Data Commons 和 Spring Data JPA 两个发布包,Commons 是 Spring Data 的公共基础包),并把相关的依赖 JAR 文件加入到 CLASSPATH 中。
首先,让持久层接口 UserDao 继承 Repository 接口。该接口使用了泛型,需要为其提供两个类型:第一个为该接口处理的域对象类型,第二个为该域对象的主键类型。修改后的 UserDao 如下:
Spring Data JPA 风格的持久层接口
public interface UserDao extends Repository<AccountInfo, Long> { public AccountInfo save(AccountInfo accountInfo); } |
然后删除 UserDaoImpl 类,因为我们前面说过,框架会为我们完成业务逻辑。最后,我们需要在 Spring 配置文件中增加如下配置,以使 Spring 识别出需要为其实现的持久层接口:
在 Spring 配置文件中启用扫描并自动创建代理的功能
<-- 需要在 <beans> 标签中增加对 jpa 命名空间的引用 --> <jpa:repositories base-package="footmark.springdata.jpa.dao" entity-manager-factory-ref="entityManagerFactory" transaction-manager-ref="transactionManager"/> |
至此便大功告成了!执行一下测试代码,然后看一下数据库,新的数据已经如我们预期的添加到表中了。如果要再增加新的持久层业务,比如希望查询出给 ID 的 AccountInfo 对象,该怎么办呢?很简单,在 UserDao 接口中增加一行代码即可:
修改后的持久层接口,增加一个方法声明
public interface UserDao extends Repository<AccountInfo, Long> { public AccountInfo save(AccountInfo accountInfo); // 你需要做的,仅仅是新增如下一行方法声明 public AccountInfo findByAccountId(Long accountId); } |
下面总结一下使用 Spring Data JPA 进行持久层开发大致需要的三个步骤:
- 声明持久层的接口,该接口继承 Repository,Repository 是一个标记型接口,它不包含任何方法,当然如果有需要,Spring Data 也提供了若干 Repository 子接口,其中定义了一些常用的增删改查,以及分页相关的方法。
- 在接口中声明需要的业务方法。Spring Data 将根据给定的策略来为其生成实现代码。
- 在 Spring 配置文件中增加一行声明,让 Spring 为声明的接口创建代理对象。配置了 <jpa:repositories> 后,Spring 初始化容器时将会扫描 base-package 指定的包目录及其子目录,为继承 Repository 或其子接口的接口创建代理对象,并将代理对象注册为 Spring Bean,业务层便可以通过 Spring 自动封装的特性来直接使用该对象。
此外,<jpa:repository> 还提供了一些属性和子标签,便于做更细粒度的控制。可以在 <jpa:repository> 内部使用 <context:include-filter>、<context:exclude-filter> 来过滤掉一些不希望被扫描到的接口。
前面提到,持久层接口继承 Repository 并不是唯一选择。Repository 接口是 Spring Data 的一个核心接口,它不提供任何方法,需要在自己定义的接口中声明需要的方法。与继承 Repository 等价的一种方式,就是在持久层接口上使用 @RepositoryDefinition 注解,并为其指定 domainClass 和 idClass 属性。如下两种方式是完全等价的:
两种等价的继承接口方式示例
public interface UserDao extends Repository<AccountInfo, Long> { …… } @RepositoryDefinition(domainClass = AccountInfo.class, idClass = Long.class) public interface UserDao { …… } |
如果持久层接口较多,且每一个接口都需要声明相似的增删改查方法,直接继承 Repository 就显得有些啰嗦,这时可以继承 CrudRepository,它会自动为域对象创建增删改查方法,供业务层直接使用。开发者只是多写了 "Crud" 四个字母,即刻便为域对象提供了开箱即用的十个增删改查方法。
但是,使用 CrudRepository 也有副作用,它可能暴露了你不希望暴露给业务层的方法。比如某些接口你只希望提供增加的操作而不希望提供删除的方法。针对这种情况,开发者只能退回到 Repository 接口,然后到 CrudRepository 中把希望保留的方法声明复制到自定义的接口中即可。
分页查询和排序是持久层常用的功能,Spring Data 为此提供了 PagingAndSortingRepository 接口,它继承自 CrudRepository 接口,在 CrudRepository 基础上新增了两个与分页有关的方法。但是,我们很少会将自定义的持久层接口直接继承自 PagingAndSortingRepository,而是在继承 Repository 或 CrudRepository 的基础上,在自己声明的方法参数列表最后增加一个 Pageable 或 Sort 类型的参数,用于指定分页或排序信息即可,这比直接使用 PagingAndSortingRepository 提供了更大的灵活性。
JpaRepository 是继承自 PagingAndSortingRepository 的针对 JPA 技术提供的接口,它在父接口的基础上,提供了其他一些方法,比如 flush(),saveAndFlush(),deleteInBatch() 等。如果有这样的需求,则可以继承该接口。
上述四个接口,到底该如何选择?其实依据很简单,根据具体的业务需求,选择其中之一。在通常情况下优先选择 Repository 接口。因为 Repository 接口已经能满足日常需求,其他接口能做到的在 Repository 中也能做到,彼此之间并不存在功能强弱的问题。只是 Repository 需要显示声明需要的方法,而其他则可能已经提供了相关的方法,不需要再显式声明,但如果对 Spring Data JPA 不熟悉,别人在检视代码或者接手相关代码时会有疑惑,他们不明白为什么明明在持久层接口中声明了三个方法,而在业务层使用该接口时,却发现有七八个方法可用,从这个角度而言,应该优先考虑使用 Repository 接口。
前面提到,Spring Data JPA 在后台为持久层接口创建代理对象时,会解析方法名字,并实现相应的功能。除了通过方法名字以外,它还可以通过如下两种方式指定查询语句:
- Spring Data JPA 可以访问 JPA 命名查询语句。只需要在定义命名查询语句时,为其指定一个符合给定格式的名字,Spring Data JPA 便会在创建代理对象时,使用该命名查询语句来实现其功能。
- 还可以直接在声明的方法上面使用 @Query 注解,并提供一个查询语句作为参数,Spring Data JPA 在创建代理对象时,便以提供的查询语句来实现其功能。