本文将从示例、原理、应用3个方面介绍spring data jpa。java
如下分析基于spring boot 2.0 + spring 5.0.4
版本源码mysql
JPA (Java Persistence API) 是 Sun 官方提出的 Java 持久化规范。它为 Java 开发人员提供了一种对象/关联映射工具来管理 Java 应用中的关系数据。他的出现主要是为了简化现有的持久化开发工做和整合 ORM 技术,结束如今 Hibernate,TopLink,JDO 等 ORM 框架各自为营的局面。值得注意的是,JPA 是在充分吸取了现有 Hibernate,TopLink,JDO 等ORM框架的基础上发展而来的,具备易于使用,伸缩性强等优势。从目前的开发社区的反应上看,JPA 受到了极大的支持和赞赏,其中就包括了 Spring 与 EJB3.0 的开发团队。git
注意:JPA 是一套规范,不是一套产品,那么像 Hibernate,TopLink,JDO 他们是一套产品,若是说这些产品实现了这个 JPA 规范,那么咱们就能够叫他们为 JPA 的实现产品。
Spring Data JPA 是 Spring 基于 ORM 框架、JPA 规范的基础上封装的一套 JPA 应用框架,底层使用了 Hibernate 的 JPA 技术实现,可以使开发者用极简的代码便可实现对数据的访问和操做。它提供了包括增删改查等在内的经常使用功能,且易于扩展!学习并使用 Spring Data JPA 能够极大提升开发效率!程序员
spring data jpa 让咱们解脱了 DAO 层的操做,基本上全部 CRUD 均可以依赖于它来实现
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency>
为什么不指定版本号呢?
由于 spring boot 的 pom 依赖了 parent,部分 jar 包的版本已在 parent 中指定,故不建议显示指定正则表达式
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.0.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent>
spring.datasource.url=jdbc:mysql://*:*/test?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC&useSSL=true spring.datasource.username= spring.datasource.password= spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.jpa.properties.hibernate.hbm2ddl.auto=update spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect spring.jpa.show-sql=true
配置就这么简单,下面简单介绍下spring.jpa.properties.hibernate.hbm2ddl.auto
有几种配置:算法
不配置此项,表示禁用自动建表功能spring
创建 entitysql
@Entity @Data public class User { @Id @GeneratedValue private long id; @Column(nullable = false, unique = true) private String userName; @Column(nullable = false) private String password; @Column(nullable = false) private int age; }
声明 UserRepository
接口,继承JpaRepository
,默认支持简单的 CRUD 操做,很是方便数据库
public interface UserRepository extends JpaRepository<User, Long> { User findByUserName(String userName); }
@Slf4j public class UserTest extends ApplicationTests { @Autowired private UserRepository userRepository; @Test @Transactional public void userTest() { User user = new User(); user.setUserName("wyk"); user.setAge(30); user.setPassword("aaabbb"); userRepository.save(user); User item = userRepository.findByUserName("wyk"); log.info(JsonUtils.toJson(item)); } }
这里标记@Transactional
,开启事务功能是为了单元测试的时候不形成垃圾数据缓存
源代码下载: 请戳这里
不少人会有疑问,直接声明接口不须要具体实现就能完成数据库的操做?下面就简单介绍下 spring data jpa 的实现原理。
对单测进行debug,能够发现userRepository
被注入了一个动态代理,被代理的类是JpaRepository
的一个实现SimpleJpaRespositry
继续往下debug,在进到findByUserName
方法的时候,发现被上文提到的JdkDynamicAopProxy
捕获,而后通过一系列的方法拦截,最终进到QueryExecutorMethodInterceptor.doInvoke
中。这个拦截器主要作的事情就是判断方法类型,而后执行对应的操做.
咱们的findByUserName
属于自定义查询,因而就进入了查询策略对应的execute
方法。在执行execute
时,会先选取对应的JpaQueryExecution
,调用AbtractJpaQuery.getExecution()
protected JpaQueryExecution getExecution() {
if (method.isStreamQuery()) { return new StreamExecution(); } else if (method.isProcedureQuery()) { return new ProcedureExecution(); } else if (method.isCollectionQuery()) { return new CollectionExecution(); } else if (method.isSliceQuery()) { return new SlicedExecution(method.getParameters()); } else if (method.isPageQuery()) { return new PagedExecution(method.getParameters()); } else if (method.isModifyingQuery()) { return method.getClearAutomatically() ? new ModifyingExecution(method, em) : new ModifyingExecution(method, null); } else { return new SingleEntityExecution(); } }
如上述代码所示,根据method变量实例化时的查询设置方式,实例化不一样的JpaQueryExecution子类实例去运行。咱们的findByUserName
最终落入了SingleEntityExecution
—— 返回单个实例的 Execution
。继续跟踪execute
方法,发现底层使用了 hibernate 的 CriteriaQueryImpl
完成了sql的拼装,这里就不作赘述了。
再来看看这类的method
。在 spring-data-jpa 中,JpaQueryMethod
就是Repository
接口中带有@Query
注解方法的所有信息,包括注解,类名,实参等的存储类,因此Repository
接口有多少个@Query
注解方法,就会包含多少个JpaQueryMethod
实例被加入监听序列。实际运行时,一个RepositoryQuery
实例持有一个JpaQueryMethod
实例,JpaQueryMethod
又持有一个Method
实例。
再来看看RepositoryQuery
,在QueryExecutorMethodInterceptor
中维护了一个Map<Method, RepositoryQuery> queries
。RepositoryQuery
的直接抽象子类是AbstractJpaQuery
,能够看到,一个RepositoryQuery
实例持有一个JpaQueryMethod
实例,JpaQueryMethod
又持有一个Method
实例,因此RepositoryQuery
实例的用途很明显,一个RepositoryQuery
表明了Repository
接口中的一个方法,根据方法头上注解不一样的形态,将每一个Repository
接口中的方法分别映射成相对应的RepositoryQuery
实例。
下面咱们就来看看spring-data-jpa对RepositoryQuery实例的具体分类:
1.SimpleJpaQuery
方法头上@Query注解的nativeQuery属性缺省值为false,也就是使用JPQL,此时会建立SimpleJpaQuery实例,并经过两个StringQuery类实例分别持有query jpql语句和根据query jpql计算拼接出来的countQuery jpql语句;
2.NativeJpaQuery
方法头上@Query注解的nativeQuery属性若是显式的设置为nativeQuery=true,也就是使用原生SQL,此时就会建立NativeJpaQuery实例;
3.PartTreeJpaQuery
方法头上未进行@Query注解,将使用spring-data-jpa首创的方法名识别的方式进行sql语句拼接,此时在spring-data-jpa内部就会建立一个PartTreeJpaQuery实例;
4.NamedQuery
使用javax.persistence.NamedQuery注解访问数据库的形式,此时在spring-data-jpa内部就会根据此注解选择建立一个NamedQuery实例;
5.StoredProcedureJpaQuery
顾名思义,在Repository接口的方法头上使用org.springframework.data.jpa.repository.query.Procedure注解,也就是调用存储过程的方式访问数据库,此时在spring-data-jpa内部就会根据@Procedure注解而选择建立一个StoredProcedureJpaQuery实例。
那么问题来了,sql 拼接的时候怎么知道是根据userName
进行查询呢?是取自方法名中的 byUsername 仍是方法参数 userName 呢? spring 具体是在何时知道查询参数的呢 ?
spring 在启动的时候会实例化一个 Repositories,它会去扫描全部的 class,而后找出由咱们定义的、继承自org.springframework.data.repository.Repositor
的接口,而后遍历这些接口,针对每一个接口依次建立以下几个实例:
SimpleJpaRespositry
—— 用来进行默认的 DAO 操做,是全部 Repository 的默认实现JpaRepositoryFactoryBean
—— 装配 bean,装载了动态代理 Proxy,会以对应的 DAO 的 beanName 为 key 注册到DefaultListableBeanFactory
中,在须要被注入的时候从这个 bean 中取出对应的动态代理 Proxy 注入给 DAOJdkDynamicAopProxy
—— 动态代理对应的InvocationHandler
,负责拦截 DAO 接口的全部的方法调用,而后作相应处理,好比findByUsername
被调用的时候会先通过这个类的 invoke 方法在JpaRepositoryFactoryBean.getRepository()
方法被调用的过程当中,仍是在实例化QueryExecutorMethodInterceptor
这个拦截器的时候,spring 会去为咱们的方法建立一个PartTreeJpaQuery
,在它的构造方法中同时会实例化一个PartTree
对象。PartTree
定义了一系列的正则表达式,所有用于截取方法名,经过方法名来分解查询的条件,排序方式,查询结果等等,这个分解的步骤是在进程启动时加载 Bean 的过程当中进行的,当执行查询的时候直接取方法对应的PartTree
用来进行 sql 的拼装,而后进行 DB 的查询,返回结果。
到此为止,咱们整个JpaRepository
接口相关的链路就算走通啦,简单的总结以下:
spring 会在启动的时候扫描全部继承自 Repository 接口的 DAO 接口,而后为其实例化一个动态代理,同时根据它的方法名、参数等为其装配一系列DB操做组件,在须要注入的时候为对应的接口注入这个动态代理,在 DAO 方法被调用的时会走这个动态代理,而后通过一系列的方法拦截路由到最终的 DB 操做执行器JpaQueryExecution
,而后拼装 sql,执行相关操做,返回结果。
基本查询分为两种,一种是 spring data 默认已经实现(只要继承JpaRepository
),一种是根据查询的方法来自动解析成 SQL。
public interface UserRepository extends JpaRepository<User, Long> { } @Test public void testBaseQuery() throws Exception { User user=new User(); userRepository.findAll(); userRepository.findOne(1l); userRepository.save(user); userRepository.delete(user); userRepository.count(); userRepository.exists(1l); // ... }
自定义的简单查询就是根据方法名来自动生成SQL,主要的语法是findXXBy,readAXXBy,queryXXBy,countXXBy, getXXBy
后面跟属性名称,举几个例子:
User findByUserName(String userName); User findByUserNameOrEmail(String username, String email); Long deleteById(Long id); Long countByUserName(String userName); List<User> findByEmailLike(String email); User findByUserNameIgnoreCase(String userName); List<User> findByUserNameOrderByEmailDesc(String email);
具体的关键字,使用方法和生产成 SQL 以下表所示
Keyword | Sample | JPQL snippet |
---|---|---|
And | findByLastnameAndFirstname | … where x.lastname = ?1 and x.firstname = ?2 |
Or | findByLastnameOrFirstname | … where x.lastname = ?1 or x.firstname = ?2 |
Is,Equals | findByFirstnameIs,findByFirstnameEquals | … where x.firstname = ?1 |
Between | findByStartDateBetween | … where x.startDate between ?1 and ?2 |
LessThan | findByAgeLessThan | … where x.age < ?1 |
LessThanEqual | findByAgeLessThanEqual | … where x.age ⇐ ?1 |
GreaterThan | findByAgeGreaterThan | … where x.age > ?1 |
GreaterThanEqual | findByAgeGreaterThanEqual | … where x.age >= ?1 |
After | findByStartDateAfter | … where x.startDate > ?1 |
Before | findByStartDateBefore | … where x.startDate < ?1 |
IsNull | findByAgeIsNull | … where x.age is null |
IsNotNull,NotNull | findByAge(Is)NotNull | … where x.age not null |
Like | findByFirstnameLike | … where x.firstname like ?1 |
NotLike | findByFirstnameNotLike | … where x.firstname not like ?1 |
StartingWith | findByFirstnameStartingWith | … where x.firstname like ?1 (parameter bound with appended %) |
EndingWith | findByFirstnameEndingWith | … where x.firstname like ?1 (parameter bound with prepended %) |
Containing | findByFirstnameContaining | … where x.firstname like ?1 (parameter bound wrapped in %) |
OrderBy | findByAgeOrderByLastnameDesc | … where x.age = ?1 order by x.lastname desc |
Not | findByLastnameNot | … where x.lastname <> ?1 |
In | findByAgeIn(Collection<age> ages)</age> | … where x.age in ?1 |
NotIn | findByAgeNotIn(Collection<age> age)</age> | … where x.age not in ?1 |
TRUE | findByActiveTrue() | … where x.active = true |
FALSE | findByActiveFalse() | … where x.active = false |
IgnoreCase | findByFirstnameIgnoreCase | … where UPPER(x.firstame) = UPPER(?1) |
在实际的开发中咱们须要用到分页、删选、连表等查询的时候就须要特殊的方法或者自定义 SQL
分页查询在实际使用中很是广泛了,spring data jpa已经帮咱们实现了分页的功能,在查询的方法中,须要传入参数Pageable
,当查询中有多个参数的时候Pageable
建议作为最后一个参数传入。Pageable
是 spring 封装的分页实现类,使用的时候须要传入页数、每页条数和排序规则
Page<User> findALL(Pageable pageable); Page<User> findByUserName(String userName,Pageable pageable);
@Test
public void testPageQuery() throws Exception { int page=1,size=10; Sort sort = new Sort(Direction.DESC, "id"); Pageable pageable = new PageRequest(page, size, sort); userRepository.findALL(pageable); userRepository.findByUserName("testName", pageable); }
有时候咱们只须要查询前N个元素,或者支取前一个实体。
User findFirstByOrderByLastnameAsc(); User findTopByOrderByAgeDesc(); Page<User> queryFirst10ByLastname(String lastname, Pageable pageable); List<User> findFirst10ByLastname(String lastname, Sort sort); List<User> findTop10ByLastname(String lastname, Pageable pageable);
其实 Spring data 大部分的 SQL 均可以根据方法名定义的方式来实现,可是因为某些缘由咱们想使用自定义的 SQL 来查询,spring data 也是完美支持的;在 SQL 的查询方法上面使用 @Query 注解,如涉及到删除和修改在须要加上 @Modifying 。也能够根据须要添加 @Transactional 对事物的支持,查询超时的设置等
@Modifying @Query("update User u set u.userName = ?1 where c.id = ?2") int modifyByIdAndUserId(String userName, Long id); @Transactional @Modifying @Query("delete from User where id = ?1") void deleteByUserId(Long id); @Transactional(timeout = 10) @Query("select u from User u where u.emailAddress = ?1") User findByEmailAddress(String emailAddress);
多表查询在 spring data jpa 中有两种实现方式,第一种是利用 hibernate 的级联查询来实现,第二种是建立一个结果集的接口来接收连表查询后的结果,这里介绍第二种方式。
首先须要定义一个结果集的接口类。
public interface HotelSummary { City getCity(); String getName(); Double getAverageRating(); default Integer getAverageRatingRounded() { return getAverageRating() == null ? null : (int) Math.round(getAverageRating()); } }
查询的方法返回类型设置为新建立的接口
@Query("select h.city as city, h.name as name, avg(r.rating) as averageRating from Hotel h left outer join h.reviews r where h.city = ?1 group by h") Page<HotelSummary> findByCity(City city, Pageable pageable); @Query("select h.name as name, avg(r.rating) as averageRating from Hotel h left outer join h.reviews r group by h") Page<HotelSummary> findByCity(Pageable pageable);
Page<HotelSummary> hotels = this.hotelRepository.findByCity(new PageRequest(0, 10, Direction.ASC, "name")); for(HotelSummary summay:hotels){ System.out.println("Name" +summay.getName()); }
在运行中 Spring 会给接口(HotelSummary
)自动生产一个代理类来接收返回的结果,代码会使用 getXX 的形式来获取
spring data jpa 底层采用 hibernate 作为 ORM 框架,因此 spring data jpa 和 mybatis 的比较其实就是 hibernate 和 mybatis 的比较。下面从几个方面来对比下二者
从基本概念和框架目标上看,两个框架差异仍是很大的。hibernate 是一个自动化更强、更高级的框架,毕竟在java代码层面上,省去了绝大部分 sql 编写,取而代之的是用面向对象的方式操做关系型数据库的数据。而 MyBatis 则是一个可以灵活编写 sql 语句,并将 sql 的入参和查询结果映射成 POJOs 的一个持久层框架。因此,从表面上看,hibernate 能方便、自动化更强,而 MyBatis 在 Sql 语句编写方面则更灵活自由。
正如上面介绍的, Hibernate 比 MyBatis 抽象封装的程度更高,理论上单个语句之心的性能会低一点(全部的框架都是同样,排除算法上的差别,越是底层,执行效率越高)。
但 Hibernate 会设置缓存,对于重复查询有必定的优化,并且从编码效率来讲,Hibernate 的编码效果确定是会高一点的。因此,从总体的角度来看性能的话,其实二者不能彻底说谁胜谁劣。
Hibernate 是完备的 ORM 框架,是符合 JPA 规范的, MyBatis 没有按照JPA那套规范实现。目前 Spring 以及 Spring Boot 官方都没有针对 MyBatis 有具体的支持,但对 Hibernate 的集成一直是有的。但这并非说 mybatis 和 spring 没法集成,MyBatis 官方社区自身也是有 对 Spring,Spring boot 集成作支持的,因此在技术上,二者都不存在问题。
总结下 mybatis 的优势:
hibernate 的优势:
JPA 的宗旨是为 POJO 提供持久化标准规范,实现使用的 Hibernate,Hibernate 是一个全自动的持久层框架,而且提供了面向对象的 SQL 支持,不须要编写复杂的 SQL 语句,直接操做 Java 对象便可,从而大大下降了代码量,让即便不懂 SQL 的开发人员,也使程序员更加专一于业务逻辑的实现。对于关联查询,也仅仅是使用一些注解便可完成一些复杂的 SQL功能。
最后再作一个简单的总结: