• Spring Data JPA方法名命名规则


    最近巩固一下JPA,网上看到这些资料,这里记录巩固一下。

    一、Spring Data Jpa方法定义的规则

    简单条件查询

            简单条件查询:查询某一个实体类或者集合。
            按照Spring Data的规范的规定,查询方法以find | read | get开头(比如 find、findBy、read、readBy、get、getBy),涉及查询条件时,条件的属性用条件关键字连接,要注意的是:条件属性以首字母大写。框架在进行方法名解析时,会先把方法名多余的前缀截取掉,然后对剩下部分进行解析。
            直接在接口中定义查询方法,如果是符合规范的,可以不用写实现,即不用写SQL,目前支持的关键字写法如下:

    二、只有查询参数

    定义一个Entity实体类:

    1. class People{
    2. private String firstName;
    3. private String lastName;
    4. }

    以上使用and条件查询时,应这样写

    findByLastNameAndFirstName(StringlastName,String firstName); 
    

    注意:条件的属性名称与个数要与参数的位置与个数一一对应

    三、查询同时排序

            如果方法的最后一个参数是 Sort 或者 Pageable 类型,也会提取相关的信息,以便按规则进行排序或者分页查询比如

    1. Page findByName(String name, Pageable pageable);
    2. List findByName(String name, Sort sort);

    排序也可以这么写

    1. List<Cus> findBySexOrderByName(String sex); //名称正序(正序时,推荐此方式,简单)
    2. List<Cus> findBySexOrderByNameAsc(String sex); //名称正序(效果同上)
    3. List<Cus> findBySexOrderByNameDesc(String sex); //名称倒序

    四、查询参数是 对象里面的对象的属性

    如,查询参数是ERole.EMemberAccount.memberCode 一般我们会这么写

    1. @Query("select e from ERole e where e.memberAccount.memberCode = ?1")
    2. List findByMerchantCode(String merchantCode);

    如果用命名规则可以这么写

    List<ERole> findByMemberAccountMemberCode(String merchantCode);
    

    查询方法解析流程

            假如我们创建如下的查询:findByUserDepUuid(),框架在解析该方法时,首先剔除findBy,然后对剩下的属性进行解析,假设查询实体为Doc。

    1. 先判断userDepUuid (根据POJO(Plain Ordinary Java Object简单java对象,实际就是普通java bean)规范,首字母变为小写。)是否是查询实体的一个属性,如果根据该属性进行查询;如果没有该属性,继续第二步。

    2. 从右往左截取第一个大写字母开头的字符串(此处为Uuid),然后检查剩下的字符串是否为查询实体的一个属性,如果是,则表示根据该属性进行查询;如果没有该属性,则重复第二步,继续从右往左截取;最后假设 user为查询实体的一个属性。

    3. 接着处理剩下部分(DepUuid),先判断user所对应的类型是否有depUuid属性,如果有,则表示该方法最终是根据 “Doc.user.depUuid” 的取值进行查询;否则继续按照步骤 2的规则从右往左截取,最终表示根据“Doc.user.dep.uuid” 的值进行查询。

    4. 可能会存在一种特殊情况,比如 Doc包含一个user的属性,也有一个 userDep 属性,此时会存在混淆。可以明确在属性之间加上 "_"以显式表达意图,比如"findByUser_DepUuid()" 或者"findByUserDep_uuid()"。

    五、命名规则不适合动态查询

    当查询条件为null时,如

    • 实体定义:对于一个客户实体Cus,包含有name和sex,均是String类型。
    • 查询方法定义:List findByNameAndSex(String name,String sex);
    • 使用时:dao.findByNameAndSex(null, "男");
    • 后台生成sql片断:where (cus0_.name is null) and cus0_.sex=?

            结论:当查询时传值是null时,数据库中只有该字段是null的记录才符合条件,并不是说忽略这个条件。也就是说,这种查询方式,只适合于明确查询条件必须传的业务,对于动态查询(条件多少是动态的,例如一般的查询列表,由最终用户使用时决定输入那些查询条件),这种简单查询是不能满足要求的。

    强调:需要强调的是,命名规则也不适合delete insert update. 只适合简单查询

     

    六、Spring Data JPA对多表查询的选择

    答案是肯定的,直接上结论。

    方案说明自定义接收对象SQL分页多表联合问题
    方案一@QueryJPQL:DTO或投影原生SQL:Object[],map(2个字段时)JPQL或原生SQLJpaRepository实体上配不配关系都可以1.查询条件要嵌入SQL语句内,一些复杂的情形不好处理,例如某个字段模糊检索,字段是动态的;2.分页查询countQuery把查询语句重复了一遍
    方案二Specification不支持,只能返回对应PO无SQL结合JpaRepository需要在实体上配置关系,如@OneToOne,否则无法实现左连接,只能联合查询1.实体需要配置连接关系2.每一个关联对象都是单独的数据库查询
    方案三EntityManager不支持投影,其他同@QueryJPQL或原生SQL自己封装实体上配不配关系都可以相比于@Query好处是,JPQL字符串可以动态拼接,可以处理一些复杂的查询情形。但是分页需要自己封装。
    方案四CriteriaQueryDTO无SQL自己封装需要在实体上配置关系,如@OneToOne否则无法实现左连接,只能联合查询同Specification,且分页需要自己封装
    终极方案QueryDSLDTOTuple无SQL支持实体上配不配关系都可以解决以上所有问题

    选择一个好的解决方案,需要考虑如下几个方面:

    • 能够自定义对象接收查询结果集;
    • 能够支持复杂的、动态的查询条件;
    • 既然使用JPA,当然最好能够用类型安全的查询方式,并且使用起来比较自然;
    • 能够原生支持分页查询;
    • 能够支持left join,并且对实体定义没有约束。

    上表就是从这几个方面进行分析,最后QueryDSL堪称完美,下面详细介绍几种方案。

    示例代码:GitHub - zhongpan/jpa-demo

    七、JPA、Hibernate、Spring Data JPA三者的关系

            先了解下JPA、Hibernate、Spring Data JPA三者的关系是什么?

            JPA是一个接口规范,随着Java EE 5发布,也是EJB3.0的一部分。Hibernate是先于JPA出现的一种历史悠久的ORM框架,它实现了JPA,也是目前用的最多的实现。而Sprint Data JPA是Spring中提供的开箱即用的基于JPA的数据库访问框架,其采用的实现正是Hibernate。Spring Data JPA提供的数据库访问能力如下:

    img

    从上述接口的名字就可以看出:

    • CrudRepository:最基本的增删改查操作
    • PagingAndSortingRepository:分页查询
    • QueryByExampleExecutor:基于样本查询,避免了传一堆参数,还要判断是否null
    • JpaSpecificationExecutor:基于Specification查询,就是对CriteriaQuery的封装,类型安全的查询方式
    • QuerydslPredicateExecutor:基于QueryDSL的查询,也是类型安全的

            上述接口的实现在SimpleJpaRepository和QuerydslJpaPredicateExecutor中,其中就是基于JPA的EntiryManager接口进行封装。如果我们要重写实现,也是通过EntiryManager来完成。

    方案一

            首先想到的方法自然是使用@Query注解,直接使用JPQL进行联合查询,自定义接收对象,left join都不在话下。主要的问题是对于一些复杂的、动态的查询条件不好处理,另外分页的countQuery不得不把主查询重写一遍,有点烦人。

     
    @Repository
    public interface VmhostDao extends JpaRepository {
    
      // 方法一:使用@Query注解,可以自定义接收对象
      // 问题:查询条件要嵌入SQL语句内,一些复杂的情形不好处理,例如某个字段模糊检索,字段是动态的;分页查询countQuery把查询语句重复了一遍
    
      @Query("select new com.example.demo.entity.VmhostDTO(v, u, t) from VmhostPO v left join AuthUserPO u on v.userid = u.id left join AuthTenantPO t on v.tenantid = t.id where v.name like %?1%")
      List findVmhost(String name);
    
      @Query("select new com.example.demo.entity.VmhostInfoDTO(v.id, v.name, u.username, t.name) from VmhostPO v left join AuthUserPO u on v.userid = u.id left join AuthTenantPO t on v.tenantid = t.id where v.name like %:name%")
      List findVmhostInfo(String name);
    
      @Query("select v.id as id, v.name as name, u.username as userName, t.name as tname from VmhostPO v left join AuthUserPO u on v.userid = u.id left join AuthTenantPO t on v.tenantid = t.id")
      List findVmhostInfoByProjection();
    
      @Query(value = "select new com.example.demo.entity.VmhostInfoDTO(v.id, v.name, u.username, t.name) from VmhostPO v left join AuthUserPO u on v.userid = u.id left join AuthTenantPO t on v.tenantid = t.id where v.name like %:name%", 
      countQuery = "select count(*) from VmhostPO v left join AuthUserPO u on v.userid = u.id left join AuthTenantPO t on v.tenantid = t.id where v.name like %:name%")
      Page findVmhostInfoByPage(String name, Pageable pageable);
    
    }
    

    方案二

            那么SQL的组织能否动态编程控制呢,自然会想到Specification查询,查询条件可以通过CriteriaQuery动态拼装。这也是Spring Data JPA中用的最广泛的查询方式。

            但是这种方式存在一些限制,首先不能灵活自定义接收对象,只能返回PO,其次要想实现left join,必须在实体上定义关系,最后关联对象不是一次查询回来的,而是单独的查询。

    public interface VmhostSpecWithRelationDao
        extends JpaRepository, JpaSpecificationExecutor {
    
      // 方案二:使用Specification查询
      // 问题:实体必须配置关系,否则无法左连接;每个关联对象是单独数据库查询
    }
    
    
    
    package com.example.demo.service;
    
    @Service
    public class VmhostService {
    
      public List listVmhostSpecWithRelation(String name) {
        Specification spec = (root, cq, cb) -> {
          root.join("user", JoinType.LEFT);
          root.join("tenant", JoinType.LEFT);
          return cb.like(root.get("name"), "%" + name + "%");
        };
        List list = vmhostSpecWithRelationDao.findAll(spec);
        return list;
      }
    
    }
    

    可能大家有两点疑问:

    • @Query和Specification能否混用,@Query定义select的结果,Specification定义查询条件

    答案:不行,总是@Query有效,你定义的Specification参数压根就不会理会

     
    // JpaSpecificationExecutor的参数和JpaRepository不一样,没啥用,SimpleJpaRepository总是用的JpaRepository的参数
    public interface VmhostSpecDao extends JpaRepository, JpaSpecificationExecutor {
    
      // 方案二:@Query和Specification是不能混用的,也无法改变接收结果集对象
    
      // 无法混用,总是query有效,spec参数压根就不会理会
      @Query("from VmhostPO")
      List findVmhost(Specification spec);
    
      // 覆盖JpaSpecificationExecutor的方法可以吗?一样的,根本不会走到findAll的默认实现 
      @Override
      @Query("select new com.example.demo.entity.VmhostInfoDTO(v.id, v.name, u.username, t.name) from VmhostPO v left join AuthUserPO u on v.userid = u.id left join AuthTenantPO t on v.tenantid = t.id")
      List findAll(Specification spec);
    }
    
    • Specification控制接收结果集对象

    答案:对不起,Specification的toPredicate中执行select是无效的,里面只能返回查询条件。

     
      // 这样写没有用,生成如下sql
      // select vmhostpo0_.id as id1_2_, vmhostpo0_.addresses as addresse2_2_, vmhostpo0_.availablezone as availabl3_2_, vmhostpo0_.baremetal as baremeta4_2_, vmhostpo0_.cpucore as cpucore5_2_, vmhostpo0_.createtime as createti6_2_, vmhostpo0_.disksize as disksize7_2_, vmhostpo0_.floatip as floatip8_2_, vmhostpo0_.hostname as hostname9_2_, vmhostpo0_.locked as locked10_2_, vmhostpo0_.metadata as metadat11_2_, vmhostpo0_.name as name12_2_, vmhostpo0_.privatenetworkid as private13_2_, vmhostpo0_.ramsize as ramsize14_2_, vmhostpo0_.tenantid as tenanti15_2_, vmhostpo0_.tenantname as tenantn16_2_, vmhostpo0_.type as type17_2_, vmhostpo0_.userid as userid18_2_, vmhostpo0_.username as usernam19_2_, vmhostpo0_.vmstatus as vmstatu20_2_ from vmhost vmhostpo0_ cross join auth_user authuserpo1_ cross join auth_tenant authtenant2_ where vmhostpo0_.userid=authuserpo1_.id and vmhostpo0_.tenantid=authtenant2_.id and (vmhostpo0_.name like ?)
    public Optional listVmhostSpec(String name) {
      Specification spec = (root, cq, cb) -> {
        // 只能cross join,要left join需要在实体上建立关系
        Root user = cq.from(AuthUserPO.class);
        Root tenant = cq.from(AuthTenantPO.class);
        // 这里执行select没有用,这个函数只能返回查询条件,外层会覆盖select
        cq.multiselect(root.get("id"), root.get("name"), user.get("username"), tenant.get("name"));
        return cb.and(cb.equal(root.get("userid"), user.get("id")), cb.equal(root.get("tenantid"), tenant.get("id")),
            cb.like(root.get("name"), "%" + name + "%"));
    
      };
      return vmhostSpecDao.findOne(spec);
    }
    

    因为SimpleJpaRepository的实现已经固定了select,跟JpaRepository的类型参数相关,跟JpaSpecificationExecutor的类型参数无关

     
    protected  TypedQuery getQuery(@Nullable Specification spec, Class domainClass, Sort sort) {
    
        CriteriaBuilder builder = em.getCriteriaBuilder();
        CriteriaQuery query = builder.createQuery(domainClass);
    
        Root root = applySpecificationToCriteria(spec, domainClass, query);
        query.select(root);
    
        if (sort.isSorted()) {
            query.orderBy(toOrders(sort, root, builder));
        }
    
        return applyRepositoryMethodMetadata(em.createQuery(query));
    }
    

    方案三

    上面两种使用方法是Spring Data JPA用的最多的,接下来只能从底层入手,直接使用EntiryManager。这种方法完全靠自己,所有接口都需要自己实现,丧失了Spring Data JPA的便利性。

     
    @Repository
    public class VmhostEMDao {
    
      @Autowired
      @PersistenceContext
      private EntityManager entityManager;
    
      // 方案三:使用原生的entityManager,解决@Query的SQL无法动态拼接问题
      // 此时分页就需要自己封装了,也没有了JPA自动实现的接口
      // 注意这里like后面要引号
    
      @SuppressWarnings("unchecked")
      public List findVmhost(String name) {
        List list = entityManager.createQuery(
            "select new com.example.demo.entity.VmhostDTO(v, u, t) from VmhostPO v left join AuthUserPO u on v.userid = u.id left join AuthTenantPO t on v.tenantid = t.id where v.name like '%"
                + name + "%'")
            .getResultList();
        return list;
      }
    
      @SuppressWarnings("unchecked")
      public List findVmhostInfoByProjection() {
        // 此时总是Object[],不支持投影
        List list = entityManager.createQuery(
            "select v.id as id, v.name as name, u.username as userName, t.name as tname from VmhostPO v left join AuthUserPO u on v.userid = u.id left join AuthTenantPO t on v.tenantid = t.id")
            .getResultList();
        return list;
      }
    
    }
    

    方案四

    类似于方案二之于方案一,我们也可以使用类型安全的查询方式CriteraQuery。

     
    @Repository
    public class VmhostCQDao {
    
      @Autowired
      @PersistenceContext
      private EntityManager entityManager;
    
      // 方案四:相对于方案三,使用了类型安全的CriteriaQuery,其实Specification也是用的CriteriaQuery,所以存在和Specification一样的限制,但是可以控制select了,比Specification灵活一点
    
      public List findVmhost(String name) {
        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
        CriteriaQuery query = builder.createQuery(VmhostDTO.class);
        // 实体上没有配置关系,无法使用left join,只能联合查询(inner join)
        Root root = query.from(VmhostPO.class);
        Root rootUser = query.from(AuthUserPO.class);
        Root rootTenant = query.from(AuthTenantPO.class);
        query.multiselect(root, rootUser, rootTenant).where(builder.equal(root.get("userid"), rootUser.get("id")),
            builder.equal(root.get("tenantid"), rootTenant.get("id")), builder.like(root.get("name"), "%" + name + "%"));
    
        List list = entityManager.createQuery(query).getResultList();
        return list;
      }
    
    }
    

    终极方案

            到了终极方案了,Spring Data JPA集成了对QueryDSL的支持,官方参考见:http://www.querydsl.com/static/querydsl/latest/reference/html_single

            是不是有点像方案二+方案四,单表的时候直接使用JpaRepository和QuerydslPredicateExecutor提供的默认实现。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    package com.example.demo.dao;
    
    import com.example.demo.entity.VmhostPO;
    
    import org.springframework.data.jpa.repository.JpaRepository;
    import org.springframework.data.querydsl.QuerydslPredicateExecutor;
    
    public interface VmhostQDSLDao
        extends JpaRepository, QuerydslPredicateExecutor, VmhostRepository {
    
      // 方案五:VmhostRepository使用原生的entityManager配合QueryDSL,完美解决所有问题
      // 对于单表也可以使用QuerydslPredicateExecutor,自动拥有默认实现
    
    }
    

            多表的时候就基于EntityManager扩展,但是querydsl已经帮我们做了很多工作,不是从头开始。querydsl的书写方式相对于CriteriaQuery也更加自然,易于理解。

    1
    2
    3
    4
    5
    6
    7
    
    @NoRepositoryBean
    public class BaseRepository {
    
      @PersistenceContext
      protected EntityManager em;
    
    }
    
    1
    2
    3
    4
    5
    6
    7
    
    public interface VmhostRepository {
    
      public List findVmhost(Predicate predicate);
    
      public QueryResults findVmhostByPage(Predicate predicate, Pageable pageable);
    
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    
    @Repository
    public class VmhostRepositoryImpl extends BaseRepository implements VmhostRepository {
    
      // 多表左连接
    
      @Override
      public List findVmhost(Predicate predicate) {
        JPAQueryFactory queryFactory = new JPAQueryFactory(em);
        JPAQuery jpaQuery = queryFactory
            .select(Projections.constructor(VmhostDTO.class, QVmhostPO.vmhostPO, QAuthUserPO.authUserPO,
                QAuthTenantPO.authTenantPO))
            .from(QVmhostPO.vmhostPO).leftJoin(QAuthUserPO.authUserPO)
            .on(QVmhostPO.vmhostPO.userid.stringValue().eq(QAuthUserPO.authUserPO.id.stringValue()))
            .leftJoin(QAuthTenantPO.authTenantPO)
            .on(QVmhostPO.vmhostPO.tenantid.stringValue().eq(QAuthTenantPO.authTenantPO.id.stringValue()));
        jpaQuery.where(predicate);
        return jpaQuery.fetch();
      }
    
      @Override
      public QueryResults findVmhostByPage(Predicate predicate, Pageable pageable) {
        JPAQueryFactory queryFactory = new JPAQueryFactory(em);
        JPAQuery jpaQuery = queryFactory
            .select(Projections.constructor(VmhostDTO.class, QVmhostPO.vmhostPO, QAuthUserPO.authUserPO,
                QAuthTenantPO.authTenantPO))
            .from(QVmhostPO.vmhostPO).leftJoin(QAuthUserPO.authUserPO)
            .on(QVmhostPO.vmhostPO.userid.stringValue().eq(QAuthUserPO.authUserPO.id.stringValue()))
            .leftJoin(QAuthTenantPO.authTenantPO)
            .on(QVmhostPO.vmhostPO.tenantid.stringValue().eq(QAuthTenantPO.authTenantPO.id.stringValue()))
            .offset(pageable.getOffset()).limit(pageable.getPageSize());
        jpaQuery.where(predicate);
        return jpaQuery.fetchResults();
      }
    
    }
    

    总结

            以上方法都还是在Spring Data JPA框架之内,如果你愿意,你也可以去重写SimpleJpaRepository,重写了注意通过如下注解启用。

    1
    
    @EnableJpaRepositories(repositoryBaseClass = XXXXXX.class)
    

            其实QueryDSL已经做了很好的封装,完全没有必要重复造轮子,Spring Data JPA也提供了很多扩展点,在保留其便利性的基础上,根据需要去扩展,不需要全部推倒重来。

    参考:

    Spring Data JPA方法名命名规则 - 简书

    Spring Data JPA中多表联合查询最佳实践 | 钟潘的博客

  • 相关阅读:
    MMEngine理解
    解决:AttributeError: ‘WebDriver‘ object has no attribute ‘find_element_by_id‘
    LeetCode112.路径总和(C++描述,递归解法)
    Android面试题——高级开发面试题二
    Canvas 指纹追踪技术
    IDEA配置Maven
    使用百度云服务器申请ssl证书配置报错问题
    jmeter提取request body中的数据,作为下个接口的入参
    LPA-star算法(Lifelong Planning)及相关思考
    Typescript面向对象(接口、类、多态、重写、抽象类、访问修饰符)
  • 原文地址:https://blog.csdn.net/yangyangye/article/details/134372292