Spring Data JPA 学习笔记

时间:2022-05-15 15:13:51
Spring Data    
一.概述
Spring 的一个子项目。用于简化数据库访问,支持NoSQL 和 关系数据存储。其主要目标是使数据库的访问变得方便快捷。SpringData 项目所支持 
NoSQL 存储:MongoDB (文档数据库)
                      Neo4j(图形数据库)    
                      Redis(键/值存储)
                      Hbase(列族数据库)
SpringData 项目所支持的关系数据存储技术:JDBC/JPA
    JPA Spring Data : 致力于减少数据访问层 (DAO) 的开发量. 开发者唯一要做的,就 只是声明持久层的接口,其他都交给 Spring Data JPA 来帮你完成!框架怎么可能代替开发者实现业务逻辑呢?比如:当有一个                        UserDao.findUserById()  这样一个方法声明,大致应该能判断出这是根据给定条件的 ID 查询出满足条件的 User  对象。Spring Data JPA 做的便是规范方法的名字,根据符合规范的名字来确定方法需要实现什么样的逻辑。

二.所需jar包
    

三.配置文件
    
四.Entity
    
    表:
        person(通过SpringData自动生成的表会自动生成主键和外键)                      address
          
五.Repository
    
     5.1 如何声明一个Repository
     Repository 接口是 Spring Data 的一个核心接口,它不提供任何方法,开发者需要在自己定义的接口中声明需要的方法     public interface Repository<T, ID extends Serializable> { } 第一个泛型为        model的class
     第二个为id的class Spring Data可以让我们只定义接口,只要遵循 Spring Data的规范,就无需写实现类。  与继承 Repository 等价的一种方式,就是在持久层接口上使用 @RepositoryDefinition 注        解,并为其指定 domainClass 和 idClass 属性。如下两种方式是完全等价的
      也可集成repository的子接口

     5.2 Repository查询方法的关键字
        
    5.3 查询方法解析流程
            假如创建如下的查询:findByUserDepUuid(),框架在解析该方法时,首先剔除 findBy,然后对剩下的属性进行解析,假设查询实体为Doc
  1. 先判断 userDepUuid (根据 POJO 规范,首字母变为小写)是否为查询实体的一个属性,如果是,则表示根据该属性进行查询;如果没有该属性,继续第二步    
  2. 从右往左截取第一个大写字母开头的字符串(此处为Uuid),然后检查剩下的字符串是否为查询实体的一个属性,如果是,则表示根据该属性进行查询;如果没有该属性,则重复第二步,继续从右往左截取;最后假设 user 为查询实体的一个属性;       
  3. 接着处理剩下部分(DepUuid),先判断 user 所对应的类型是否有depUuid属性,如果有,则表示该方法最终是根据 “ Doc.user.depUuid” 的取值进行查询;否则继续按照步骤 2 的规则从右往左截取,最终表示根据 “Doc.user.dep.uuid” 的值进行查询。
  4. 可能会存在一种特殊情况,比如 Doc包含一个 user 的属性,也有一个 userDep 属性,此时会存在混淆。可以明确在属性之间加上 "_" 以显式表达意图,比如 "findByUser_DepUuid()" 或者 "findByUserDep_uuid()"
  5. 特殊的参数: 还可以直接在方法的参数上加入分页或排序的参数,  比如:Page<UserModel> findByName(String name, Pageable pageable);  List<UserModel> findByName(String name, Sort sort);     
     5.4 查询@Query注解
            这种查询可以声明在 Repository 方法中,摆脱像命名查询那样的约束,将查询直接在相应的接口方法中声明,结构更为清晰,这是 Spring data 的特有实现。
            
         
        占位符绑定参数:
  •     索引参数如下所示,索引值从1开始,查询中 ”?X” 个数需要与方法定义的参数个数相一致,并且顺序也要一致  
  •     命名参数(推荐使用这种方式):可以定义好参数名,赋值时采用@Param("参数名"),而不用管顺序。 
                             
  • 如果是 @Query 中有 LIKE 关键字,后面的参数需要前面或者后面加 %,这样在传递参数值的时候就可以不加 %:  
                         @Query("select o from UserModel o where o.name like ?1%")     public List<UserModel> findByUuidOrAge(String name);
                        @Query("select o from UserModel o where o.name like %?1")    public List<UserModel> findByUuidOrAge(String name);
                        @Query("select o from UserModel o where o.name like %?1%")    public List<UserModel> findByUuidOrAge(String name);
  • @Query 本地查询 
                        设置nativeQuery为true,
                        比如:@Query(value="select * from tbl_user where name like %?1" ,nativeQuery=true)    public List<UserModel>                 findByUuidOrAge(String name);  
            
        5.5@Modifying注解和事务
  • @Query 与 @Modifying 这两个 annotation一起声明,可定义个性化更新操作,例如只涉及某些字段更新时最为常用,示例如下:                    
                注意:
                        1.方法的返回值应该是 int,表示更新语句所影响的行数在调用的地方必须加事务,
                        2.可以通过自定义的 JPQL 完成 UPDATE 和 DELETE 操作. 注意: JPQL 不支持使用 INSERT
                        3.在 @Query 注解中编写 JPQL 语句, 但必须使用 @Modifying 进行修饰. 以通知 SpringData, 这是一个 UPDATE 或 DELETE 操作
                        4.UPDATE 或 DELETE 操作需要使用事务, 此时需要定义 Service 层. 在 Service 层的方法上添加事务操作.
                        5.默认情况下, SpringData 的每个方法上有事务, 但都是一个只读事务. 他们不能完成修改操作!
                        
                        没有事务不能正常执行 
                        会报:
                        org.springframework.dao.InvalidDataAccessApiUsageException: Executing an update/delete query; nested exception is javax.persistence.TransactionRequiredException: Executing an                                                       
update/delete query
 
六.SpringData的事务
  • Spring Data 提供了默认的事务处理方式,即 所有的查询均声明为只读事务
  • 对于自定义的方法,如需改变 Spring Data 提供的事务默认方式,可以在方法上注解 @Transactional 声明 
  • 进行多个 Repository 操作时,也应该使它们在同一个事务中处理,按照分层架构的思想,这部分属于业务逻辑层,因此, 需要在 Service 层实现对多个 Repository 的调用,并在相应的方法上声明事务。

七.Repository子类 以及其他类
        
        7.1 CrudRepository 接口
            自带一些方法 
                保存/删除(单个与集合)
                根据id查询 查询所有 查询条数 是否存在该id....
            
            注:保存时id可以不填写 如果id有注解( @Id  @GeneratedValue )
     
        7.2PagingAndSortingRepository接口
             实现了分页和排序的功能
             是  CrudRepository 接口的子接口  
            
            
            分页使用:  PageRequest 类即可
            测试代码:  
                分页       
@Test
public void getByPage (){
//pageNo 0 开始 .
int pageNo = 6 - 1 ;
int pageSize = 5 ;
//Pageable 接口通常使用的其 PageRequest 实现类 . 其中封装了需要分页的信息
// 排序相关的 . Sort 封装了排序的信息
//Order 是具体针对于某一个属性进行升序还是降序 .
PageRequest pageable = new PageRequest(pageNo , pageSize) ;
Page<Person> page = personRepository .findAll(pageable) ;

System. out .println( " 总记录数 : " + page.getTotalElements()) ;
System. out .println( " 当前第几页 : " + (page.getNumber() + 1 )) ;
System. out .println( " 总页数 : " + page.getTotalPages()) ;
System. out .println( " 当前页面的 List: " + page.getContent()) ;
System. out .println( " 当前页面的记录数 : " + page.getNumberOfElements()) ;
}

               
      分页+排序
@Test
public void getByPageAndOrder (){
//pageNo 0 开始 .
int pageNo = 6 - 1 ;
int pageSize = 5 ;
//Pageable 接口通常使用的其 PageRequest 实现类 . 其中封装了需要分页的信息
// 排序相关的 . Sort 封装了排序的信息
//Order 是具体针对于某一个属性进行升序还是降序 .
Order order1 = new Order(Direction. DESC , "id" ) ;
Order order2 = new Order(Direction. ASC , "email" ) ;
Sort sort = new Sort(order1 , order2) ;

PageRequest pageable = new PageRequest(pageNo , pageSize , sort) ;
Page<Person> page = personRepository .findAll(pageable) ;

System. out .println( " 总记录数 : " + page.getTotalElements()) ;
System. out .println( " 当前第几页 : " + (page.getNumber() + 1 )) ;
System. out .println( " 总页数 : " + page.getTotalPages()) ;
System. out .println( " 当前页面的 List: " + page.getContent()) ;
System. out .println( " 当前页面的记录数 : " + page.getNumberOfElements()) ;
}

7.3JpaRepository接口
    属于PagingAndSortingRepository接口的子类

该接口提供了JPA的相关功能 
List<T> findAll(); //查找所有实体 
List<T> findAll(Sort sort); //排序、查找所有实体 
List<T> save(Iterable<? extends T> entities);//保存集合 
void flush();//执行缓存与数据库同步 T saveAndFlush(T entity);//强制执行持久化 
void deleteInBatch(Iterable<T> entities);//删除一个实体集合
@Test
public void testJpaRepository (){
Person person = new Person() ;
person.setBirth( new Date()) ;
person.setEmail( " xy@atguigu.com " ) ;
person.setLastName( "xyz" ) ;
person.setId( 28 ) ;

Person person2 = personRepository .saveAndFlush(person) ;

System. out .println(person == person2) ;
}

7.4JpaSpecificationExecutor接口
      
        只需要一个泛型
        JpaSpecificationExecutor<Person>
      

        
        不属于Repository体系,实现一组 JPA Criteria 查询相关的方法 

Specification:封装  JPA Criteria 查询条件。通常使用匿名内部类的方式来创建该接口的对象
代码示例:
/**
* 目标 : 实现带查询条件的分页 . id > 9145071 的条件
*
* 调用 JpaSpecificationExecutor Page <T> findAll(Specification <T> spec, Pageable pageable);
* Specification: 封装了 JPA Criteria 查询的查询条件
* Pageable: 封装了请求分页的信息 : 例如 pageNo, pageSize, Sort
*/
@Test
public void testJpaSpecificationExecutor (){
int pageNo = 3 - 1 ;
int pageSize = 5 ;
PageRequest pageable = new PageRequest(pageNo , pageSize) ;

// 通常使用 Specification 的匿名内部类
Specification<Person> specification = new Specification<Person>() {
/**
* @param *root: 代表查询的实体类 .
* @param query : 可以从中可到 Root 对象 , 即告知 JPA Criteria 查询要查询哪一个实体类 . 还可以
* 来添加查询条件 , 还可以结合 EntityManager 对象得到最终查询的 TypedQuery 对象 .
* @param *cb: CriteriaBuilder 对象 . 用于创建 Criteria 相关对象的工厂 . 当然可以从中获取到 Predicate 对象
* @return: *Predicate 类型 , 代表一个查询条件 .
*/
@Override
public Predicate toPredicate (Root<Person> root ,
CriteriaQuery<?> query , CriteriaBuilder cb) {
Path path = root.get( "id" ) ;
//gt表示大于
Predicate predicate = cb.gt(path , 9145071 ) ;
return predicate ;
}
} ;

Page<Person> page = personRepository .findAll(specification , pageable) ;

System. out .println( " 总记录数 : " + page.getTotalElements()) ;
System. out .println( " 当前第几页 : " + (page.getNumber() + 1 )) ;
System. out .println( " 总页数 : " + page.getTotalPages()) ;
System. out .println( " 当前页面的 List: " + page.getContent()) ;
System. out .println( " 当前页面的记录数 : " + page.getNumberOfElements()) ;
}
        
7.5自定义Repository
   ①为某一个 Repository 上添加自定义方法 实现步骤
  1. 创建一个实现类名字为自定义Repository名字+Impl(强制,但可以通过配置修改)
  2. 实现类与接口中定义相同名称参数返回值的方法(一般为了强制约束一致 会新建一个新的接口并且实体类实现这个接口 原Repository继承这个接口)  
               

    类图关系如下:
                

                                    实际上在使用 PersonRepository 的 test 方法时,
                                    会调用 PersonRepositoryImpl 中 test 方法的实现
    
// 测试自定义 Repository
@Test
public void testMyRepository (){
System. out .println( personRepository .test( 9145069 )) ;
}
   
 ②为所有的 Repository 都添加自实现的方法
    
  1. 创建一个 自定义的扩展接口CommonMethodTest
  2. 创建一个扩展接口的实现类CommonMethodTestImpl
  3. 创建一个 MyRepositoryFactory 与FactoryBean 这里建在一起放到CommonJpaRepositoryFactoryBean中
  4. 修改配置文件

< jpa :repositories base-package ="com.jw.myRepository"
entity-manager-factory-ref ="entityManagerFactory"
factory-class ="com.jw.myRepository.CommonJpaRepositoryFactoryBean" ></ jpa :repositories>

                5.新创建的Repository继承自定义的 CommonMethodTest接口即可
       
            注意事项:
                @NoRepositoryBean一定要有的,还有全局的扩展实现类不要用Imp作为后缀名,不然会报异常的(目前还没搞清楚报异常的具体原因,个人猜测可能是和局部的扩展有冲突吧)


八.其他

    8.1本文代码打包
        
    
    8.2  推荐其他学习资料
             官方文档
    
    8.3 补充
           ①JPA拿到EntityManage
@PersistenceContext
private EntityManager entityManager ;
         
            ②Entity 中忽略属性或者级联列名 等等注解  都应该加在属性的get方法上

             ③ SPEL表达式(使用


   '#{#entityName}'值为'Book'对象对应的数据表名称(book)。

public interface BookQueryRepositoryExample extends Repository<Book, Long>{

       @Query(value = "select * from #{#entityName} b where b.name=?1", nativeQuery = true)
       List<Book> findByName(String name);


 ④ 用Reporsity 拿到的 非实体类对象 对象将是object[]   唯一数值将是 将会返回BigDecimal

⑤IN 查询可以传一个List<Long>