SpringDataJpa框架使用【超详细!】

SpringDataJpa框架使用【超详细!】

SpringDataJpa框架使用文档
一、什么是 Jpa ?
jpa 的全称是 Java Persistence API , 中文的字面意思就是 java 的持久层 API , jpa 就是定义了一系列标准,让实体类和数据库中的表建立一个对应的关系,当我们在使用 java 操作实体类的时候能达到操作数据库中表的效果(不用写sql ,就可以达到效果),jpa 的实现思想即是 ORM (Object Relation Mapping),对象关系映射,用于在关系型数据库和业务实体对象之间作一个映射。

jpa 并不是一个框架,是一类框架的总称,持久层框架 Hibernate 是 jpa 的一个具体实现,本文要谈的 spring data jpa 又是在 Hibernate 的基础之上的封装实现。

使用 jpa 是可以解决一些我们写 sql 语句的烦恼,相反另一个数据库层的框架 mybatis是专注 sql 语句的;

二、SpringDataJpa常用的 jpa 的配置

  1. 项目依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
  1. application.properties 配置

#项目端口的常用配置
server.port=8081
 
# 数据库连接的配置
spring.datasource.url=jdbc:mysql:///jpa?useSSL=false
spring.datasource.username=root
spring.datasource.password=zempty123
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
 
#数据库连接池的配置,hikari 连接池的配置
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.connection-timeout=10000
spring.datasource.hikari.maximum-pool-size=15
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.auto-commit=true
 
 
#通过 jpa 自动生成数据库中的表
 
spring.jpa.hibernate.ddl-auto=update
#该配置比较常用,当服务首次启动会在数据库中生成相应表,后续启动服务时如果实体类有增加属性会在数据中添加相应字段,
#原来数据仍在,该配置除了 update ,\
#还有其他配置值,
#create :该值慎用,每次重启项目的时候都会删除表结构,重新生成,原来数据会丢失不见。
#create-drop :慎用,当项目关闭,数据库中的表会被删掉。
#validate : 验证数据库和实体类的属性是否匹配,不匹配将会报错。
 
spring.jpa.show-sql=true
#该配置当在执行数据库操作的时候会在控制台打印 sql 语句,方便我们检查排错等。
 
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
#数据库的方言配置。

三、类映射到数据库表的常用注解

1、@Entity

用来注解该类是一个实体类用来进行和数据库中的表建立关联关系,首次启动项目的时候,默认会在数据中生成一个同实体类相同名字的表(table),也可以通过注解中的 name 属性来修改表(table)名称, 如@Entity(name=“stu”) , 这样数据库中表的名称则是 stu 。 该注解十分重要,如果没有该注解首次启动项目的时候你会发现数据库没有生成对应的表。

2、@Id

@Id 类的属性注解,该注解表明该属性字段是一个主键,该属性必须具备,不可缺少。

3、@GeneratedValue

该注解通常和 @Id 主键注解一起使用,用来定义主键的呈现形式,

该注解通常有多种使用策略:

  • @GeneratedValue(strategy= GenerationType.IDENTITY) 该注解由数据库自动生成,主键自增型,在 mysql 数据库中使用最频繁,oracle 不支持。
  • @GeneratedValue(strategy= GenerationType.AUTO) 主键由程序控制,默认的主键生成策略,oracle 默认是序列化的方式,mysql 默认是主键自增的方式。
  • @GeneratedValue(strategy= GenerationType.SEQUENCE) 根据底层数据库的序列来生成主键,条件是数据库支持序列,Oracle支持,Mysql不支持。
  • @GeneratedValue(strategy= GenerationType.TABLE) 使用一个特定的数据库表格来保存主键,较少使用。

以上的主键生成策略当中,在数据库 mysql 当中 IDENTITY 和 AUTO 用的较多,二者当中 IDENTITY 用的多些

4、@Column

是一个类的属性注解,该注解可以定义一个字段映射到数据库属性的具体特征,比如字段长度,映射到数据库时属性的具体名字等。

5、@Transient

是一个属性注解,该注解标注的字段不会被应射到数据库当中。

实例代码:

@Setter//lombok 的注解
@Getter//lombok 的注解
@Accessors(chain = true) //lombok 的注解
@Entity(name = "stu")
//@Table
public class Student { 
    @Id
    @GeneratedValue(strategy= GenerationType.TABLE)
    private long id;
 
    @Column(length = 100)
    private String name;
 
    @Transient
    private String test;
 
    private int age;
 
    private LocalTime onDuty;
 
    private LocalDate onPosition;
 
    private LocalDateTime birthdayTime;
}

四、使用SpringDataJpa进行增删改查

  1. 定义Dao层

定义一个 Student 的 dao 层,这样我们的增删改查就已经有了

public interface StudentRepository extends JpaRepository<Student,Integer> {
}

在 spring boot 项目中在 dao 层我们不需要写 @Repository 注解 ,我们在使用的时候直接注入使用就好,这里需要说明一点, 我们在更新数据的时候,可以先查询,然后更改属性,使用 save 方法保存就好。

  1. 使用关键字自定义查询

我们可以使用 jpa 提供的 find 和 get 关键字完成常规的查询操作,使用 delete 关键字完成删除,使用 count 关键字完成统计等

public interface StudentRepository extends JpaRepository<Student,Integer> {
 
    // 查询数据库中指定名字的学生
    List<Student> findByName(String name);
 
    // 根据名字和年龄查询学生
    List<Student> getByNameAndAge(String name, Integer age);
 
    //删除指定名字的学生,删除时需要在Service层调用是添加事务注解@Transactional
    Long deleteByName(String name);
 
    // 统计指定名字学生的数量
    Long countByName(String name);
 
}

jpa关键词查询是通过方法名称关键字的搭配,底层生成sql的方式来实现与数据库的交互,其关键词的搭配又很多方式,基本能覆盖表查询的所又情况,其余关键字查询可以自行百度;

3、使用 sql 增删改查

jpa同样支持写sql语句来操作数据,sql 有两种呈现形式:

  • JPQL 形式的 sql 语句,from 后面是以类名呈现的。
  • 原生的 sql 语句,需要使用 nativeQuery = true 指定使用原生 sql

示例代码

public interface ClassRoomRepository extends JpaRepository<ClassRoom,Integer> {
 
    //使用的 JPQL 的 sql 形式 from 后面是类名
    // ?1 代表是的是方法中的第一个参数
    @Query("select s from ClassRoom s where s.name =?1")
    List<ClassRoom> findClassRoom1(String name);
 
    //这是使用正常的 sql 语句去查询
    // :name 是通过 @Param 注解去确定的
    @Query(nativeQuery = true,value = "select * from class_room c where c.name =:name")
    List<ClassRoom> findClassRoom2(@Param("name")String name);
}

sql 中的参数传递也有两种形式:

  1. 使用问号 ?,紧跟数字序列,数字序列从1 开始,如 ?1 接收第一个方法参数的值。

  1. 使用冒号:,紧跟参数名,参数名是通过@Param 注解来确定。

  1. 使用 Sort 来对数据进行一个排序

实例化Sort

 public List<Teacher> getTeachers(String subject) {
        // 第一种方法实例化出 Sort 类,根据年龄进行升序排列
        Sort sort1 = Sort.by(Sort.Direction.ASC, "age");
 
        //定义多个字段的排序
        Sort sort2 = Sort.by(Sort.Direction.DESC, "id", "age");
 
        // 通过实例化 Sort.Order 来排序多个字段
        List<Sort.Order> orders = new ArrayList<>();
        Sort.Order order1 = new Sort.Order(Sort.Direction.DESC, "id");
        Sort.Order order2 = new Sort.Order(Sort.Direction.DESC, "age");
        orders.add(order1);
        orders.add(order2);
        Sort sort3 = Sort.by(orders);
 
        //可以传不同的 sort1,2,3 去测试效果
        return teacherRepositoty.getTeachers(subject, sort1);
    }

Dao层多传一个参数Sort

public interface TeacherRepositoty extends JpaRepository<Teacher,Integer> {
 
    // 正常使用,只是多加了一个 sort 参数而已
    @Query("select t from Teacher t where t.subject = ?1")
    List<Teacher> getTeachers(String subject, Sort sort);
}
  1. jpa 的分页操作

实例化 PageRequest (PageRequest是Pageable接口的实现类)

public Page<Teacher> getPage(@PathVariable("subject") String subject) {
        // 第一种方法实例化 Pageable
        Pageable pageable1 = PageRequest.of(1, 2);
    
        //第二种实例化 Pageable
        Sort sort = Sort.by(Sort.Direction.ASC, "age");
        Pageable pageable2 = PageRequest.of(1, 2, sort);
 
        //第三种实例化 Pageable
        Pageable pageable3 = PageRequest.of(1, 2, Sort.Direction.DESC, "age");
    
        //可以传入不同的 Pageable,测试效果
        Page page = teacherRepositoty.getPage(subject, pageable3);
        System.out.println(page.getTotalElements());
        System.out.println(page.getTotalPages());
        System.out.println(page.hasNext());
        System.out.println(page.hasPrevious());
        System.out.println(page.getNumberOfElements());
        System.out.println(page.getSize());
        return page;
    }

PageRequest 一共有三个可以实例化的静态方法:

  • public static PageRequest of(int page, int size)
  • public static PageRequest of(int page, int size, Sort sort) 分页的同时还可以针对分页后的结果进行一个排序。
  • public static PageRequest of(int page, int size, Direction direction, String… properties) 直接针对字段进行排序。

Dao层多传一个参数Pageable

public interface TeacherRepositoty extends JpaRepository<Teacher,Integer> {
 
    //正常使用,只是多加了一个 Pageable 参数而已
    @Query("select t from Teacher t where t.subject = :subject")
    Page<Teacher> getPage(@Param("subject") String subject, Pageable pageable);
}

五、jpa 使用 Specification

  1. 接口继承JpaSpecificationExecutor

public interface TeacherRepositoty extends JpaRepository<Teacher,Integer> , JpaSpecificationExecutor {
}
 
//JpaSpecificationExecutor提供了如下的几个方法供我们使用 方法参数:Specification
public interface JpaSpecificationExecutor<T> {
 
    Optional<T> findOne(@Nullable Specification<T> var1);
 
    List<T> findAll(@Nullable Specification<T> var1);
 
    Page<T> findAll(@Nullable Specification<T> var1, Pageable var2);
 
    List<T> findAll(@Nullable Specification<T> var1, Sort var2);
 
    long count(@Nullable Specification<T> var1);
 
}
  1. 实例化 Specification

Specification 是一个函数式接口,里面有一个抽象的方法:

 Predicate toPredicate(Root<T> var1, CriteriaQuery<?> var2, CriteriaBuilder var3);

参数说明:

  • Predicate 是用来建立 where 后的查寻条件的相当于上述sql语句的 age > 20。
  • Root 使用来定位具体的查询字段,比如 root.get(“age”) ,定位 age字段,
  • CriteriaBuilder是用来构建一个字段的范围,相当于 > ,= ,<,and …. 等等
  • CriteriaQuery 可以用来构建整个 sql 语句,可以指定sql 语句中的 select 后的查询字段,也可以拼接 where , groupby 和 having 等复杂语句。

代码示例

//多条件查询并排序
public List<Entity类> queryBanner(Integer location) {
        return findAll(
                //实例化 Specification 类
                (root, query, criteriaBuilder) -> {
                    // 构建查询条件
                    Predicate predicate = criteriaBuilder.equal(root.get(“数据库字段”),与数据库字段比较的参数 );
                    // 使用 and 连接上一个条件
                    predicate = criteriaBuilder.and(
                            predicate,
                            criteriaBuilder.equal(root.get(数据库字段), 与数据库字段比较的参数));
                    return predicate;
                },
            //传入Sort排序参数进行排序
            Sort.by(Sort.Order.desc("数据库字段")));
    }
  1. Jpa的Projection (投影映射)

作用:将从查询到的数据封装自定一的接口中;

自定义接口:

public interface TeacherProjection {
 
    String getName();
 
    Integer getAge();
 
    @Value("#{target.name +' and age is' + target.age}")
    String getTotal();
}

接口中的方法以 get 开头 + 属性名,属性名首字母大写, 例如 getName(), 也可以通过 @Value 注解中使用 target.属性名获取属性,也可以把多个属性值拼接成一个字符串。

定义好一个接口后,在查询方法中指定返回接口类型的数据即可;

 

 1.核心方法

  • 查询所有数据 findAll()
  • 修改 添加数据  S save(S entity)
  • 分页查询 Page<S> findAll(Example<S> example, Pageable pageable)
  • 根据id查询 findOne()
  • 根据实体类属性查询: findByProperty (type Property); 例如:findByAge(int age)
  • 删除 void delete(T entity)
  • 计数 查询 long count() 或者 根据某个属性的值查询总数 countByAge(int age)
  • 是否存在   boolean existsById(ID primaryKey)

2.查询关键字

-and

And 例如:findByUsernameAndPassword(String user, Striang pwd);

-or
Or 例如:findByUsernameOrAddress(String user, String addr);

-between
Between 例如:SalaryBetween(int max, int min);

-"<"
LessThan 例如: findBySalaryLessThan(int max);

-">"
GreaterThan 例如: findBySalaryGreaterThan(int min);

-is null
IsNull 例如: findByUsernameIsNull();

-is not null
IsNotNull NotNull 与 IsNotNull 等价 例如: findByUsernameIsNotNull();

-like
Like 例如: findByUsernameLike(String user);

-not like
NotLike 例如: findByUsernameNotLike(String user);

-order by
OrderBy 例如: findByUsernameOrderByNameAsc(String user);直接通过name正序排序

-"!="
Not 例如: findByUsernameNot(String user);

-in
In 例如: findByUsernameIn(Collection<String> userList) ,方法的参数可以是 Collection 类型,也可以是数组或者不定长参数;

-not in
NotIn 例如: findByUsernameNotIn(Collection<String> userList) ,方法的参数可以是 Collection 类型,也可以是数组或者不定长参数;

-Top/Limit
查询方法结果的数量可以通过关键字来限制,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);
  • 详细查询语法
关键词示例对应的sql片段

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 (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)

… 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.firstame) = UPPER(?1)

 3.内置方法

  • Sort_排序
Sort sort =new Sort(Sort.Direction.ASC,"id");
//其中第一个参数表示是降序还是升序(此处表示升序)
//第二个参数表示你要按你的 entity(记住是entity中声明的变量,不是数据库中表对应的字段)中的那个变量进行排序
  • PageRequest_分页
PageRequest pageRequest = new PageRequest(index, num, sort);
//index偏移量 num查询数量 sort排序

    分页+排序实现:

DemoBean demoBean = new DemoBean();
demoBean.setAppId(appId); //查询条件
//创建查询参数
Example<DemoBean> example = Example.of(demoBean);
//获取排序对象
Sort sort = new Sort(Sort.Direction.DESC, "id");
//创建分页对象
PageRequest pageRequest = new PageRequest(index, num, sort);
//分页查询
return demoRepository.findAll(example, pageRequest).getContent();
  • Example_实例查询

    创建一个ExampleMatcher对象,最后再用Example的of方法构造相应的Example对象并传递给相关查询方法。我们看看Spring的例子。

Person person = new Person();                          
person.setFirstname("Dave");  //Firstname = 'Dave'                          
 
ExampleMatcher matcher = ExampleMatcher.matching()                     
                        .withMatcher("name", GenericPropertyMatchers.startsWith()) //姓名采用“开始匹配”的方式查询
                          .withIgnorePaths("int");  //忽略属性:是否关注。因为是基本类型,需要忽略掉
 
Example<Person> example = Example.of(person, matcher);  //Example根据域对象和配置创建一个新的ExampleMatcher  

    ExampleMatcher用于创建一个查询对象,上面的代码就创建了一个查询对象。withIgnorePaths方法用来排除某个属性的查询。withIncludeNullValues方法让空值也参与查询,就是我们设置了对象的姓,而名为空值.

 

1、概念定义:

    上面例子中,是这样创建“实例”的:Example<Customer> ex = Example.of(customer, matcher);我们看到,Example对象,由customer和matcher共同创建。

    A、实体对象:在持久化框架中与Table对应的域对象,一个对象代表数据库表中的一条记录,如上例中Customer对象。在构建查询条件时,一个实体对象代表的是查询条件中的“数值”部分。如:要查询名字是“Dave”的客户,实体对象只能存储条件值“Dave”。

    B、匹配器:ExampleMatcher对象,它是匹配“实体对象”的,表示了如何使用“实体对象”中的“值”进行查询,它代表的是“查询方式”,解释了如何去查的问题。如:要查询FirstName是“Dave”的客户,即名以“Dave"开头的客户,该对象就表示了“以什么开头的”这个查询方式,如上例中:withMatcher("name", GenericPropertyMatchers.startsWith())

    C、实例:即Example对象,代表的是完整的查询条件。由实体对象(查询条件值)和匹配器(查询方式)共同创建。

    再来理解“实例查询”,顾名思义,就是通过一个例子来查询。要查询的是Customer对象,查询条件也是一个Customer对象,通过一个现有的客户对象作为例子,查询和这个例子相匹配的对象。

 

2、特点及约束(局限性):

    A、支持动态查询。即支持查询条件个数不固定的情况,如:客户列表中有多个过滤条件,用户使用时在“地址”查询框中输入了值,就需要按地址进行过滤,如果没有输入值,就忽略这个过滤条件。对应的实现是,在构建查询条件Customer对象时,将address属性值置具体的条件值或置为null。

    B、不支持过滤条件分组。即不支持过滤条件用 or(或) 来连接,所有的过滤查件,都是简单一层的用 and(并且) 连接。

    C、仅支持字符串的开始/包含/结束/正则表达式匹配 和 其他属性类型的精确匹配。查询时,对一个要进行匹配的属性(如:姓名 name),只能传入一个过滤条件值,如以Customer为例,要查询姓“刘”的客户,“刘”这个条件值就存储在表示条件对象的Customer对象的name属性中,针对于“姓名”的过滤也只有这么一个存储过滤值的位置,没办法同时传入两个过滤值。正是由于这个限制,有些查询是没办法支持的,例如要查询某个时间段内添加的客户,对应的属性是 addTime,需要传入“开始时间”和“结束时间”两个条件值,而这种查询方式没有存两个值的位置,所以就没办法完成这样的查询。

 

3、ExampleMatcher的使用 :

  • 一些问题:

(1)Null值的处理。当某个条件值为Null,是应当忽略这个过滤条件呢,还是应当去匹配数据库表中该字段值是Null的记录?
(2)基本类型的处理。如客户Customer对象中的年龄age是int型的,当页面不传入条件值时,它默认是0,是有值的,那是否参与查询呢?
(3)忽略某些属性值。一个实体对象,有许多个属性,是否每个属性都参与过滤?是否可以忽略某些属性?
(4)不同的过滤方式。同样是作为String值,可能“姓名”希望精确匹配,“地址”希望模糊匹配,如何做到?

(5)大小写匹配。字符串匹配时,有时可能希望忽略大小写,有时则不忽略,如何做到?

  • 一些方法:

1、关于基本数据类型。
实体对象中,避免使用基本数据类型,采用包装器类型。如果已经采用了基本类型,

而这个属性查询时不需要进行过滤,则把它添加到忽略列表(ignoredPaths)中。

2、Null值处理方式。

默认值是 IGNORE(忽略),即当条件值为null时,则忽略此过滤条件,一般业务也是采用这种方式就可满足。当需要查询数据库表中属性为null的记录时,可将值设为INCLUDE,这时,对于不需要参与查询的属性,都必须添加到忽略列表(ignoredPaths)中,否则会出现查不到数据的情况。

3、默认配置、特殊配置。

默认创建匹配器时,字符串采用的是精确匹配、不忽略大小写,可以通过操作方法改变这种默认匹配,以满足大多数查询条件的需要,如将“字符串匹配方式”改为CONTAINING(包含,模糊匹配),这是比较常用的情况。对于个别属性需要特定的查询方式,可以通过配置“属性特定查询方式”来满足要求。

4、非字符串属性

如约束中所谈,非字符串属性均采用精确匹配,即等于。

5、忽略大小写的问题。

忽略大小的生效与否,是依赖于数据库的。例如 MySql 数据库中,默认创建表结构时,字段是已经忽略大小写的,所以这个配置与否,都是忽略的。如果业务需要严格区分大小写,可以改变数据库表结构属性来实现,具体可百度。

  • 一些例子:

综合使用:

//创建查询条件数据对象
Customer customer = new Customer();
customer.setName("zhang");
customer.setAddress("河南省");
customer.setRemark("BB");

//创建匹配器,即如何使用查询条件
ExampleMatcher matcher = ExampleMatcher.matching() //构建对象
        .withStringMatcher(StringMatcher.CONTAINING) //改变默认字符串匹配方式:模糊查询
        .withIgnoreCase(true) //改变默认大小写忽略方式:忽略大小写
        .withMatcher("address", GenericPropertyMatchers.startsWith()) //地址采用“开始匹配”的方式查询
        .withIgnorePaths("focus");  //忽略属性:是否关注。因为是基本类型,需要忽略掉

//创建实例
Example<Customer> ex = Example.of(customer, matcher); 

//查询
List<Customer> ls = dao.findAll(ex);

查询null值:

//创建查询条件数据对象
Customer customer = new Customer();

//创建匹配器,即如何使用查询条件
ExampleMatcher matcher = ExampleMatcher.matching() //构建对象
        .withIncludeNullValues() //改变“Null值处理方式”:包括
        .withIgnorePaths("id","name","sex","age","focus","addTime","remark","customerType");  //忽略其他属性

//创建实例
Example<Customer> ex = Example.of(customer, matcher); 

//查询
List<Customer> ls = dao.findAll(ex);

三.Spring data jpa 注解

1.Repository注解

@Modifying //做update操作时需要添加

@Query // 自定义Sql

@Query(value = "SELECT * FROM USERS WHERE X = ?1", nativeQuery = true)
  User findByEmailAddress(String X);
@Query("select u from User u where u.firstname = :firstname") //不加nativeQuery应使用HQL
  User findByLastnameOrFirstname(@Param("lastname") String lastname);

@Transactional //事务

@Async //异步操作

 

2.Entity注解

@Entity //不写@Table默认为user
@Table(name="t_user") //自定义表名
public class user {
 
    @Id //主键
    @GeneratedValue(strategy = GenerationType.AUTO)//采用数据库自增方式生成主键
    //JPA提供的四种标准用法为TABLE,SEQUENCE,IDENTITY,AUTO.
    //TABLE:使用一个特定的数据库表格来保存主键。
    //SEQUENCE:根据底层数据库的序列来生成主键,条件是数据库支持序列。
    //IDENTITY:主键由数据库自动生成(主要是自动增长型)
    //AUTO:主键由程序控制。
 
    @Transient //此字段不与数据库关联
    @Version//此字段加上乐观锁
    //字段为name,不允许为空,用户名唯一
    @Column(name = "name", unique = true, nullable = false)
    private String name;
 
    @Temporal(TemporalType.DATE)//生成yyyy-MM-dd类型的日期
    //出参时间格式化
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    //入参时,请求报文只需要传入yyyymmddhhmmss字符串进来,则自动转换为Date类型数据
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm")
    private Date createTime;
 
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

四.继承JpaSpecificationExecutor接口进行复杂查询

spring data jpa 通过创建方法名来做查询,只能做简单的查询,那如果我们要做复杂一些的查询呢,多条件分页怎么办,这里,spring data jpa为我们提供了JpaSpecificationExecutor接口,只要简单实现toPredicate方法就可以实现复杂的查询

参考:https://www.cnblogs.com/happyday56/p/4661839.html

1.首先让我们的接口继承于JpaSpecificationExecutor

public interface TaskDao extends JpaSpecificationExecutor<Task>{
}

2.JpaSpecificationExecutor提供了以下接口

public interface JpaSpecificationExecutor<T> {
 
    T findOne(Specification<T> spec);
 
    List<T> findAll(Specification<T> spec);
 
    Page<T> findAll(Specification<T> spec, Pageable pageable);
 
    List<T> findAll(Specification<T> spec, Sort sort);
 
    long count(Specification<T> spec);
}
 
//其中Specification就是需要我们传入查询方法的参数,它是一个接口 
 
public interface Specification<T> {
 
    Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb);
}

提供唯一的一个方法toPredicate,我们只要按照JPA 2.0 criteria api写好查询条件就可以了,关于JPA 2.0 criteria api的介绍和使用,欢迎参考 
http://blog.csdn.net/dracotianlong/article/details/28445725 

http://developer.51cto.com/art/200911/162722.htm

3.接下来我们在service bean

@Service
public class TaskService {
 
    @Autowired TaskDao taskDao ;
 
 
    /**
     * 复杂查询测试
     * @param page
     * @param size
     * @return
     */
    public Page<Task> findBySepc(int page, int size){
 
        PageRequest pageReq = this.buildPageRequest(page, size);
        Page<Task> tasks = this.taskDao.findAll(new MySpec(), pageReq);
        //传入了new MySpec() 既下面定义的匿名内部类 其中定义了查询条件
        return tasks;
 
    }
 
     /**
      * 建立分页排序请求 
      * @param page
      * @param size
      * @return
      */
     private PageRequest buildPageRequest(int page, int size) {
           Sort sort = new Sort(Direction.DESC,"createTime");
           return new PageRequest(page,size, sort);
     }
 
    /**
     * 建立查询条件
     */
    private class MySpec implements Specification<Task>{
 
        @Override
        public Predicate toPredicate(Root<Task> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
 
     //1.混合条件查询
          Path<String> exp1 = root.get("taskName");
            Path<Date>  exp2 = root.get("createTime");
            Path<String> exp3 = root.get("taskDetail");
            Predicate predicate = cb.and(cb.like(exp1, "%taskName%"),cb.lessThan(exp2, new Date()));
            return cb.or(predicate,cb.equal(exp3, "kkk"));
 
           /* 类似的sql语句为:
            Hibernate: 
                select
                    count(task0_.id) as col_0_0_ 
                from
                    tb_task task0_ 
                where
                    (
                        task0_.task_name like ?
                    ) 
                    and task0_.create_time<? 
                    or task0_.task_detail=?
            */
 
    //2.多表查询
        Join<Task,Project> join = root.join("project", JoinType.INNER);
            Path<String> exp4 = join.get("projectName");
            return cb.like(exp4, "%projectName%");
 
           /* Hibernate: 
            select
                count(task0_.id) as col_0_0_ 
            from
                tb_task task0_ 
            inner join
                tb_project project1_ 
                    on task0_.project_id=project1_.id 
            where
                project1_.project_name like ?*/ 
           return null ;  
        }
    }
}

 

4.实体类task代码如下

@Entity
@Table(name = "tb_task")
public class Task {
 
    private Long id ;
    private String taskName ;
    private Date createTime ;
    private Project project;
    private String taskDetail ;
 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
 
    @Column(name = "task_name")
    public String getTaskName() {
        return taskName;
    }
    public void setTaskName(String taskName) {
        this.taskName = taskName;
    }
 
    @Column(name = "create_time")
    @DateTimeFormat(pattern = "yyyy-MM-dd hh:mm:ss")
    public Date getCreateTime() {
        return createTime;
    }
    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }
 
 
    @Column(name = "task_detail")
    public String getTaskDetail() {
        return taskDetail;
    }
    public void setTaskDetail(String taskDetail) {
        this.taskDetail = taskDetail;
    }
 
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "project_id")
    public Project getProject() {
        return project;
    }
    public void setProject(Project project) {
        this.project = project;
    } 
}

通过重写toPredicate方法,返回一个查询 Predicate,spring data jpa会帮我们进行查询。
 

也许你觉得,每次都要写一个类来实现Specification很麻烦,那或许你可以这么写

public class TaskSpec {
 
    public static Specification<Task> method1(){
 
        return new Specification<Task>(){
            @Override
            public Predicate toPredicate(Root<Task> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                return null;
            } 
        };
    }
 
    public static Specification<Task> method2(){
 
        return new Specification<Task>(){
            @Override
            public Predicate toPredicate(Root<Task> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                return null;
            } 
        };
    } 
}

那么用的时候,我们就这么用

Page<Task> tasks = this.taskDao.findAll(TaskSpec.method1(), pageReq);

五.Spring data jpa + QueryDSL 进行复杂查询

  1. QueryDSL仅仅是一个通用的查询框架,专注于通过Java API构建类型安全的SQL查询。
  2. Querydsl可以通过一组通用的查询API为用户构建出适合不同类型ORM框架或者是SQL的查询语句,也就是说QueryDSL是基于各种ORM框架以及SQL之上的一个通用的查询框架。
  3. 借助QueryDSL可以在任何支持的ORM框架或者SQL平台上以一种通用的API方式来构建查询。目前QueryDSL支持的平台包括JPA,JDO,SQL,Java Collections,RDF,Lucene,Hibernate Search。

P.s.配置可以根据官网介绍来配置

1 实体类

城市类:

@Entity
@Table(name = "t_city", schema = "test", catalog = "")
public class TCity {
    //省略JPA注解标识
    private int id;
    private String name;
    private String state;
    private String country;
    private String map;
}

旅馆类:

@Entity
@Table(name = "t_hotel", schema = "test", catalog = "")
public class THotel {
    //省略JPA注解标识
    private int id;
    private String name;
    private String address;
    private Integer city;//保存着城市的id主键
}

2 单表动态分页查询

Spring Data JPA中提供了QueryDslPredicateExecutor接口,用于支持QueryDSL的查询操作

public interface tCityRepository extends JpaRepository<TCity, Integer>, QueryDslPredicateExecutor<TCity> {
}

这样的话单表动态查询就可以参考如下代码:

//查找出Id小于3,并且名称带有`shanghai`的记录.
 
//动态条件
QTCity qtCity = QTCity.tCity; //SDL实体类
//该Predicate为querydsl下的类,支持嵌套组装复杂查询条件
Predicate predicate = qtCity.id.longValue().lt(3).and(qtCity.name.like("shanghai"));
//分页排序
Sort sort = new Sort(new Sort.Order(Sort.Direction.ASC,"id"));
PageRequest pageRequest = new PageRequest(0,10,sort);
//查找结果
Page<TCity> tCityPage = tCityRepository.findAll(predicate,pageRequest);

3 多表动态查询

QueryDSL对多表查询提供了一个很好地封装,看下面代码:

/**
     * 关联查询示例,查询出城市和对应的旅店
     * @param predicate 查询条件
     * @return 查询实体
     */
    @Override
    public List<Tuple> findCityAndHotel(Predicate predicate) {
        JPAQueryFactory queryFactory = new JPAQueryFactory(em);
        JPAQuery<Tuple> jpaQuery = queryFactory.select(QTCity.tCity,QTHotel.tHotel)
                                        .from(QTCity.tCity)
                                        .leftJoin(QTHotel.tHotel)
                                        .on(QTHotel.tHotel.city.longValue().eq(QTCity.tCity.id.longValue()));
        //添加查询条件
        jpaQuery.where(predicate);
        //拿到结果
        return jpaQuery.fetch();
    }

城市表左连接旅店表,当该旅店属于这个城市时查询出两者的详细字段,存放到一个Tuple的多元组中.相比原生sql,简单清晰了很多.
那么该怎么调用这个方法呢?

Test
    public void findByLeftJoin(){
        QTCity qtCity = QTCity.tCity;
        QTHotel qtHotel = QTHotel.tHotel;
        //查询条件
        Predicate predicate = qtCity.name.like("shanghai");
        //调用
        List<Tuple> result = tCityRepository.findCityAndHotel(predicate);
        //对多元组取出数据,这个和select时的数据相匹配
        for (Tuple row : result) {
            System.out.println("qtCity:"+row.get(qtCity));
            System.out.println("qtHotel:"+row.get(qtHotel));
            System.out.println("--------------------");
        }
        System.out.println(result);
    }

这样做的话避免了返回Object[]数组,下面是自动生成的sql语句:

select
        tcity0_.id as id1_0_0_,
        thotel1_.id as id1_1_1_,
        tcity0_.country as country2_0_0_,
        tcity0_.map as map3_0_0_,
        tcity0_.name as name4_0_0_,
        tcity0_.state as state5_0_0_,
        thotel1_.address as address2_1_1_,
        thotel1_.city as city3_1_1_,
        thotel1_.name as name4_1_1_ 
    from
        t_city tcity0_ 
    left outer join
        t_hotel thotel1_ 
            on (
                cast(thotel1_.city as signed)=cast(tcity0_.id as signed)
            ) 
    where
        tcity0_.name like ? escape '!'

 

4 多表动态分页查询

分页查询对于queryDSL无论什么样的sql只需要写一遍,会自动转换为相应的count查询,也就避免了文章开始的问题4,下面代码是对上面的查询加上分页功能:

@Override
 public QueryResults<Tuple> findCityAndHotelPage(Predicate predicate,Pageable pageable) {
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);
    JPAQuery<Tuple> jpaQuery = queryFactory.select(QTCity.tCity.id,QTHotel.tHotel)
                                               .from(QTCity.tCity)
                                               .leftJoin(QTHotel.tHotel)
                                               .on(QTHotel.tHotel.city.longValue().eq(QTCity.tCity.id.longValue()))
                                               .where(predicate)
                                               .offset(pageable.getOffset())
                                               .limit(pageable.getPageSize());
    //拿到分页结果
    return jpaQuery.fetchResults();

和上面不同之处在于这里使用了offsetlimit限制查询结果.并且返回一个QueryResults,该类会自动实现count查询和结果查询,并进行封装.调用形式如下:

@Test
    public void findByLeftJoinPage(){
        QTCity qtCity = QTCity.tCity;
        QTHotel qtHotel = QTHotel.tHotel;
        //条件
        Predicate predicate = qtCity.name.like("shanghai");
        //分页
        PageRequest pageRequest = new PageRequest(0,10);
        //调用查询
        QueryResults<Tuple> result = tCityRepository.findCityAndHotelPage(predicate,pageRequest);
        //结果取出
        for (Tuple row : result.getResults()) {
            System.out.println("qtCity:"+row.get(qtCity));
            System.out.println("qtHotel:"+row.get(qtHotel));
            System.out.println("--------------------");
        }
        //取出count查询总数
        System.out.println(result.getTotal());
    }

生成的原生count查询sql,当该count查询结果为0的话,则直接返回,并不会再进行具体数据查询:

select
    count(tcity0_.id) as col_0_0_ 
from
    t_city tcity0_ 
left outer join
    t_hotel thotel1_ 
        on (
            cast(thotel1_.city as signed)=cast(tcity0_.id as signed)
        ) 
where
    tcity0_.name like ? escape '!'

生成的原生查询sql:

select
    tcity0_.id as id1_0_0_,
    thotel1_.id as id1_1_1_,
    tcity0_.country as country2_0_0_,
    tcity0_.map as map3_0_0_,
    tcity0_.name as name4_0_0_,
    tcity0_.state as state5_0_0_,
    thotel1_.address as address2_1_1_,
    thotel1_.city as city3_1_1_,
    thotel1_.name as name4_1_1_ 
from
    t_city tcity0_ 
left outer join
    t_hotel thotel1_ 
        on (
            cast(thotel1_.city as signed)=cast(tcity0_.id as signed)
        ) 
where
    tcity0_.name like ? escape '!' limit ?

查看打印,可以发现对应的city也都是同一个对象,hotel是不同的对象.

 

5 改造

有了上面的经验,改造就变得相当容易了.
首先前面的一堆sql可以写成如下形式,无非是多了一些select和left join

JPAQueryFactory factory = new JPAQueryFactory(entityManager);
        factory.select($.pcardCardOrder)
               .select($.pcardVcardMake.vcardMakeDes)
               .select($.pcardVtype.cardnumRuleId,$.pcardVtype.vtypeNm)
               .select($.pcardCardbin)
               .leftJoin($.pcardVcardMake).on($.pcardCardOrder.makeId.eq($.pcardVcardMake.vcardMakeId))
               //......省略

查询条件使用Predicate代替,放在service拼接,或者写一个生产条件的工厂都可以.

jpaQuery.where(predicate);

最后的分页处理就和之前的一样了

jpaQuery.offset(pageable.getOffset()).limit(pageable.getPageSize());
return jpaQuery.fetchResults();

写在最后:

    个人认为jpa的意义就在于少用原生sql 为了方便开发 封装已经是在所难免了. 推荐多使用简单查询,需要使用动态查询的时候推荐使用JpaSpecificationExecutor个人认为比较好用.

    虽然我还是喜欢原生的写法...

另外很多时候简单的条件可以在server层进行判断调用不同的Dao层方法就可以。

 

posted @ 2024-01-05 14:45  爵岚  阅读(6700)  评论(0编辑  收藏  举报