西宫硝子|

西宫结弦

园龄:3年2个月粉丝:1关注:1

SpringDataJPA从入门到精通

SpringDataJPA

一、整体认识 JPA

image

1. 市场上 orm框架对比

1.1 MyBatis

  • mybatis本身是 apache的一个开源项目 ibatis,2010年这个项目由 apache software foundation迁移到了 google code。并且改名为 mybatis。mybatis着力于 pojo于sql之间的映射关系,可以进行更为细致的 sql,使用起来十分灵活,上手简单,容易掌握,所以深受开发者的喜欢,目前市场占有率最高,比较适合互联网应用公司的 api场景。

1.2 Hibernate

  • Hibernate是一个开放源代码的对象关系映射框架,对JDBC进行了非常轻量级的对象封装,使得Java程序员可以随心所欲地使用对象编程思维来操纵数据库,并且对象有自己的生命周期,着力对象与对象之间的关系,有自己的HQL查询语言,所以数据库移植性很好。 Hibernate是完备的ORM框架,是符合JPA规范的。Hibernate有自己的缓存机制。从上手的角度来说比较难,比较适合企业级的应用系统开发。

1.3 Spring Data JPA

  • Hibernate是一个开放源代码的对象关系映射框架,对JDBC进行了非常轻量级的对象封装,使得Java程序员可以随心所欲地使用对象编程思维来操纵数据库,并且对象有自己的生命周期,着力对象与对象之间的关系,有自己的HQL查询语言,所以数据库移植性很好。 Hibernate是完备的ORM框架,是符合JPA规范的。Hibernate有自己的缓存机制。从上手的角度来说比较难,比较适合企业级的应用系统开发。

2. jap的介绍以及开源实现

  • JPA是Java Persistence API的简称,中文名为Java持久层API,是JDK 5.0注解或XML描述对象-关系表的映射关系,并将运行期的实体对象持久化到数据库中。
  • Sun引入新的JPA ORM规范出于两个原因:其一,简化现有Java EE和Java SE应用开发工作;其二,Sun希望整合ORM技术,实现天下归一。

2.1 JPA包括以下3方面的内容:

  1. 一套API标准。在javax.persistence的包下面,用来操作实体对象,执行CRUD操作,框架在后台替代我们完成所有的事情,开发者从烦琐的JDBC和SQL代码中解脱出来。
  2. 面向对象的查询语言:Java Persistence Query Language(JPQL)。这是持久化操作中很重要的一个方面,通过面向对象而非面向数据库的查询语言查询数据,避免程序的SQL语句紧密耦合。
  3. ORM(object/relational metadata)元数据的映射。JPA支持XML和JDK5.0注解两种元数据的形式,元数据描述对象和表之间的映射关系,框架据此将实体对象持久化到数据库表中。
    JPA的宗旨是为POJO提供持久化标准规范,由此可见,经过这几年的实践探索,能够脱离容器独立运行,方便开发和测试的理念已经深入人心了。Hibernate 3.2+、TopLink 10.1.3以及OpenJPA都提供了JPA的实现,以及最后的Spring的整合Spring Data JPA。目前互联网公司和传统公司大量使用了JPA的开发标准规范。
    image

3. 了解 SpringData

3.1 Spring Data介绍

  • Spring Data项目是从 2010年发展起来的,从创立之初 SpringData就想提供一个大家熟悉的、一致的、基于Spring的数据访问编程模型,同时仍然保留底层数据存储的特殊特性。它可以轻松地让开发者使用数据访问技术,包括关系数据库、非关系数据库(NoSQL)和基于云的数据服务。
  • Spring Data Common是Spring Data所有模块的公用部分,该项目提供跨Spring数据项目的共享基础设施。它包含了技术中立的库接口以及一个坚持java类的元数据模型。
  • Spring Data不仅对传统的数据库访问技术JDBC、Hibernate、 JDO、TopLick、JPA、Mybitas做了很好的支持、扩展、抽象、提供方便的API,还对NoSQL等非关系数据做了很好的支持,包括MongoDB、 Redis、Apache Solr等。

3.2 Spring Data的子项目

  • 主要子项目(main modules)如下

    1. Spring Data Commons
    2. Spring Data Gemfire
    3. Spring Data JPA
    4. Spring Data KeyValue
    5. Spring Data LDAP
    6. Spring Data MongoDB
    7. Spring Data REST
    8. Spring Data Redis
    9. Spring Data for Apache Cassandra
    10. Spring Data for Apache Solr
  • 社区支持的子项目(community modules)

    1. Spring Data Aerospike
    2. Spring Data Couchbase
    3. Spring Data DynamoDB
    4. Spring Data Elasticsearch
    5. Spring Data Hazelcast
    6. Spring Data Jest
    7. Spring Data Neo4j
    8. Spring Data Vault
  • 其他子项目(Related modules):

    1. Spring Data JDBC Extensions
    2. Spring for Apache Hadoop
    3. Spring Content

3.3 Spring Data操作的主要特性

  • Spring Data项目旨在为大家提供一种通用的编码模式。数据访问对象实现了对物理数据层的抽象,为编写查询方法提供了方便。通过对象映射,实现域对象和持续化存储之间的转换,而模板提供的是对底层存储实体的 访问实现。操作上主要有如下特征:
    1. 提供模板操作,如Spring Data Redis和Spring Data Riak。
    2. 强大的Repository和定制的数据存储对象的抽象映射。
    3. 对数据访问对象的支持(Auting等)。
      image

4. Spring Data JPA的主要类及结构图

4.1 我们需要掌握和使用到的类

1. 七个 Repository接口

  1. Repository (org.springframework.data.repository)
  2. CrudRepository (org.springframework.data.repository)
  3. PagingAndSortingRepository (org.springframework.data.repository)
  4. QueryByExampleExecutor (org.springframework.data.repository.query)
  5. JpaRepository (org.springframework.data.jpa.repository)
  6. JpaSpecificationExecutor (org.springframework.data.jpa.repository)
  7. QueryDslPredicateExecutor (org.springframework.data.querydsl)

2. 两个实现类:

  1. SimpleJpaRepository (org.springframework.data.jpa.repository.support)
  2. QueryDslJpaRepository (org.springframework.data.jpa.repository.support)

4.2 关系结构

image

4.3 真正底层封装 jpa底层的类

  1. EntityManager (javax.persistence)
  2. EntityManagerImpl (org.hibernate.jpa.internal)

5. 快速入门

  1. 环境
    jdk 1.8
    maven 3.x
    idea
  2. 创建数据库
    CREATE TABLE USER(
        id INT (11) NOT NULL PRIMARY KEY AUTO_INCREMENT,
        NAME VARCHAR (50) DEFAULT NULL,
        email VARCHAR (200) DEFAULT NULL
    ) ;
    
  3. pom
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>spring-data-jpa-01-threshold</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-data-jpa-01-threshold</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
    
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.45</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
    
  4. application.properties
    # 数据源配置
    spring.datasource.driver-class-name=com.mysql.jdbc.Driver
    spring.datasource.username=root
    spring.datasource.password=root
    spring.datasource.url=jdbc:mysql://localhost:3306/test
    # 显示 sql语句
    spring.jpa.show-sql=true
    # 正向工程
    spring.jpa.hibernate.ddl-auto=update
    
  5. entity
    //省略 set get tostring 方法
    @Entity //告诉JPA这是一个实体类(和数据表映射的类)
    public class User implements Serializable {
        @Id //这是一个主键
        //会报异常 Table 'test.hibernate_sequence' doesn't exist【数据库中的主键是自增的方式】
        //@GeneratedValue(strategy = GenerationType.AUTO)
        @GeneratedValue(strategy = GenerationType.IDENTITY) //自增主键
        private Long id;
        private String name;
        private String email;
    }
    
  6. 创建一个对象实现 CrudRepository
    public interface UserRepository extends CrudRepository<User, Long> {
    
    }
    
  7. controller
    @RestController 
    @RequestMapping("/user")
    public class UserController {
        @Autowired
        UserRepository userRepository;
    
        @GetMapping("/add")
        public void addNewUser(
                @RequestParam String name,
                @RequestParam String email) {
            User user = new User(null, name, email);
            userRepository.save(user);
        }
    
        @GetMapping("/all")
        public Iterable<User> getAllUsers() {
            return userRepository.findAll();
        }
    }
    
  8. 运行 main方法进行测试

二、 JPA基础查询方法

image

1. Spring Data Common的Repository

  • Repository位于Spring Data Common的lib里面,是Spring Data里面做数据库操作的最底层的抽象接口、最顶级的父类,源码里面其实什么方法都没有,仅仅起到一个标识作用。管理域类以及域类的id类型作为类型参数,此接口主要作为标记接口捕获要使用的类型,并帮助你发现扩展此接口的接口。Spring底层做动态代理的时候发现只要是它的子类或者实现类,都代表储存库操作。
  • Repository的源码如下:
    package org.springframework.data.repository;
    import org.springframework.stereotype.Indexed;
    @Indexed
    public interface Repository<T, ID> {
    }
    
  • 有了这个类,我们就能找到好多Spring Data JPA提供的基本接口和操作类,及其实现方法。这个接口定义了所有Repostory操作的实体和ID两个泛型参数。我们不需要继承任何接口,只要继承这个接口,就可以使用Spring JPA里面提供的很多约定的方法查询和注解查询。

2. Repository的类层次关系(diagms/hierarchy/structure)

  1. 利用 idea工具查看 Repository的类的继承体系
    image
    image
    image

3. CrudRepository方法详解

  • 通过类关系图可以看到CrudRepository提供了公共的通用的CRUD方法。

3.1. CrudRepository interface内容

package org.springframework.data.repository;
import java.util.Optional;
@NoRepositoryBean
public interface CrudRepository<T, ID> extends Repository<T, ID> {
    //保存实体方法
    <S extends T> S save(S entity);
    //批量保存。原理和步骤(1)相同。实现方法就是for循环调用上面的save方法。
    <S extends T> Iterable<S> saveAll(Iterable<S> entities);
    //根据主键查询实体。
    Optional<T> findById(ID id);
    //据主键判断实体是否存在。
    boolean existsById(ID id);
    //查询实体的所有列表。
    Iterable<T> findAll();
    //根据主键列表查询实体列表。
    Iterable<T> findAllById(Iterable<ID> ids);
    //查询总数。
    long count();
    //根据主键删除。我们通过刚才的类关系查看其他实现类。
    void deleteById(ID id);
    void delete(T entity);
    void deleteAllById(Iterable<? extends ID> ids);
    void deleteAll(Iterable<? extends T> entities);
    void deleteAll();
}
  1. 保存实体方法。我们通过刚才的类关系查看其他实现类。 SimpleJpaRepository里面的实现方法:
    @Transactional
    public <S extends T> S save(S entity) {
        Assert.notNull(entity, "Entity must not be null.");
        if (this.entityInformation.isNew(entity)) {
            this.em.persist(entity);
            return entity;
        } else {
            return this.em.merge(entity);
        }
    }
    
  • 我们发现它是先检查传进去的实体是不是存在,然后判断是新增还是更新;是不是存在两种根据机制,一种是根据主键来判断,另一种是根据Version来判断。如果我们去看JPA控制台打印出来的SQL,最少会有两条,一条是查询,一条是insert或者update。
    @Transactional
    public void deleteById(ID id) {
        Assert.notNull(id, "The given id must not be null!");
        this.delete(this.findById(id).orElseThrow(() -> {
            return new EmptyResultDataAccessException(String.format("No %s entity with id %s exists!", this.entityInformation.getJavaType(), id), 1);
        }));
    }
    
  • 我们看到JPA会先去查询一下,再做保存,不存在抛出异常。
  • 这里特别强调一下delete和save方法,因为在实际工作中有的人会画蛇添足,自己先去查询再做判断处理,其实Spring JPA底层都已经考虑到了。

4. PagingAndSortingRepository方法详解

  • 可以看到PagingAndSortingRepository继承CrudRepository所有的基本方法,它增加了分页和排序等对查询结果进行限制的基本的、常用的、通用的一些分页方法。

4.1 PagingAndSortingRepository interface内容

package org.springframework.data.repository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
@NoRepositoryBean
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
    Iterable<T> findAll(Sort sort);
    Page<T> findAll(Pageable pageable);
}
  1. 根据排序取所有对象的集合。
  2. 根据分页和排序进行查询,并用Page对象封装。Pageable 对象包含分页和Sort对象。
    PagingAndSortingRepository和CrudRepository都是Spring Data Common的标准接口,如果我们采用JPA,那它对应的实现类就是Spring Data JPA的model里面的SimpleJpaRepository。如果是其他NoSQL的实现Mongodb,那它的实现就在Spring Data Mongodb的model里面。
    实现内容如下
    public Page<T> findAll(Pageable pageable) {
        return (Page)(isUnpaged(pageable) ? new PageImpl(this.findAll()) : this.findAll((Specification)null, pageable));
    }
    

4.2 PagingAndSortingRepository interface使用示例

  • 只需要继承PagingAndSortingRepository的接口即可,其他不用做任何改动。
    public interface UserPagingAndSortingRepository extends PagingAndSortingRepository<User, Long> {
    }
    
  • UserController
    @Autowired
    UserPagingAndSortingRepository userPSRepository;
    
    /**
     * 验证排序和分页查询方法
     *
     * @return
     */
    @GetMapping("/page")
    public Page<User> getAllUserByPage() {
        //第二种
        Page<User> page = userPSRepository.findAll(
                PageRequest.of(1, 5,
                        Sort.by(Sort.Direction.DESC, "id")));
    
        //第一种
        Iterable<User> page2 =
                userPSRepository.findAll(
                        PageRequest.of(1, 5,
                                Sort.by(Sort.Order.desc("id"))));
        //打印
        System.out.println(page.getContent());
        page2.forEach(System.out::println);
        return page;
    }
    
    /**
     * 排序查询方法
     *
     * @return
     */
    @GetMapping("/sort")
    public Iterable<User> getAllUserWithSort() {
        return userPSRepository.findAll(Sort.by(Sort.Order.asc("name")));
    }
    

5. JpaRepository方法详解

5.1 JpaRepository详解

  • JpaRepository 到这里可以进入分水岭了,上面的那些都是Spring Data为了兼容NoSQL而进行的一些抽象封装,从JpaRepository开始是对关系型数据库进行抽象封装。从类图可以看得出来它继承了PagingAndSortingRepository类,也就继承了其所有方法,并且实现类也是SimpleJpaRepository。从类图上还可以看出JpaRepository继承和拥有了QueryByExampleExecutor的相关方法。
package org.springframework.data.jpa.repository;
import java.util.List;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.QueryByExampleExecutor;

@NoRepositoryBean
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
    List<T> findAll();
    List<T> findAll(Sort sort);
    List<T> findAllById(Iterable<ID> ids);
    <S extends T> List<S> saveAll(Iterable<S> entities);
    void flush();
    <S extends T> S saveAndFlush(S entity);
    <S extends T> List<S> saveAllAndFlush(Iterable<S> entities);

    /** @deprecated */
    @Deprecated
    default void deleteInBatch(Iterable<T> entities) {
        this.deleteAllInBatch(entities);
    }

    void deleteAllInBatch(Iterable<T> entities);
    void deleteAllByIdInBatch(Iterable<ID> ids);
    void deleteAllInBatch();

    /** @deprecated */
    @Deprecated
    T getOne(ID id);
    T getById(ID id);
    <S extends T> List<S> findAll(Example<S> example);
    <S extends T> List<S> findAll(Example<S> example, Sort sort);
}
  • 通过源码和CrudRepository相比较,它支持Query By Example,批量删除,提高删除效率,手动刷新数据库的更改方法,并将默认实现的查询结果变成了List。

5.2 JpaRepository的使用方法

  • JpaRepository的使用方法也一样,只需要继承它即可

6. Repository的实现类SimpleJpaRepository

  • SimpleJpaRepository是JPA整个关联数据库的所有Repository的接口实现类。如果想进行扩展,可以继承此类,如QueryDsl的扩展,还有默认的处理机制。如果将此类里面的实现方法看透了,基本上JPA的API就能掌握大部分。同时也是Spring JPA动态代理的实现类。
  • SimpleJpaRepository的部分源码如下
package org.springframework.data.jpa.repository.support;

@Repository
@Transactional(
    readOnly = true
)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
    private static final String ID_MUST_NOT_BE_NULL = "The given id must not be null!";
    private final JpaEntityInformation<T, ?> entityInformation;
    private final EntityManager em;
    private final PersistenceProvider provider;
    @Nullable
    private CrudMethodMetadata metadata;
    private EscapeCharacter escapeCharacter;

    @Transactional
    public void deleteAllInBatch() {
        this.em.createQuery(this.getDeleteAllQueryString()).executeUpdate();
    }
}
  • 可以看出SimpleJpaRepository的实现机制还挺清晰的,通过EntityManger进行实体的操作,JpaEntityInforMation里面保存着实体的相关信息以及crud方法的元数据等

三、定义查询方法

image

1. 定义查询方法的配置方法

  • 由于Spring JPA Repository的实现原理是采用动态代理的机制,所以我们介绍两种定义查询方法:从方法名称中可以指定特定用于存储的查询和更新,或通过使用@Query手动定义的查询,这个取决于实际存储操作。只需要实体Repository继承Spring Data Common里面的Repository接口即可,就像前面我们讲的一样。如果你想有其他更多默认通用方法的实现,可以选择JpaRepository、 PagingAndSortingRepository、CrudRepository等接口,也可以直接继承我们后面要讲的JpaSpecificationExecutor、 QueryByExampleExecutor和自定义Response,都可以达到同样的效果。
  • 如果你不想扩展Spring数据接口,还可以使用它来注解存储库接口@RepositoryDefinition。扩展CrudRepository公开了一套完整的方法来操纵实体。如果你希望对所暴露的方法有选择性,只需要将暴露的方法复制CrudRepository到域库中即可。其实也是自定义Repository的一种。
    选择性的暴露 CRUD方法
    @NoRepositoryBean
    public interface MyBaseRepository<T, ID> extends Repository<T, ID> {
        T findOne(ID id);
        T save(T entity);
    }
    public interface UserRepository extends MyBaseRepository<User, Long> {
        User findByEmailAddress(String emailAddress);
    }
    
  • 在这个示例的第一步中为所有域存储库定义了一个公共基础接口,并将其暴露出来。findOne(…)和save(…)方法将被路由到由Spring Data提供的、你选择的存储库的基本存储库实现中,例如JPA中的SimpleJpaRepository。因为它们正在匹配方法签名CrudRepository,所以UserRepository将能够保存用户,并通过id查找单个用户信息,以及触发查询以通过其电子邮件地址查找Users。

2. 方法的查询策略设置

  • 通过@EnableJpaRepositories(queryLookupStrategy= QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND)可以配置方法的查询策略,其中QueryLookupStrategy.Key的值一共有三个。
    1. CREATE:直接根据方法名进行创建。规则是根据方法名称的构造进行尝试,一般的方法是从方法名中删除给定的一组已知前缀,并解析该方法的其余部分。如果方法名不符合规则,启动的时候就会报异常。
    2. USE_DECLARED_QUERY:声明方式创建,即本书说的注解方式。启动的时候会尝试找到一个声明的查询,如果没有找到就将抛出一个异常。查询可以由某处注释或其他方法声明。
    3. CREATE_IF_NOT_FOUND:这个是默认的,以上两种方式的结合版。先用声明方式进行查找,如果没有找到与方法相匹配的查询,就用create的方法名创建规则创建一个查询。
  • 除非有特殊需求,一般直接用默认的,不用管。配置示例如下:
    @EnableJpaRepositories(queryLookupStrategy = QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND)
    @SpringBootApplication
    public class SpringDataJpa01ThresholdApplication {
        public static void main(String[] args) {
            SpringApplication.run(SpringDataJpa01ThresholdApplication.class, args);
        }
    }
    
  • QueryLookupStrategy是策略的定义接口, JpaQueryLookupStrategy是具体策略的实现类。
    image

3. 查询方法的创建

  • 内部基础架构中有个根据方法名的查询生成器机制,对于在存储库的实体上构建约束查询很有用。该机制方法的前缀有find…By、 read…By、query…By、count…By和get…By,从这些方法可以分析它的其余部分(实体里面的字段)。引入子句可以包含其他表达式,例如在Distinct要创建的查询上设置不同的标志。然而,第一个By作为分隔符来指示实际标准的开始。在一个非常基本的水平上,你可以定义实体性条件,并与它们串联(And和Or)。
  • 用一句话概括,待查询功能的方法名由查询策略(关键字)、查询字段和一些限制性条件组成。在如下例子中,可以直接在controller里面进行调用以查看效果:
    public interface PersonRepository extends Repository<User, Long> {
        //and 的查询关系
        List<User> findByEmailAddressAndLastName(String emailAddress, String lastName);
    
        //包括 distinct去重,or的sql语法
        List<User> findDistinctPeopleByLastNameOrFirstName(String lastName, String firstName);
    
        //根据 lastName字段查询忽略大小写
        List<User> findByLastNameIgnoreCase(String lastName);
    
        //根据 lastName 和 firstName 查询 equal并且忽略大小写
        List<User> findByLastNameAndFirstNameAllIgnoreCase(String lastName, String firstName);
    
        //对查询结果 根据lastName排序
        List<User> findByLastNameOrderByFirstNameAsc(String lastName);
        List<User> findByLastNameOrderByFirstNameDesc(String lastName);
    }
    
  • 解析方法的实际结果取决于创建查询的持久性存储。但是,有一些常见的事项需要注意:
    1. 表达式通常是可以连接的运算符的属性遍历。你可以使用组合属性表达式AND和OR。你还可以将运算关键字Between、 LessThan、GreaterThan、Like作为属性表达式。受支持的操作员可能因数据存储而异,因此请参阅官方参考文档的相应部分内容。
    2. 该方法解析器支持设置一个IgnoreCase标志个别特性(例如, findByLastnameIgnoreCase(…))或支持忽略大小写(通常是一个类型的所有属性为String的情况下,例如, findByLastnameAndFirstnameAllIgnoreCase(…))。是否支持忽略示例可能会因存储而异,因此请参阅参考文档中的相关章节,了解特定于场景的查询方法。
    3. 可以通过OrderBy在引用属性和提供排序方向(Asc或Desc)的查询方法中附加一个子句来应用静态排序。要创建支持动态排序的查询方法来影响查询结果。

4. 关键字列表

关键字 示例 JPQL表达式
And findByLastNameAndFirstName ... where x.lastName = ?1 and x.firstName = ?2
Or findByLastNameOrFirstName ... where x.lastName = ?1 or x.firstName = ?2
Is、Equals findByFirstName、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(参数增加前缀 %)
EndingWith findByFirstNameEndingWith ... where x.firstName like ?1(参数增加后缀 %)
Containing findByFirstNameContaining ... where x.firstName like ?1(参数被 %包裹)
OrderBy findByAgeOrderByLastNameDesc ... where x.age =?1 order by x.lastName desc
Not findByLastNameNot ... where x.lastName <> ?1
In findByIn(Collection<Age> ages) ... where x.age in ?1
NotIn findByAgeNotIn(Collection<Age> ages) ... where x.age not in ?1
True findByActiveTrue ... where x.active = true
False findByActiveFalse() ... where x.active = false
IgnoreCase findByFirstNameIgnoreCase ... where UPPER(x.firstName) = UPPER(?1)
  • 注意,除了 find的前缀之外,我们可以查看 PartTree的源码,还有其他前缀
    public class PartTree implements Streamable<PartTree.OrPart> {
        private static final String KEYWORD_TEMPLATE = "(%s)(?=(\\p{Lu}|\\P{InBASIC_LATIN}))";
        private static final String QUERY_PATTERN = "find|read|get|query|search|stream";
        private static final String COUNT_PATTERN = "count";
        private static final String EXISTS_PATTERN = "exists";
        private static final String DELETE_PATTERN = "delete|remove";
        private static final Pattern PREFIX_TEMPLATE = Pattern.compile("^(find|read|get|query|search|stream|count|exists|delete|remove)((\\p{Lu}.*?))??By");
        private final PartTree.Subject subject;
        private final PartTree.Predicate predicate;
    
        private static class Subject {
            private static final String DISTINCT = "Distinct";
            private static final Pattern COUNT_BY_TEMPLATE = Pattern.compile("^count(\\p{Lu}.*?)??By");
            private static final Pattern EXISTS_BY_TEMPLATE = Pattern.compile("^(exists)(\\p{Lu}.*?)??By");
            private static final Pattern DELETE_BY_TEMPLATE = Pattern.compile("^(delete|remove)(\\p{Lu}.*?)??By");
            private static final String LIMITING_QUERY_PATTERN = "(First|Top)(\\d*)?";
            private static final Pattern LIMITED_QUERY_TEMPLATE = Pattern.compile("^(find|read|get|query|search|stream)(Distinct)?(First|Top)(\\d*)?(\\p{Lu}.*?)??By");
            private final boolean distinct;
            private final boolean count;
            private final boolean exists;
            private final boolean delete;
            private final Optional<Integer> maxResults;
        }
        private static class Predicate implements Streamable<PartTree.OrPart> {
            private static final Pattern ALL_IGNORE_CASE = Pattern.compile("AllIgnor(ing|e)Case");
            private static final String ORDER_BY = "OrderBy";
            private final List<PartTree.OrPart> nodes;
            private final OrderBySource orderBySource;
            private boolean alwaysIgnoreCase;
        }
    }
    
  • 使用的时候要配合不同的返回结果进行使用,列如:(除了条件字段,pojo中的属性字段一定要存在并且首字母大小,删除修改的方法添加事务)
    public interface UserRepository extends CrudRepository<User, Long> {
        Long countByName(String name);//查询总数
    
        //对于删除和修改需要进行事务否则运行出现异常
        @Transactional
        Long deleteByName(String name);//根据一个字段进行删除操作
    
        @Transactional
        List<User> removeByName(String name);
    }
    

5. 方法的查询策略的属性表达式

  • 属性表达式(Property Expressions)只能引用托管(泛化)实体的直接属性,如前一个示例所示。在查询创建时,你已经确保解析的属性是托管实体的属性。同时,还可以通过遍历嵌套属性定义约束。假设一个Person实体对象里面有一个Address属性里面包含一个ZipCode属性。
  • 在这种情况下,方法名为:
    List<person> findByAddressZipCode(String zipCode);
    
  • 创建及其查找的过程是:解析算法首先将整个 part(AddressZipCode)解释为属性,并使用该名称(uncapitalized)检查域类的属性。如果算法成功,就使用该属性。如果不是,就拆分右侧驼峰部分的信号源到头部和尾部,并试图找出相应的属性,在我们的例子中是AddressZip和Code。如果算法找到一个具有头部的属性,那么它需要尾部,并从那里继续构建树,然后按照刚刚描述的方式将尾部分割。如果第一个分割不匹配,就将分割点移动到左边(Address、ZipCode),然后继续。
  • 虽然这在大多数情况下应该起作用,但是算法可能会选择错误的属性。假设Person类也有一个addressZip属性,该算法将在第一个分割轮中匹配,并且基本上会选择错误的属性,最后失败(因为该类型addressZip可能没有code属性)。
  • 要解决这个歧义,可以在方法名称中手动定义遍历点,所以我们的方法名称最终会是:
    List<person> findByAddress_ZipCode(String zipCode);
    
  • 当然Spring JPA里面是将下划线视为保留字符,但是强烈建议遵循标准Java命名约定(不使用属性名称中的下划线,而是使用骆驼示例)。命名属性的时候注意一下这个特性。
  • 可以到PartTreeJpaQuery.class查询一下相关的method的name拆分和实现逻辑,也可以利用开发工具的Search anywhere视图输入PropertyExpression,然后Find Used就可以跟出很多源码,再设置一个断点,就可以进行代码分析了。

6 查询结果的处理

6.1 参数选择分页和排序(Pageable、Sort)

a. 特定类型的参数,动态的将分页和排序应用于查询

  • 示例:自查询方法中使用 Pageable,Slice和 Sort。
    Page<User> findByName(String name, Pageable pageable);
    Slice<User> findByName(String name, Pageable pageable);
    List<User> findByName(String name, Sort sort);
    List<User> findByName(String name, Pageable pageable);
    
    @Resource
    UserRepository userRepository;
    @Test
    void contextLoads() {
        userRepository.findByName("dd", Sort.by(Sort.Direction.ASC, "name"));
        userRepository.findByName("ff", PageRequest.of(0, 5));
    //Hibernate: select user0_.id as id1_0_, user0_.email as email2_0_, user0_.name as name3_0_ from user user0_ where user0_.name=? order by user0_.name asc
    //Hibernate: select user0_.id as id1_0_, user0_.email as email2_0_, user0_.name as name3_0_ from user user0_ where user0_.name=? limit ?
    }
    
  • 第一种方法允许将org.springframework.data.domain.Pageable实例传递给查询方法,以便动态地将分页添加到静态定义的查询中。 Page知道可用的元素和页面的总数。它通过基础框架里面触发计数查询来计算总数。由于这可能是昂贵的,具体取决于所使用的场景,说白了,当用到Pageable的时候会默认执行一条cout语句。而Slice的作用是,只知道是否有下一个Slice可用,不会执行count,所以当查询较大的结果集时,只知道数据是足够的就可以了,而且相关的业务场景也不用关心一共有多少页。
  • 排序选项也通过Pageable实例处理。如果只需要排序,那么在org.springframework.data.domain.Sort参数中添加一个参数即可。正如你可以看到的,只返回一个List也是可能的。在这种情况下, Page将不会创建构建实际实例所需的附加元数据(这反过来意味着必须不被发布的附加计数查询),而仅仅是限制查询仅查找给定范围的实体。

b. 限制查询结果

  • 官方示例
  • 示例:使用first或top关键字来限制查询方法的结果,这两个关键字可以互换使用。您可以将一个可选的数值附加到top或first指定要返回的最大结果大小。如果忽略该数字,则假定结果大小为 1
    User findFirstByOrderByLastnameAsc();
    User findTopByOrderByAgeDesc();
    Page<User> queryFirst10ByLastname(String lastname, Pageable pageable);
    Slice<User> findTop3ByLastname(String lastname, Pageable pageable);
    List<User> findFirst10ByLastname(String lastname, Sort sort);
    List<User> findTop10ByLastname(String lastname, Pageable pageable);
    
  • 限制表达式还支持Distinct支持不同查询的数据存储的关键字。此外,对于将结果集限制为一个实例的查询,Optional支持将结果用关键字包装。
  • 如果分页或切片应用于限制查询分页(以及可用页数的计算),则将其应用于受限结果。

6.2 查询结果的不同形式(List、Stream、Page、Future)

  • Page和List在上面的示例中都有涉及
    a. 流式查询结果

  • 您可以使用 Java 8 Stream<T>作为返回类型以增量方式处理查询方法的结果。Stream数据存储特定的方法用于执行流式传输,而不是将查询结果包装在 中,如以下示例所示:

    @Query("select u from User u")
    Stream<User> findAllByCustomQueryAndStream();
    Stream<User> readAllByFirstnameNotNull();
    @Query("select u from User u")
    Stream<User> streamAllPaged(Pageable pageable);
    
  • Stream潜在地包装了底层数据存储特定的资源,因此必须在使用后关闭。您可以Stream使用close()方法或使用 Java 7try-with-resources块手动关闭,如以下示例所示:

    try (Stream<User> stream = repository.findAllByCustomQueryAndStream()) {
      stream.forEach(…);
    }
    
  • 并非所有 Spring Data 模块当前都支持Stream作为返回类型
    b. 异步查询结果

  • 可以使用Spring 的异步方法运行能力异步运行存储库查询。这意味着该方法在调用时立即返回,而实际查询发生在已提交给 Spring 的任务中TaskExecutor。异步查询不同于反应式查询,不应混合使用。有关反应式支持的更多详细信息,请参阅商店特定的文档。以下示例显示了一些异步查询:

    @Async
    Future<User> findByFirstname(String firstname); ①
    @Async
    CompletableFuture<User> findOneByFirstname(String firstname); ② 
    @Async
    ListenableFuture<User> findOneByLastname(String lastname); ③
    
    1. 使用java.util.concurrent.Future作为返回类型。
    2. 使用 Java 8java.util.concurrent.CompletableFuture作为返回类型。
    3. 使用 aorg.springframework.util.concurrent.ListenableFuture作为返回类型。
      c. Projections对查询结果地扩展
  • Spring JPA对Projections扩展的支持是非常好的。从字面意思上理解就是映射,指的是和DB查询结果的字段映射关系。一般情况下,返回的字段和DB查询结果的字段是一一对应的,但有的时候,我们需要返回一些指定的字段,不需要全部返回,或者只返回一些复合型的字段,还要自己写逻辑。Spring Data正是考虑到了这一点,允许对专用返回类型进行建模,以便我们有更多的选择,将部分字段显示成视图对象。

  • 假设Person是一个正常的实体,和数据表Person一一对应,正常的写法如下:

    @Entity
    class Person {
        @Id UUID id;
        String firstname, lastname;
        Address address;
        static class Address {
        String zipCode, city, street;
        }
    }
    interface PersonRepository extends Repository<Person, UUID> {
        Collection<Person> findByLastname(String lastname);
    }
    
  • 我们想仅仅返回其中与name相关的字段,应该怎么做呢?基于projections的思路,其实是比较容易的。我们只需要声明一个接口,包含要返回的属性的方法即可,例如:

    interface NamesOnly {
      String getFirstname();
      String getLastname();
    }
    
  • Repository里面的写法如下,直接用这个对象接收结果即可:

    interface PersonRepository extends Repository<Person, UUID> {
      Collection<NamesOnly> findByLastname(String lastname);
    }
    
  • 在Ctroller里面直接调用对象可以查看结果。原理是运行时底层会有动态代理机制为这个接口生成一个实现实体类。

  • 查询关联的子对象,例如:

    interface PersonSummary {
      String getFirstname();
      String getLastname();
     AddressSummary getAddress();
    
      interface AddressSummary {
        String getCity();
      }
    }
    
  • @Value 和 @SPEL也支持

    interface NamesOnly {
    
      @Value("#{target.firstname + ' ' + target.lastname}")
      String getFullName();
      …
    }
    
  • PersonRepository里面保持不变,这样会返回一个firstname和lastname相加的只有fullName的结果集合。

  • 对 Spel表达式地支持不止这些:

    @Component
    class MyBean {
      String getFullName(Person person) {
        …//自定义地运算
      }
    }
    
    interface NamesOnly {
    
      @Value("#{@myBean.getFullName(target)}")
      String getFullName();
      …
    }
    
  • 还可以通过Spel表达式取到方法里面的参数值。

    interface NamesOnly {
    
      @Value("#{args[0] + ' ' + target.firstname + '!'}")
      String getSalutation(String prefix);
    }
    
  • 这时可能有人会想,只能用interface吗?Dto支持吗?其实也是可以的,我们也可以定义自己的Dto实体类。需要哪些字段,我们直接在Dto类中使用get/set属性即可。例如:

    class NamesOnly {
      private final String firstname, lastname;
      //注意构造方法
      NamesOnly(String firstname, String lastname) {
        this.firstname = firstname;
        this.lastname = lastname;
      }
      String getFirstname() {
        return this.firstname;
      }
      String getLastname() {
        return this.lastname;
      }
      // equals(…) and hashCode() implementations
    }
    
  • 支持动态projections。通过泛化,可以根据不同的业务情况返回不同的字段集合。可以对PersonRepository做一定的变化,例如:

    interface PersonRepository extends Repository<Person, UUID> {
      <T> Collection<T> findByLastname(String lastname, Class<T> type);
    }
    //调用可以同 class类型动态指定返回不同字段地结果集合
    void someMethod(PersonRepository people) {
      //想包含全字段,直接用原始 entity(Person.class) 接收即可
      Collection<Person> aggregates =
        people.findByLastname("Matthews", Person.class);
      //如果想仅仅返回名称,只需要指定 Dto即可
      Collection<NamesOnly> aggregates =
        people.findByLastname("Matthews", NamesOnly.class);
    }
    
  • Projections的应用场景还是挺多的,希望大家好好体会,以利用更优雅的代码实现不同的场景,不必用数组、冗余的对象去接收查询结果。

四、注解查询方法

image

1. @Query 详解

1.1 语法及源码

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@QueryAnnotation
@Documented
public @interface Query {
    //指定 jpql地查询语句。nativeQuery=true 地时候,是原生地 sql语句
    String value() default "";
    //指定 count地 jpql语句,如果不指定将根据 query自动生成。
    //nativeQuery=true 地时候,是原生地 sql语句
    String countQuery() default "";
    //根据那个字段 count,一般默认即可
    String countProjection() default "";
    //默认是 false,表示 value里面是不是原生地 sql语句
    boolean nativeQuery() default false;
    //可以指定一个 query地名字,必须是唯一地。
    //如果不指定,默认地生成规则是:{$domainClass}.${queryMethodName}
    String name() default "";
    //可以指定一个 count地query名字,必须是唯一地。
    //如果不指定,默认地生成规则是:${$domainClass}.${queryMethodName}.count
    String countName() default "";
}

1.2 @Query用法

  • 使用命名查询为实体声明查询是一种有效的方法,对于少量查询很有效。一般只需要关心@Query里面的value和nativeQuery的值。使用声明式JPQL查询有一个好处,就是启动的时候就知道语法正确与否。
  • 示例1:声明一个注解在 Repository地查询方法上。
    public interface UserRepository extends JpaRepository<User, Long> {
        @Query("select u from User u where u.emailAddress = ?1")
        User findByEmailAddress(String emailAddress);
    }
    
  • 示例2:Like查询,注意 firstName不会自动加上%关键字地
    public interface UserRepository extends JpaRepository<User, Long> {
        @Query("select u from User u where u.firstname like %?1")
        List<User> findByFirstnameEndsWith(String firstname);
    }
    
  • 示例3:直接使用原始 sql
    public interface UserRepository extends JpaRepository<User, Long> {
        @Query(value = "SELECT * FROM USERS WHERE EMAIL_ADDRESS = ?1", nativeQuery = true)
        User findByEmailAddress(String emailAddress);
    }
    

tips
nativeQuery 不支持直接 sort地参数查询

  • 示例4:nativeQuery排序地错误写法,下面这个是启动不起来地
    public interface UserRepository extends JpaRepository<User, Long> {
        @Query(value = "SELECT * FROM USERS WHERE LASTNAME = ?1",
                nativeQuery = true)
        Page<User> findByLastname(String lastname, Pageable pageable);
    }
    
  • 示例5:nativeQuery排序地正确写法
    public interface UserRepository extends JpaRepository<User, Long> {
        @Query(value = "SELECT * FROM USERS WHERE LASTNAME = ?1 order by ?2",
                nativeQuery = true)
        Page<User> findByLastname(String lastname, String sort);
    }
    //调用地地方 lastName是数据里面地字段名,不是对象地片段名
    repository.findByLastname("jack", "lastname");
    

1.3 @Query排序

public interface UserRepository extends JpaRepository<User, Long> {
    @Query("select u from User u where u.lastname like ?1%")
    List<User> findByAndSort(String lastname, Sort sort);
    @Query("select u.id, LENGTH(u.firstname) as fn_len from User u where u.lastname like ?1%")
    List<Object[]> findByAsArrayAndSort(String lastname, Sort sort);
}

repo.findByAndSort("lannister", Sort.by("firstname")); //Sort指向域模型中属性的有效表达式。
repo.findByAndSort("stark", Sort.by("LENGTH(firstname)")); //无效的Sort包含函数调用。抛出异常。
repo.findByAndSort("targaryen", JpaSort.unsafe("LENGTH(firstname)")); //有效Sort包含显式不安全 Order。
repo.findByAsArrayAndSort("bolton", Sort.by("fn_len")); //Sort指向别名函数的有效表达式。

1.4 @Query 分页

  • 直接用 Page对象接收接口,参数直接用 Pageable地实现类即可。
    public interface UserRepository extends JpaRepository<User, Integer> {
        @Query(value = "select u from User u where u.lastname = ?1")
        Page<User> findByLastname(String lastname, Pageable pageable);
    }
    //调用者地写法
    repository.findByLastname("jack", QPageRequest.of(1, 10));
    
  • 原生sql:nativeQuery 分页
    public interface UserRepository extends JpaRepository<User, Long> {
        @Query(value = "SELECT * FROM USERS WHERE LASTNAME = ?1",
                countQuery = "SELECT count(*) FROM USERS WHERE LASTNAME = ?1",
                nativeQuery = true)
        Page<User> findByLastname(String lastname, Pageable pageable);
    }
    

2. @Param用法

  • 默认情况下,Spring Data JPA 使用基于位置的参数绑定,如前面所有示例中所述。这使得在重构参数位置时查询方法有点容易出错。为了解决这个问题,您可以使用@Param注解给方法参数一个具体的名称并在查询中绑定名称,如下例所示:
    public interface UserRepository extends JpaRepository<User, Long> {
      @Query("select u from User u where u.firstname = :firstname or u.lastname = :lastname")
      User findByLastnameOrFirstname(@Param("lastname") String lastname,
                                     @Param("firstname") String firstname);
    }
    
  • 方法参数根据它们在定义的查询中的顺序进行切换。

3. SpEl表达式地支持

本文作者:西宫结弦

本文链接:https://www.cnblogs.com/NishimiyaShouko/p/15768687.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   西宫结弦  阅读(402)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起