本文主要讲述 Spring Data JPA,可是为了避免至于给 JPA 和 Spring 的初学者形成较大的学习曲线,咱们首先从 JPA 开始,简单介绍一个 JPA 示例;接着重构该示例,并引入 Spring 框架,这两部分不会涉及过多的篇幅,若是但愿可以深刻学习 Spring 和 JPA,能够根据本文最后提供的参考资料进一步学习。 html
自 JPA 伴随 Java EE 5 发布以来,受到了各大厂商及开源社区的追捧,各类商用的和开源的 JPA 框架如雨后春笋般出现,为开发者提供了丰富的选择。它一改以前 EJB 2.x 中实体 Bean 笨重且难以使用的形象,充分吸取了在开源社区已经相对成熟的 ORM 思想。另外,它并不依赖于 EJB 容器,能够做为一个独立的持久层技术而存在。目前比较成熟的 JPA 框架主要包括 Jboss 的 Hibernate EntityManager、Oracle 捐献给 Eclipse 社区的 EclipseLink、Apache 的 OpenJPA 等。 java
本文的示例代码基于 Hibernate EntityManager 开发,可是读者几乎不用修改任何代码,即可以很是容易地切换到其余 JPA 框架,由于代码中使用到的都是 JPA 规范提供的接口 / 类,并无使用到框架自己的私有特性。示例主要涉及七个文件,可是很清晰:业务层包含一个接口和一个实现;持久层包含一个接口、一个实现、一个实体类;另外加上一个 JPA 配置文件和一个测试类。相关类 / 接口代码以下: mysql
@Entity @Table(name = "t_accountinfo") public class AccountInfo implements Serializable { private Long accountId; private Integer balance; // 此处省略 getter 和 setter 方法。 } |
public interface UserService { public AccountInfo createNewAccount(String user, String pwd, Integer init); } |
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; } } |
<?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("ZhangJianPing", "123456", 1); } } |
回页首 spring
接下来咱们引入 Spring,以展现 Spring 框架对 JPA 的支持。业务层接口 UserService 保持不变,UserServiceImpl 中增长了三个注解,以让 Spring 完成依赖注入,所以再也不须要使用 new 操做符建立 UserDaoImpl 对象了。同时咱们还使用了 Spring 的声明式事务: 数据库
@Service("userService") public class UserServiceImpl implements UserService { @Autowired private UserDao userDao; @Transactional public AccountInfo createNewAccount( String name, String pwd, Integer init) { …… } } |
对于持久层,UserDao 接口也不须要修改,只需修改 UserDaoImpl 实现,修改后的代码以下: 数组
@Repository("userDao") public class UserDaoImpl implements UserDao { @PersistenceContext private EntityManager em; @Transactional public Long save(AccountInfo accountInfo) { em.persist(accountInfo); return accountInfo.getAccountId(); } } |
<?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("ZhangJianPing", "123456", 1); } } |
经过对比重构先后的代码,能够发现 Spring 对 JPA 的简化已经很是出色了,咱们能够大体总结一下 Spring 框架对 JPA 提供的支持主要体如今以下几个方面: 网络
回页首 app
经过前面的分析能够看出,Spring 对 JPA 的支持已经很是强大,开发者只需关心核心业务逻辑的实现代码,无需过多关注 EntityManager 的建立、事务处理等 JPA 相关的处理,这基本上也是做为一个开发框架而言所能作到的极限了。然而,Spring 开发小组并无止步,他们再接再砺,于最近推出了 Spring Data JPA 框架,主要针对的就是 Spring 惟一没有简化到的业务逻辑代码,至此,开发者连仅剩的实现持久层业务逻辑的工做都省了,惟一要作的,就只是声明持久层的接口,其余都交给 Spring Data JPA 来帮你完成!
至此,读者可能会存在一个疑问,框架怎么可能代替开发者实现业务逻辑呢?毕竟,每个应用的持久层业务甚至领域对象都不尽相同,框架是怎么作到的呢?其实这背后的思想并不复杂,好比,当你看到 UserDao.findUserById() 这样一个方法声明,大体应该能判断出这是根据给定条件的 ID 查询出知足条件的 User 对象。Spring Data JPA 作的即是规范方法的名字,根据符合规范的名字来肯定方法须要实现什么样的逻辑。
接下来咱们针对前面的例子进行改造,让 Spring Data JPA 来帮助咱们完成业务逻辑。在着手写代码以前,开发者须要先 下载Spring Data JPA 的发布包(须要同时下载 Spring Data Commons 和 Spring Data JPA 两个发布包,Commons 是 Spring Data 的公共基础包),并把相关的依赖 JAR 文件加入到 CLASSPATH 中。
首先,让持久层接口 UserDao 继承 Repository 接口。该接口使用了泛型,须要为其提供两个类型:第一个为该接口处理的域对象类型,第二个为该域对象的主键类型。修改后的 UserDao 以下:
public interface UserDao extends Repository<AccountInfo, Long> { public AccountInfo save(AccountInfo accountInfo); } |
而后删除 UserDaoImpl 类,由于咱们前面说过,框架会为咱们完成业务逻辑。最后,咱们须要在 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 进行持久层开发大体须要的三个步骤:
此外,<jpa:repository> 还提供了一些属性和子标签,便于作更细粒度的控制。能够在 <jpa:repository> 内部使用 <context:include-filter>、<context:exclude-filter> 来过滤掉一些不但愿被扫描到的接口。具体的使用方法见 Spring参考文档。
前面提到,持久层接口继承 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 吸引开发者的一个很重要的因素。该功能其实并不是 Spring Data JPA 独创,而是源自一个开源的 JPA 框架 Hades,该框架的做者 Oliver Gierke 自己又是 Spring Data JPA 项目的 Leader,因此把 Hades 的优点引入到 Spring Data JPA 也就是瓜熟蒂落的了。
框架在进行方法名解析时,会先把方法名多余的前缀截取掉,好比 find、findBy、read、readBy、get、getBy,而后对剩下部分进行解析。而且若是方法的最后一个参数是 Sort 或者 Pageable 类型,也会提取相关的信息,以便按规则进行排序或者分页查询。
在建立查询时,咱们经过在方法名中使用属性名称来表达,好比 findByUserAddressZip ()。框架在解析该方法时,首先剔除 findBy,而后对剩下的属性进行解析,详细规则以下(此处假设该方法针对的域对象为 AccountInfo 类型):
可能会存在一种特殊状况,好比 AccountInfo 包含一个 user 的属性,也有一个 userAddress 属性,此时会存在混淆。读者能够明确在属性之间加上 "_" 以显式表达意图,好比 "findByUser_AddressZip()" 或者 "findByUserAddress_Zip()"。
在查询时,一般须要同时根据多个属性进行查询,且查询的条件也格式各样(大于某个值、在某个范围等等),Spring Data JPA 为此提供了一些表达条件查询的关键字,大体以下:
使用 @Query 建立查询
@Query 注解的使用很是简单,只需在声明的方法上面标注该注解,同时提供一个 JP QL 查询语句便可,以下所示:
public interface UserDao extends Repository<AccountInfo, Long> { @Query("select a from AccountInfo a where a.accountId = ?1") public AccountInfo findByAccountId(Long accountId); @Query("select a from AccountInfo a where a.balance > ?1") public Page<AccountInfo> findByBalanceGreaterThan( Integer balance,Pageable pageable); } |
不少开发者在建立 JP QL 时喜欢使用命名参数来代替位置编号,@Query 也对此提供了支持。JP QL 语句中经过": 变量"的格式来指定参数,同时在方法的参数前面使用 @Param 将方法参数与 JP QL 中的命名参数对应,示例以下:
public interface UserDao extends Repository<AccountInfo, Long> { public AccountInfo save(AccountInfo accountInfo); @Query("from AccountInfo a where a.accountId = :id") public AccountInfo findByAccountId(@Param("id")Long accountId); @Query("from AccountInfo a where a.balance > :balance") public Page<AccountInfo> findByBalanceGreaterThan( @Param("balance")Integer balance,Pageable pageable); } |
此外,开发者也能够经过使用 @Query 来执行一个更新操做,为此,咱们须要在使用 @Query 的同时,用 @Modifying 来将该操做标识为修改查询,这样框架最终会生成一个更新的操做,而非查询。以下所示:
@Modifying @Query("update AccountInfo a set a.salary = ?1 where a.salary < ?2") public int increaseSalary(int after, int before); |
命名查询是 JPA 提供的一种将查询语句从方法体中独立出来,以供多个方法共用的功能。Spring Data JPA 对命名查询也提供了很好的支持。用户只须要按照 JPA 规范在 orm.xml 文件或者在代码中使用 @NamedQuery(或 @NamedNativeQuery)定义好查询语句,惟一要作的就是为该语句命名时,须要知足”DomainClass.methodName()”的命名规则。假设定义了以下接口:
public interface UserDao extends Repository<AccountInfo, Long> { ...... public List<AccountInfo> findTop5(); } |
若是但愿为 findTop5() 建立命名查询,并与之关联,咱们只须要在适当的位置定义命名查询语句,并将其命名为 "AccountInfo.findTop5",框架在建立代理类的过程当中,解析到该方法时,优先查找名为 "AccountInfo.findTop5" 的命名查询定义,若是没有找到,则尝试解析方法名,根据方法名字建立查询。
Spring Data JPA 在为接口建立代理对象时,若是发现同时存在多种上述状况可用,它该优先采用哪一种策略呢?为此,<jpa:repositories> 提供了 query-lookup-strategy 属性,用以指定查找的顺序。它有以下三个取值:
默认状况下,Spring Data JPA 实现的方法都是使用事务的。针对查询类型的方法,其等价于 @Transactional(readOnly=true);增删改类型的方法,等价于 @Transactional。能够看出,除了将查询的方法设为只读事务外,其余事务属性均采用默认值。
若是用户以为有必要,能够在接口方法上使用 @Transactional 显式指定事务属性,该值覆盖 Spring Data JPA 提供的默认值。同时,开发者也能够在业务层方法上使用 @Transactional 指定事务属性,这主要针对一个业务层方法屡次调用持久层方法的状况。持久层的事务会根据设置的事务传播行为来决定是挂起业务层事务仍是加入业务层的事务。具体 @Transactional 的使用,请参考Spring的参考文档。
有些时候,开发者可能须要在某些方法中作一些特殊的处理,此时自动生成的代理对象不能彻底知足要求。为了享受 Spring Data JPA 带给咱们的便利,同时又可以为部分方法提供自定义实现,咱们能够采用以下的方法:
<jpa:repositories base-package="footmark.springdata.jpa.dao"> <jpa:repository id="accountDao" repository-impl-ref=" accountDaoPlus " /> </jpa:repositories> <bean id="accountDaoPlus" class="......."/> |
此外,<jpa:repositories > 提供了一个 repository-impl-postfix 属性,用以指定实现类的后缀。假设作了以下配置:
<jpa:repositories base-package="footmark.springdata.jpa.dao" repository-impl-postfix="Impl"/> |
则在框架扫描到 AccountDao 接口时,它将尝试在相同的包目录下查找 AccountDaoImpl.java,若是找到,便将其中的实现方法做为最终生成的代理类中相应方法的实现。
本文主要介绍了 Spring Data JPA 的使用,以及它与 Spring 框架的无缝集成。Spring Data JPA 其实并不依赖于 Spring 框架,有兴趣的读者能够参考本文最后的"参考资源"进一步学习。
下载
描述 | 名字 | 大小 | 下载方法 |
---|---|---|---|
样例代码 | sample-code.rar | 5KB | HTTP |
学习
讨论