JPA 概述及常用注解详解、SpringDataJpa 使用指南

本文涉及的产品
云数据库 RDS SQL Server,独享型 2核4GB
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: JPA 概述及常注解详解、SpringDataJpa 使用指南

概述

JPA官方文档:https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#preface

JPA(Java Persistence API)是 Java 标准中的一套ORM规范(提供了一些编程的 API 接口,具体实现由 ORM 厂商实现,如Hiernate、TopLink 、Eclipselink等都是 JPA 的具体实现),借助 JPA 技术可以通过注解或者XML描述【对象-关系表】之间的映射关系,并将实体对象持久化到数据库中(即Object Model与Data Model间的映射)。

JPA是Java持久层API,由Sun公司开发,希望规范、简化Java对象的持久化工作,整合ORM技术,整合第三方ORM框架,建立一种标准的方式,目前也是在按照这个方向发展,但是还没能完全实现。在ORM框架中,Hibernate框架做了较好的 JPA 实现,已获得Sun的兼容认证。

JPA 的优势:

1.开发者面向 JPA 规范的接口,但底层的JPA实现可以任意切换:觉得Hibernate好的,可以选择Hibernate JPA实现;觉得TopLink好的,可以选择TopLink JPA实现。

2.开发者可以避免为使用Hibernate学习一套ORM框架,为使用TopLink又要再学习一套ORM框架。

JPA为我们提供了以下规范:

  • ORM映射元数据:JPA支持XML和注解两种元数据的形式,元数据描述对象和表之间的映射关系,框架据此将实体对象持久化到数据库表中
  • JPA的Criteria API:提供API来操作实体对象,执行CRUD操作,框架会自动将之转换为对应的SQL,使开发者从繁琐的 JDBC、SQL中解放出来。
  • JPQL查询语言:通过面向对象而非面向数据库的查询语言查询数据,避免程序的SQL语句紧密耦合。


Hibernate介绍

Hibernate对数据库结构提供了较为完整的封装,Hibernate的O/R Mapping实现了POJO 和数据库表之间的映射,以及SQL 的自动生成和执行。往往只需定义好了POJO 到数据库表的映射关系,即可通过Hibernate 提供的方法完成持久层操作。甚至不需要对SQL 的熟练掌握, Hibernate/OJB 会根据制定的存储逻辑,自动生成对应的SQL 并调用JDBC 接口加以执行。

Hibernate框架(3.2及以上版本)对JPA接口规范做了较好的实现,主要是通过以下三个组件来实现的:

  • hibernate-annotation:是Hibernate支持annotation方式配置的基础,它包括了标准的 JPA annotation以及Hibernate自身特殊功能的annotation。
  • hibernate-core:是Hibernate的核心实现,提供了Hibernate所有的核心功能。
  • hibernate-entitymanager:实现了标准的 JPA,可以把它看成hibernate-core和 JPA 之间的适配器,它并不直接提供ORM的功能,而是对hibernate-core进行封装,使得Hibernate符合 JPA 的规范。

hibernate对 JPA 的支持,不是另提供了一套专用于 JPA 的注解。一些重要的注解如@Column, @OneToMany等,hibernate并没有提供,这说明 JPA 的注解已经是hibernate 的核心,hibernate只提供了一些补充,而不是两套注解。JPA 和hibernate都提供了的注解(例如@Entity),若 JPA 的注解够用,就直接用,若 JPA 的注解不够用,直接使用hibernate的即可。


Spring Data JPA介绍

Spring Data JPA是在实现了 JPA 规范的基础上封装的一套 JPA 应用框架(Criteria API还是有些复杂)。虽然ORM框架都实现了 JPA 规范,但是在不同的ORM框架之间切换仍然需要编写不同的代码,而使用Spring Data JPA能够方便的在不同的ORM框架之间进行切换而不需要更改代码。Spring Data JPA旨在通过统一ORM框架的访问持久层的操作,来提高开发人的效率。

Spring Data JPA是一个 JPA 数据访问抽象。也就是说Spring Data JPA不是一个实现或 JPA 提供的程序,它只是一个抽象层,主要用于减少为各种持久层存储实现数据访问层所需的样板代码量。但是它还是需要JPA提供实现程序,其实Spring Data JPA底层就是使用的 Hibernate实现。

Spring Data JPA 其实并不依赖于 Spring 框架。

Spring Data JPA 通过Repository来支持上述功能,默认提供的几种Repository已经满足了绝大多数需求:

  • JpaRepository( 为Repository的子接口:JpaRepository -> PagingAndSortingRepository -> CrudRepository -> Repository)
  • QueryByExampleExecutor
  • JpaSpecificationExecutor
  • QuerydslPredicateExecutor

后三者用于更复杂的查询,如动态查询、关联查询等;第一种用得最多,提供基于方法名(query method)的查询,用户可基于第一种继承创建自己的子接口(只要是Repository的子接口即可),并声明各种基于方法名的查询方法。

  • Repository 的实现类:

    • SimpleJpaRepository
  • QueryDslJpaRepository


JPA与springDataJpa、Hibernate 之间的关系

533121-20160122105513781-959577889.png


Querydsl-JPA 介绍

Springdata-JPA是对 JPA 使用的封装,Querydsl-JPA也是基于各种ORM之上的一个通用查询框架,使用它的API类库可以写出“Java代码的sql”,不用去手动接触sql语句,表达含义却如sql般准确。更重要的一点,它能够构建类型安全的查询,这比起 JPA 使用原生查询时有很大的不同,可以不必再对恶心的“Object[]”进行操作了。SpringDataJPA + Querydsl-JPA联合使用方案是使用 JPA 操作数据库的最佳方案,它们之间有着完美的相互支持,以达到更高效的编码。


JPA 注解

Entity 常用注解

参考:JPA & Spring Data JPA学习与使用小记

指定对象与数据库字段映射时注解的位置:如@Id、@Column等注解指定Entity的字段与数据库字段对应关系时,注解的位置可以在Field(属性)或Property(属性的get方法上),两者统一用其中一种,不能两者均有。推荐用前者。

@Entity、@Table

@Entity(必需)

标注在实体类上。

映射实体类。指出该 Java 类为实体类,将映射到指定的关系数据库表。

应用了此注解后,将会自动将类名映射作为数据库表名、将类内的字段名映射为数据库表的列名。映射策略默认是按驼峰命名法拆分将类名或字段名拆分成多部分,然后以下划线连接,如StudentEntity -> student_entity、studentName -> student_name。若不按默认映射,则可通过@Table、@Column指定,见下面。


@Table(可选)

标注在实体类上。

映射数据库表名。当实体类与其映射的数据库表名不同名时需要使用 @Table 标注说明,该标注与 @Entity 标注并列使用

  • schema属性:指定数据库名
  • name属性:指定表名,不指定时默认按驼峰命名法拆分将类名,并以下划线连接


@DynamicInsert、@DynamicUpdate

@DynamicInsert(可选)

标注在实体类上。

设置为true,表示insert对象的时候,生成动态的insert语句,如果这个字段的值是null就不会加入到insert语句中,默认false。


@DynamicUpdate(可选)

标注在实体类上。

设置为true,表示执行update对象时,在生成动态的update语句前,会先查询该表在数据库中的字段值,并对比更新使用的对象中的字段值与数据库中的字段值是否相同,若相同(即该值没有修改),则该字段就不会被加入到update语句中。

默认false,表示无论更新使用实体类中的字段值与数据库中的字段值是否一致,都加入到update语句中,即都使用对象中所有字段的值覆盖数据库中的字段值。

比如只想更新某个属性,但是却把整个属性都更改了,这并不是我们希望的结果,我们希望的结果是:我更改了哪写字段,只要更新我修改的字段就够了。

注意:

@DynamicUpdate的动态更新的含义是,比较更新要使用的实体类中的所有字段值与从数据库中查询出来的所有字段值,判断其是否有修改,不同则加入到update语句中更新字段值。看这个例子,数据库中id=1的记录所有字段都是非空的,但是实体类中只有name有值,也就是所有字段都变了,只是其他字段被更新为了新的空值。

所以 jpa 更新数据库字段值,无论是否有 @DynamicUpdate注解,均需要手动先select对象,然后通过set更新对象的属性值,然后再save对象,实现更新操作


@Id、@GeneratedValue

@Id(必需)

标注在实体类成员变量或getter方法之上。

映射生成主键。用于声明一个实体类的属性映射为数据库的一个主键列。

若同时指定了下面的@GeneratedValue则存储时会自动生成主键值,否则在存入前用户需要手动为实体赋一个主键值。

主键值类型可以是:

  • Primitive types:

    boolean, byte, short, char, int, long, float, double

  • Equivalent wrapper classes from package java.lang:

    Byte,Short, Character, IntegerLong, Float, Double

  • java.math.BigInteger, java.math.BigDecimal
  • java.lang.String
  • java.util.Date, java.sql.Date, java.sql.Time, java.sql.Timestamp
  • Any enum type
  • Reference to an entity object
  • composite of several keys above

指定联合主键,有@IdClass、@EmbeddedId两种方法。


@GeneratedValue

@GeneratedValue 用于标注主键的生成策略,通过 strategy 属性指定。

默认情况下,JPA 自动选择一个最适合底层数据库的主键生成策略:SqlServer 对应 identity,MySQL 对应 auto increment

  • AUTO: JPA自动选择合适的策略,是默认选项
  • IDENTITY:采用数据库 ID自增长的方式来自增主键字段,Oracle 不支持这种方式
  • TABLE:通过表产生主键,框架借由表模拟序列产生主键,使用该策略可以使应用更易于数据库移植。
  • SEQUENCE:通过序列产生主键,通过 @SequenceGenerator 注解指定序列名,MySql 不支持这种方式


@Column、@Basic、@Transient

@Column(可选)

标注在实体类成员变量或getter方法之上。

映射表格列。当实体的属性与其映射的数据库表的列不同名时需要使用 @Column 标注说明。

类的字段名在数据库中对应的字段名可以通过此注解的name属性指定,不指定则默认为将属性名按驼峰命名法拆分并以下划线连接,如 createTime 对应 create_time

注意:即使name的值中包含大写字母,对应到db后也会转成小写,如@ Column(name="create_Time") 在数据库中字段名仍为create_time。可通过SpringBoot配置参数【spring.jpa.hibernate.naming.physical-strategy】配置对应策略,如指定name值是什么,数据库中就对应什么名字的列名。默认值为:【org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy】

@Column注解一共有10个属性,这10个属性均为可选属性,各属性含义分别如下:

  • name:定义了被标注字段在数据库表中所对应字段的名称。
  • unique:该字段是否为唯一标识,默认为false。也可以使用@Table标记中的@UniqueConstraint。
  • nullable:该字段是否可以为null值,默认为true。
  • length:字段的长度,当字段的类型为varchar时,该属性才有效,默认为255个字符。
  • insertable:在使用“INSERT”脚本插入数据时,是否插入该字段的值。默认为true。
  • updatable:在使用“UPDATE”脚本插入数据时,是否更新该字段的值。默认为true。

    insertable = false 和updatable = false 一般多用于只读的属性,例如主键和外键等。这些字段的值通常是自动生成的。

  • columnDefinition:建表时创建该字段的DDL语句,一般用于通过Entity生成表定义时使用。

    (如果DB中表已经建好,该属性没有必要使用)

  • precision和scale:表示精度,当字段类型为double时,precision表示数值的总长度,scale表示小数点所占的位数。
  • table:定义了包含当前字段的表名。


@Basic(可选)

表示一个简单的属性到数据表的字段的映射,对于没有任何标注的属性或getter方法,默认标注为 @Basic

  • fetch 表示属性的读取策略,有 EAGER 和 LAZY 两种,分别为立即加载和延迟加载
  • optional 表示该属性是否允许为 null,默认为 true


@Transient:忽略属性

定义暂态属性。表示该属性并非一个到数据库表的字段的映射,ORM 框架将忽略该属性。

如果一个属性并非数据库表的字段映射,就必须将其标识为 @Transient,否则ORM 框架默认为其注解 @Basic,例如工具方法不需要映射。


@Temporal:时间日期精度

标注在实体类成员变量或getter方法之上。可选。

在 JavaAPI 中没有定义 Date 类型的精度,而在数据库中表示 Date 类型的数据类型有 Date(年月日),Time(时分秒),TimeStamp(年月日时分秒) 三种精度,进行属性映射的时候可以使用 @Temporal 注解调整精度。

目前此注解只能用于修饰JavaAPI中的【java.util.Date】、【java.util.Calendar】类型的变量,TemporalType 取 DATE、TIME、TIMESTAMP 时在MySQL中分别对应的 DATE、TIME、DATETIME 类型。

示例:

    @Temporal(TemporalType.TIMESTAMP)
    @CreationTimestamp //org.hibernate.annotations.CreationTimestamp,用于在JPA执行insert操作时自动更新该字段值
    @Column(name = "create_time", updatable=false )        //为防止手动set,可设false以免该字段被更新
    private Date createTime;
    
    @Temporal(TemporalType.TIMESTAMP)
    @UpdateTimestamp //org.hibernate.annotations.UpdateTimestamp,用于在JPA执行update操作时自动更新该字段值
    private Date updateTime;


时间日期自动更新:

1、Hibernate的注解:

​ @CreationTimestamp(创建时间)、@UpdateTimestamp(更新时间)

​ 用法:在时间日期类型属性上加上注解即可

2、SpringDataJPA的注解:(可参阅https://blog.csdn.net/tianyaleixiaowu/article/details/77931903

​ @CreatedDate(创建时间)、@LastModifiedDate(更新时间)、@CreatedBy、@LastModifiedBy

用法:

  1. 在实体类加上注解 @EntityListeners(AuditingEntityListener.class)
  2. 在启动类上加上注解 @EnableJpaAuditing
  3. 在实体类中属性中加上面四种注解

示例:

@Data
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    protected Integer id;
    
    // 创建时间
    @CreatedDate
    @Column(name = "create_time", updatable=false )        //为防止手动set,可设false以免该字段被更新
    private Long createTime;
    
    // 更新时间
    @LastModifiedDate
    @Column(name = "update_time")
    private Long updateTime;
}


其他注解

@MappedSuperClass:共有字段超类

标注在实体类上。

共有字段超类中声明了各Entity共有的字段,即数据库中多表中共有的字段,如create_time、update_time、id等。

标注为@MappedSuperclass的类将不是一个完整的实体类,将不会映射到数据库表,但是其属性都将映射到其子类的数据库字段中。

标注为@MappedSuperclass的类不能再标注@Entity或@Table注解,也无需实现序列化接口。

允许多级继承

注解的类继承另一个实体类或标注@MappedSuperclass类,可以使用 @AttributeOverride 或 @AttributeOverrides 注解重定义其父类属性映射到数据库表中字段。


@IdClass:指定联合主键类

标注在实体类上。

指定联合主键类。如:@IdClass(StudentExperimentEntityPK.class)

主键类StudentExperimentEntityPK需要满足:

  • 实现Serializable接口
  • 有默认的public无参数的构造方法
  • 重写equals和hashCode方法。equals方法用于判断两个对象是否相同,EntityManger通过find方法来查找Entity时,是根据equals的返回值来判断的。hashCode方法返回当前对象的哈希码
  • 它的类型和名称必须与使用 @Id 进行标注的实体主键字段的类型和名称一致。

示例:

/** 
 * 实体类
 */
@Data
@Entity
@Table(name = "customer_course")
@IdClass(CustomerCourseEntityPK.class)    // 指定联合主键类
public class CustomerCourseEntity {
    @Id
    @Column(name = "customer_id", length = ColumnLengthConstrain.LEN_ID_MAX)
    private String customerId;
    
    @Id
    @Column(name = "course_id", length = ColumnLengthConstrain.LEN_ID_MAX)
    private String courseId;
    
    @Column(name = "max_number")
    private Integer maxNumber;
    
    @ManyToOne
    @JoinColumn(name = "course_id", referencedColumnName = "id", nullable = false, insertable = false, updatable = false)
    private CourseEntity courseByCourseId;
}
    

/** 
 * 联合主键类
 */
@Data
public class CustomerCourseEntityPK implements Serializable {
    private static final long serialVersionUID = 1L;

    private String customerId;

    private String courseId;
}


@EmbeddedId:联合主键

标注在实体类成员变量或getter方法上。

功能与@IdClass一样用于指定联合主键。不同的是其标注在实体内的主键类变量上,且主键类应该标注 @Embeddable 注解。

此外在主键类内指定的字段在实体类内可以不再指定,若再指定则需为@Column加上insertable = false, updatable = false属性

示例:

@Data
@Entity
@Table(name = "customer_course")
@IdClass(CustomerCourseEntityPK.class)    // 指定联合主键类
public class CustomerCourseEntity {
    
    @EmbeddedId
    private CustomerCourseEntityPK id;
    
    @Column(name = "max_number")
    private Integer maxNumber;
}
    

/** 
 * 联合主键类
 */
@Data
@Embeddable
public class CustomerCourseEntityPK implements Serializable {
    private static final long serialVersionUID = 1L;
    
    @Column(name = "customer_id", length = ColumnLengthConstrain.LEN_ID_MAX)
    private String customerId;
    
    @Column(name = "course_id", length = ColumnLengthConstrain.LEN_ID_MAX)
    private String courseId;
}


@Inheritance:表结构复用

标注在实体类上。

用于表结构复用。指定被该注解修饰的类被子类继承后子类和父类的表结构的关系。

通过strategy属性指定关系,有三种策略:

  1. SINGLE_TABLE:适用于共同字段多独有字段少的关联关系定义。子类和父类对应同一个表且所有字段在一个表中,还会自动生成(也可通过 @DiscriminatorColumn 指定)一个字段 varchar 'dtype' 用来表示一条数据是属于哪个实体的。为默认值。(未使用@Inheritance或使用了但没指定strategy属性时默认采用此策略)。
  2. JOINED:子类和父类对应不同表,父类属性对应的列(除了主键)不会且无法再出现在子表中。子表自动产生与父表主键对应的外键与父表关联。同样地也可通过@DiscriminatorColumn为父类指定一个字段用于标识一条记录属于哪个子类。
  3. TABLE_PER_CLASS:子类和父类对应不同表且各类自己的所有字段(包括继承的)分别都出现在各自的表中;表间没有任何外键关联。此策略最终效果与@MappedSuperClass等同。


@Inheritance与@MappedSuperclass的区别:

  • @MappedSuperclass子类与父类没有外键关系、不会对应同一个表
  • @Inheritance适用于表关联后者适用于定义公共字段
  • 两者是可以混合使用


@Inheritance、@MappedSuperClass可用于定义Inheritance关系。这些方式的一个缺点是子类中无法覆盖从父类继承的字段的定义(如父类中name是not null,但子类中允许为null)。

除了 @Inheritance、@MappedSuperClass外,还有一种Inheritance方法(此法可解决上述不足):先定义一个Java POJO(干净的POJO,没有任何对该类使用任何的ORM注解),然后不同子类继承该父类,并分别在不同子类中进行ORM定义即可。此法下不同子类拥有父类的公共字段且该字段在不同子类中对应的数据库列定义可不同。


@Embedded、@Embeddable

  当一个实体类要在多个不同的实体类中进行使用,而其不需要生成数据库表

  @Embeddable:标注在类上,表示此类是可以被其他类嵌套

  @Embedded:标注在属性上,表示嵌套被@Embeddable注解的同类型类


@Enumerated:映射枚举

  使用此注解映射枚举字段,以String类型存入数据库

  注入数据库的类型有两种:EnumType.ORDINAL(Interger)、EnumType.STRING(String)


@TableGenerator:主键值生成器

TableGenerator定义一个主键值生成器,在 @GeneratedValue的属性strategy = GenerationType.TABLE时,generator属性中可以使用生成器的名字。生成器可以在类、方法或者属性上定义。

生成器是为多个实体类提供连续的ID值的表,每一行为一个类提供ID值,ID值通常是整数。

属性说明:

  • name:生成器的唯一名字,可以被Id元数据使用。
  • table:生成器用来存储id值的Table定义。
  • pkColumnName:生成器表里用来保存主键名字的字段
  • valueColumnName:生成器表里用来保存主键值的字段
  • pkColumnValue:生成器表里用来保存主键名字的字段的值
  • initialValue:id值的初始值。
  • allocationSize:id值的增量

示例:

@Entity
public class Employee {

    @Id
    @Column(name = "id")
    @TableGenerator(name = "hf_opert_id_gen",     // 此处的名字要和下面generator属性值一致
        table = "mcs_hibernate_seq",            // 主键保存到数据库的表名
        pkColumnName = "sequence_name",         // 表里用来保存主键名字的字段
        valueColumnName = "sequence_next_hi_value",        // 表里用来保存主键值的字段
        pkColumnValue = "user_id",     // 表里名字字段对应的值
        allocationSize = 1)            // 自动增长,设置为1
    @GeneratedValue(strategy = GenerationType.TABLE, generator = "hf_opert_id_gen")
    private Integer id;

}


@JoinColumn、@JoinColumns

@JoinColumn:指定外键

如果在实体类的某个属性上定义了联表关系(OneToOne或OneTOMany等),则使用@JoinColumn注解来定义关系的属性。JoinColumn的大部分属性和Column类似。

属性说明:

  • name:主表的列名。若不指定,默认为 关联表的名称 + “_” + 关联表主键的字段名,例如 address_id
  • referencedColumnName:关联表作为外键的列名。若不指定,默认为关联表的主键作为外键。
  • unique:是否唯一 ,默认false
  • nullable:是否允许为空,默认true
  • insertable:是否允许插入,默认true
  • updatable:是否允许更新,默认true
  • columnDefinition:定义建表时创建此列的DDL
  • secondaryTable:从表名。如果此列不建在主表上(默认建在主表),该属性定义该列所在从表的名字。
  • foreignKey():外键。默认@ForeignKey(ConstraintMode.PROVIDER_DEFAULT);
@Data
@Entity
public class Person {
    ...
    
    // Person和Address是一对一关系。Address表中名为id_address的列作为外键指向Person表中名为address_id的列
    @OneToOne
    @JoinColumn(name="address_id", referencedColumnName="id_address", unique=true)
    private Address address;
}


@Data
@Entity
public class Address {
    
    @Id
    @column(name ="id_address")
    private Integer idAddress;
}


@JoinColumns

如果在实体类的某个属性上定义了联表关系(OneToOne或OneTOMany等),并且关系存在多个JoinColumn,则使用@JoinColumns注解定义多个JoinColumn的属性。

属性说明:

  • value:定义JoinColumn数组,指定每个JoinColumn的属性
@Data
@Entity
public class Custom {
    
    // Custom和Order是一对一关系。Order表中一个名为CUST_ID的列作为外键指向Custom对应表中名为ID_CUST的列,另一名为CUST_NAME的列作为外键指向Custom对应表中名为NAME_CUST的列
    @OneToOne
    @JoinColumns({
        @JoinColumn(name="CUST_ID", referencedColumnName="ID_CUST"),
        @JoinColumn(name="CUST_NAME", referencedColumnName="NAME_CUST")
    })
    private Order order;
}


@OneToOne、@OneToMany

@OneToOne

描述一个 一对一的关联

属性说明:

  • fetch:表示抓取策略,默认为FetchType.LAZY
  • cascade:表示级联操作策略。CascadeType.ALL,当前类增删改查改变之后,关联类跟着增删改查。
@Data
@Entity
public class Person {
    ...
    
    // Person和Address是一对一关系。Address表中名为id_address的列作为外键指向Person表中名为address_id的列
    @OneToOne
    @JoinColumn(name="address_id", referencedColumnName="id_address", unique=true)
    private Address address;
}


@OneToMany

描述一个 一对多的关联,该属性应该为集体类型,在数据库中并没有实际字段。

属性说明:

  • fetch:表示抓取策略,默认为FetchType.LAZY,因为关联的多个对象通常不必从数据库预先读取到内存
  • cascade:表示级联操作策略,对于OneToMany类型的关联非常重要,通常该实体更新或删除时,其关联的实体也应当被更新或删除

例如:实体User和Order是OneToMany的关系,则实体User被删除时,其关联的实体Order也应该被全部删除


@ManyToOne、@ManyToMany

@ManyToOne

表示一个多对一的映射,该注解标注的属性通常是数据库表的外键

属性说明:

  • optional:是否允许该字段为null,该属性应该根据数据库表的外键约束来确定,默认为true
  • fetch:表示抓取策略,默认为FetchType.EAGER
  • cascade:表示默认的级联操作策略,可以指定为ALL,PERSIST,MERGE,REFRESH和REMOVE中的若干组合,默认为无级联操作
  • targetEntity:表示该属性关联的实体类型。该属性通常不必指定,ORM框架根据属性类型自动判断 targetEntity


@ManyToMany

描述一个多对多的关联。多对多关联上是两个一对多关联,但是在ManyToMany描述中,中间表是由ORM框架自动处理

属性说明:

  • targetEntity:表示多对多关联的另一个实体类的全名,例如:package.Book.class
  • mappedBy:表示多对多关联的另一个实体类的对应集合属性名称

两个实体间相互关联的属性必须标记为@ManyToMany,并相互指定targetEntity属性, 需要注意的是,有且只有一个实体的@ManyToMany注解需要指定mappedBy属性,指向targetEntity的集合属性名称,利用ORM工具自动生成的表除了User和Book表外,还自动生成了一个User_Book表,用于实现多对多关联


@NamedStoredProcedureQuery

定义在一个实体上面声明存储过程。有多个存储过程,可以用@NamedStoredProcedureQueries。

  • name:自定义存储过程在java中的唯一别名,调用时使用;
  • procedureName:数据库中的存储过程名;
  • parameters:存储过程的参数

    • @StoredProcedureParameter:定义存储过程的参数属性

      • name:参数名。和数据库里的参数名字一样
      • mode:参数模式。ParameterMode.IN、OUT、INOUT、REF_CURSOR
      • type:参数数据类型。String.class、Integer.class,Long.class等


@JsonFormat、@DateTimeFormat

@JsonFormat

后端到前端的时间格式的转换。注意:该注解并非 JPA 注解。

// 出参时间格式化
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;

也可以在配置文件中配置进行时间戳统一转换:

spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8


@DateTimeFormat

前端到后端的时间格式的转换。注意:该注解并非 JPA 注解。

// 入参出参时间格式化。请求报文只需要传入"yyyy-MM-dd HH:mm:ss"格式字符串进来,则自动转换为Date类型数据
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm")
private Date createTime;


主键生成策略

1. 通用策略

参考:https://blog.csdn.net/chenlong220192/article/details/46678461

通过annotation来映射hibernate实体,基于annotation的hibernate主键标识@Id,由@GeneratedValue设定其生成规则。

GeneratedValue注解

用于标注主键的生成策略,通过 strategy 属性指定。默认情况下,JPA 自动选择一个最适合底层数据库的主键生成策略:SqlServer对应identity,MySQL 对应 auto increment 。

源码定义:

@Target({METHOD,FIELD})    
@Retention(RUNTIME)    
public @interface GeneratedValue{    
    GenerationType strategy() default AUTO;    
    String generator() default "";    
} 

其中GenerationType:

public enum GenerationType{    
    TABLE,    
    SEQUENCE,    
    IDENTITY,    
    AUTO   
}  

JPA提供的四种标准用法为:

  • AUTO:JPA自动选择合适的策略,是默认选项
  • IDENTITY:采用数据库ID自增长的方式来自增主键字段。Oracle 不支持这种方式
  • TABLE:通过表产生主键,框架借由表模拟序列产生主键,使用该策略可以使应用更易于数据库移植
  • SEQUENCE:通过序列产生主键,通过@SequenceGenerator 注解指定序列名。MySql不支持这种方式


1、AUTO 用法

// 使用示例
@Id  
@GeneratedValue(strategy = GenerationType.AUTO)  

在指定主键时,如果不指定主键生成策略,默认为AUTO。

// 使用示例
@Id  

此时主键生成策略,为默认值AUTO。

以下指定主键生成策略为AUTO,效果同上:

// 使用示例
@Id  
@GeneratedValue(strategy = GenerationType.AUTO)  


2、IDENTITY 用法

// 使用示例
@Id  
@GeneratedValue(strategy = GenerationType.IDENTITY)  


3、TABLE 用法

// 使用示例。
@Id  
@GeneratedValue(strategy = GenerationType.TABLE, generator="pk_gen")  
@TableGenerator(
    name = "pk_gen",  
    table="tb_generator",  
    pkColumnName="gen_name",  
    valueColumnName="gen_value",  
    pkColumnValue="PAYABLEMOENY_PK",  
    allocationSize=1  
)  

使用此策略需要数据库存在相应的表。此处应用表 tb_generator,定义为:

CREATE TABLE  tb_generator (  
  id NUMBER NOT NULL,  
  gen_name VARCHAR2(255) NOT NULL,  
  gen_value NUMBER NOT NULL,  
  PRIMARY KEY(id)  
)  

插入纪录,供生成主键使用

INSERT INTO tb_generator(id, gen_name, gen_value) VALUES (1,PAYABLEMOENY_PK', 1); 

在主键生成后,这条纪录的value值,按allocationSize递增。

// @TableGenerator的源码定义: 
@Target({TYPE, METHOD, FIELD})   
@Retention(RUNTIME)  
public @interface TableGenerator {  
  String name();  
  String table() default "";  
  String catalog() default "";  
  String schema() default "";  
  String pkColumnName() default "";  
  String valueColumnName() default "";  
  String pkColumnValue() default "";  
  int initialValue() default 0;  
  int allocationSize() default 50;  
  UniqueConstraint[] uniqueConstraints() default {};  
}  

以上属性说明如下:

  • name 表示该表主键生成策略的名称,它被引用在@GeneratedValue中设置的“generator”值中;
  • table 表示表生成策略所持久化的表名,例如,这里表使用的是数据库中的“tb_generator”;
  • catalog 属性和 schema属性具体指定表所在的目录名或是数据库模式名;
  • pkColumnName 属性的值表示在持久化表中,该主键生成策略所对应键值的名称。

    例如在“tb_generator”中将“gen_name”作为数据库表中主键的键值对的名称;

  • valueColumnName 属性的值表示在持久化表中,该主键当前所生成的值,它的值将会随着每次创建累加。

    例如,在“tb_generator”中将“gen_value”作为数据库表中主键的键值对的键值;

  • pkColumnValue 属性的值表示在持久化表中,该生成策略所对应的主键。例如在“tb_generator”表中,将“gen_name”的值为“CUSTOMER_PK”;
  • initialValue 表示主键初始值,默认为0;
  • allocationSize 表示每次主键值增加的大小。例如设置成1,则表示每次创建新记录后自动加1,默认为50;
  • UniqueConstraint 与@Table标记中的用法类似;


4、SEQUENCE 用法

// 使用示例
@Id  
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator="aaa")  
@SequenceGenerator(name="aaa", sequenceName="seq_payment")  

@SequenceGenerator 源码定义:

@Target({TYPE, METHOD, FIELD})   
@Retention(RUNTIME)  
public @interface SequenceGenerator {  
 String name();  
 String sequenceName() default "";  
 int initialValue() default 0;  
 int allocationSize() default 50;  
}  

以上属性说明如下:

  • name 表示该表主键生成策略的名称,它被引用在@GeneratedValue中设置的“generator”值中。
  • sequenceName 表示生成策略用到的数据库序列名称;
  • initialValue 表示主键初识值,默认为0;
  • allocationSize 表示每次主键值增加的大小。例如设置成1,则表示每次创建新记录后自动加1,默认为50;


2. hibernate策略

hibernate提供多种主键生成策略,有点是类似于JPA,基于Annotation的方式通过@GenericGenerator实现

以下是hibernate特有的:

  • native:对于 oracle 采用 Sequence 方式,对于MySQL 和 SQL Server 采用identity(自增主键生成机制),native就是将主键的生成工作交由数据库完成,hibernate不管(常用);
  • uuid:采用128位的uuid算法生成主键,uuid被编码为一个32位16进制数字的字符串,占用空间大(字符串类型);
  • hilo:使用hilo生成策略,要在数据库中建立一张额外的表,默认表名为hibernate_unique_key,默认字段为integer类型,名称是next_hi(比较少用);
  • assigned:在插入数据的时候主键由程序处理(很常用),这是 <generator>元素没有指定时的默认生成策略。等同于JPA中的AUTO;
  • identity:使用SQL Server 和 MySQL 的自增字段,这个方法不能放到 Oracle 中,Oracle 不支持自增字段,要设定sequence(MySQL 和 SQL Server 中很常用),等同于JPA中的INDENTITY;
  • increment:插入数据的时候hibernate会给主键添加一个自增的主键,但是一个hibernate实例就维护一个计数器,所以在多个实例运行的时候不能使用这个方法;
  • select:使用触发器生成主键(主要用于早期的数据库主键生成机制,少用);
  • sequence:调用底层数据库的序列来生成主键,要设定序列名,不然hibernate无法找到;
  • seqhilo:通过hilo算法实现,但是主键历史保存在Sequence中,适用于支持 Sequence 的数据库,如 Oracle(比较少用);
  • foreign:使用另外一个相关联的对象的主键,通常和联合起来使用;
  • guid:采用数据库底层的guid算法机制,对应MYSQL的uuid()函数,SQL Server的newid()函数,ORACLE的rawtohex(sys_guid())函数等;
  • uuid.hex:看uuid,建议用uuid替换;
  • sequence-identity:sequence策略的扩展,采用立即检索策略来获取sequence值,需要JDBC3.0和JDK4以上(含1.4)版本;


对于这些hibernate主键生成策略和各自的具体生成器之间的关系,在org.hibernate.id.IdentifierGeneratorFactory中指定了:

static {  
  GENERATORS.put("uuid", UUIDHexGenerator.class);  
  GENERATORS.put("hilo", TableHiLoGenerator.class);  
  GENERATORS.put("assigned", Assigned.class);  
  GENERATORS.put("identity", IdentityGenerator.class);  
  GENERATORS.put("select", SelectGenerator.class);  
  GENERATORS.put("sequence", SequenceGenerator.class);  
  GENERATORS.put("seqhilo", SequenceHiLoGenerator.class);  
  GENERATORS.put("increment", IncrementGenerator.class);  
  GENERATORS.put("foreign", ForeignGenerator.class);  
  GENERATORS.put("guid", GUIDGenerator.class);  
  GENERATORS.put("uuid.hex", UUIDHexGenerator.class); //uuid.hex is deprecated  
  GENERATORS.put("sequence-identity", SequenceIdentityGenerator.class);  
}  
// 上面十二种策略,加上native,hibernate一共默认支持十三种生成策略。


使用示例:

// uuid
@GeneratedValue(generator = "paymentableGenerator")    
@GenericGenerator(name = "paymentableGenerator", strategy = "uuid")
// 除以下所列特殊适配格式外,其他策略均采用上面第一种格式
// select
@GeneratedValue(generator = "paymentableGenerator")  
@GenericGenerator(name="select", strategy="select",  
                  parameters = { @Parameter(name = "key", value = "idstoerung") }) 
// sequence
@GeneratedValue(generator = "paymentableGenerator")  
@GenericGenerator(name = "paymentableGenerator", strategy = "sequence", 
                  parameters = { @Parameter(name = "sequence", value = "seq_payablemoney") })  
// seqhilo
@GeneratedValue(generator = "paymentableGenerator")  
@GenericGenerator(name = "paymentableGenerator", strategy = "seqhilo",  
                  parameters = { @Parameter(name = "max_lo", value = "5") })  
// foreign 
@GeneratedValue(generator = "idGenerator")  
@GenericGenerator(name = "idGenerator", strategy = "foreign", 
                  parameters = { @Parameter(name = "property", value = "employee") }) 
// sequence-identity
@GeneratedValue(generator = "paymentableGenerator")  
@GenericGenerator(name = "paymentableGenerator", strategy = "sequence-identity",
                  parameters = { @Parameter(name = "sequence", value = "seq_payablemoney") })


hibernate每种主键生成策略提供接口org.hibernate.id.IdentifierGenerator的实现类,如果要实现自定义的主键生成策略也必须实现此接口。

IdentifierGenerator提供一generate方法,generate方法返回产生的主键。

// 源码展示
public interface IdentifierGenerator {  
    /** 
     * The configuration parameter holding the entity name 
     */  
    public static final String ENTITY_NAME = "entity_name";  
      
  /** 
   * Generate a new identifier. 
   * @param session 
   * @param object the entity or toplevel collection for which the id is being generated 
   * 
   * @return a new identifier 
   * @throws HibernateException 
   */  
  public Serializable generate(SessionImplementor session, Object object)   
    throws HibernateException;  
}  


3. 自定义策略

由@GenericGenerator实现

GenericGenerator注解

源码定义:

@Target({PACKAGE, TYPE, METHOD, FIELD})  
@Retention(RUNTIME)  
public @interface GenericGenerator {  
 /** 
  * unique generator name 
  */  
 String name();  
 /** 
  * Generator strategy either a predefined Hibernate 
  * strategy or a fully qualified class name. 
  */  
 String strategy();  
 /** 
  * Optional generator parameters 
  */  
 Parameter[] parameters() default {};  
} 

以上属性说明如下:

  • name 属性指定生成器名称;
  • strategy 属性指定具体生成器的类名;
  • parameters 得到strategy指定的具体生成器所用到的参数;


通过@GenericGenerator自定义主键生成策略

import org.hibernate.HibernateException;
import org.hibernate.MappingException;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.id.UUIDHexGenerator;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.type.Type;

import java.io.Serializable;
import java.util.Properties;

/**
 * 自定义主键生成策略。实现自己设置ID,同时保留原来的主键生成策略(32位UUID)不变。
 * 调用的保存方法需为Repository.save()或EntityManager.merge()
 * 若调用的保存方法为EntityManager.persist(),且传入对象有id值时,仍会报错!
 */
public class CustomUUIDGenerator extends UUIDHexGenerator {
    private String entityName;

    @Override
    public void configure(Type type, Properties params, ServiceRegistry serviceRegistry) throws MappingException {
        entityName = params.getProperty(ENTITY_NAME);
        if (entityName == null) {
            throw new MappingException("no entity name");
        }
        super.configure(type, params, serviceRegistry);
    }
    @Override
    public Serializable generate(SharedSessionContractImplementor session, Object object) throws HibernateException {
        Serializable id = session.getEntityPersister(entityName, object).getIdentifier(object, session);
        if (id != null) {
            return id;
        }
        return super.generate(session, object);
    }
}


/** 
 * Entity实体类中使用
 */
@GeneratedValue(generator = "paymentableGenerator")  
@GenericGenerator(name = "paymentableGenerator", strategy = "{自定义主键生成策略的全限定类名}") 
private String id;


Repository 相关注解

Repository相关注解主要在SpringDataJpa的Repository中使用。

@Query:自定义 JPQL 或原生Sql 查询,摆脱命名查询的约束

    @Query("select u from User u where u.firstname = :firstname")     // JPQL
    User findByLastnameOrFirstname(@Param("lastname") String lastname);

    @Query(value = "SELECT * FROM USERS WHERE X = ?1", nativeQuery = true)    // 原生sql
    User findByEmailAddress(String X);

Ø 关于@Query中参数的占位符:

  1. 方式一:标识符 : :参数名

    可以定义好参数名,赋值时采用@Param(“参数名”),而不用管顺序。

  2. 方式二:使用索引下标:?索引值

    索引值从1开始,查询中 ”?X” 个数需要与方法定义的参数个数相一致,并且顺序也要一致。


@Modifying:DELETE和UPDATE操作必须加上@modifying注解,以通知Spring Data 这是一个DELETE或UPDATE操作。

@Transactional:UPDATE或者DELETE操作需要使用事务

@Async:异步操作

@NoRepositoryBean:避免Spring容器为此接口创建实例。可用于定义公共Repository

/**
 * 可以将业务中用到的公共方法抽离到公共Repository中
 */
@NoRepositoryBean //避免Spring容器为此接口创建实例。不被Service层直接用到的Repository(如base repository)均应加此声明
public interface BaseRepository<T, ID> {
    @Modifying
    @Query("update #{#entityName} set isDelete='N' where id in ?1 ")
    Integer myUpdateAsNotDeleted(Collection<String> ids);
}


SpringDataJpa 框架使用

官方文档:https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#preface

基本使用

基本使用步骤及依赖

基本使用步骤

  1. 将 spring-data-jpa 包,数据库驱动包等添加为项目依赖;
  2. 配置文件定义相应的数据源;
  3. 定义Entity实体类;
  4. 定义自己业务相关的的 JPA repository 接口,继承自 JpaRepository 或者JpaSpecificationExecutor;
  5. 为应用添加注解@EntityScan、@EnableJpaRepositories,此步不是必须的;
  6. 将自定义的 JPA repository 接口注入到服务层并使用它们进行相应的增删改查;

依赖

    <!-- Spring Boot JPA 依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

     <!-- mysql 驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <!-- pgsql 驱动-->
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
    </dependency>


Entity 类与 Respository 接口示例

Entity类

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Table(name="CUSTOMERS")
@Entity
@DynamicInsert
@DynamicUpdate
public class Customer {
    
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    @Column(name = "id",insertable = false, updatable = false, length = 32)
    private Integer id;
    
    @Column(name = "name", nullable = false, length = 10)
    private String name;
    
    @Column(name = "age")
    private Integer age;
    
    @Temporal(TemporalType.TIMESTAMP)
    @CreationTimestamp
    @Column(name = "create_date", columnDefinition = "timestamp(6)")
    private Date createDate;
    
    @Temporal(TemporalType.TIMESTAMP)
    @UpdateTimestamp
    @Column(name = "update_date", columnDefinition = "timestamp(6)")
    private Date updateTime;
}

Respository接口

// Customer 为该respository对应的实体类,Long为实体类的主键的类型
public interface CustomerRespository extends JpaRespository<Customer, Long>{
}


注解扫描及 JPA、JDBC 常用配置

注解扫描

在SpringBoot中:

  • 默认情况下,当Entity类、Repository类与主类在同一个包下或在主类所在包的子类时,Entity类、Repository类会被自动扫描到并注册到Spring容器,此时使用者无需任何额外配置。
  • 当不在同一包或不在子包下时,需要分别通过在主类上加注解

    • @EntityScan( basePackages = {"xxx.xxx"}) 来指定Entity的位置

      可多处使用@EntityScan。它们的basePackages可有交集,但必须覆盖到所有被Resository使用到的Entity,否则会报错。

    • @EnableJpaRepositories( basePackages = {"xxx.xxx"}) 来指定Repository类的位置

      可多处使用@EnableJpaRepositories。它们的basePackages不能有交集否则会报重复定义的错(除非配置允许覆盖定义),必须覆盖到所有被使用到的Resository。


JPA和JDBC常用配置

在利用SpringBoot框架进行开发时,大部分服务避不开用数据库进行数据存储和使用。SpringBoot里面一般有两种方式进行数据表的创建和数据存储。

  • Spring JDBC,我们需要在application.yml或者application.properties中配置JDBC相关属性,主要是spring.datasource.xxx属性配置。当然,使用jpa也需要用到spring.datasource.url/username/password等属性配置进行数据库地址、用户名、密码等配置。
  • Spring Boot JPA,我们需要在application.yml或者application.properties中配置jpa相关属性spring.jpa.xxx属性配置。


配置模板:

spring:
  datasource:
      # driver-class-name: com.mysql.jdbc.Driver
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
    username: root
    password: root
    type: com.alibaba.druid.pool.DruidDataSource    # 数据源配置
    schema: classpath:db/schema.sql                    # 建表语句脚本的存放路径
    data: classpath:db/data.sql                        # 数据库初始化数据的存放路径
    sql-script-encoding: UTF-8                        # 设置脚本的编码
  jpa:
      database: mysql                # 配置数据库方言。使用JPA访问数据库时,必需配置。
    hibernate:
      ddl-auto: none            # 每次程序启动时的数据库初始化策略
    database-platform: org.hibernate.dialect.MySQL5InnoDBDialect    # 配置数据库引擎,不配置则默认为myisam引擎
    show-sql: true                # 日志打印执行的SQL
      properties:
      hibernate:
          # show_sql: true        # 日志打印执行的SQL。与spring.jpa.show-sql配置效果相同,两者使用其一即可。
        format_sql: true        # 格式化sql语句

配置说明:

spring.datasource.xxx

spring.datasource.driver-class-name:配置driver的类名,默认是从JDBC URL中自动探测

spring.datasource.url:配置数据库JDBC连接串

spring.datasource.username:配置数据库连接用户名

spring.datasource.password:配置数据库连接用户名对应的密码

spring.datasource.type:连接池配置

spring.datasource.schema:使用脚本创建表的语句的存放路径,classpath/db表示在工程的resource层级下的db目录中存放

spring.datasource.data:使用脚本初始化数据库数据的语句的存放路径

spring.datasource.sql-script-encoding:设置脚本的编码,默认常用设置为UTF-8

使用上述方式建表时,spring.jpa.hibernet.ddl-auto设置成none,否则有啥问题,我也没尝试过。这样配置可以避免两种方式一起使用


spring.jpa.xxx

spring.jpa.hibernet.ddl-auto 值说明:

  • create: 服务程序重启后,加载hibernate时都会删除上一次服务生成的表,然后根据服务程序中的model(entity)类再重新生成表,这个值慎用,会导致数据库中原表数据丢失。
  • create-drop :服务服务程序重启后,加载hibernate时根据model(entity)类生成表,当sessionFactory关闭时,创建的表就自动删除。
  • update:默认常用属性,第一次加载hibernate时根据model(entity)类会自动建立表结构,后面服务程序重启时,加载hibernate会根据model(entity)类自动更新表结构,如果表结构改变了,但是表行仍然存在,不会删除以前的行(对于表结构行只增不减)。
  • validate :服务程序重启后,每次加载hibernate时,验证创建数据库表结构,只会和数据库中的表进行比较,如果不同,就会报错。不会创建新表,但是会插入新值。
  • none :什么也不做。

spring.jpa.database:配置数据库方言,使用JPA访问数据库时,必需配置。

spring.jpa.database-platform:配置数据库引擎。SpringBoot 2.0后使用JPA、Hibernate来操作MySQL,Hibernate默认使用MyISM存储引擎而非InnoDB,前者不支持外键故会忽略外键定义。

使用JPA访问数据库的注意事项

  • spring.jpa.database和spring.jpa.database-platform 这两项配置至少要配置一个来指明数据库方言
  • 访问的是MySQL数据库时,spring.datasource.driver-class-name 需配置为 com.mysql.cj.jdbc.Driver
  • MySQL jdbc 6.0 版本以上 spring.datasource.url 中地址必须要配置 “serverTimezone”参数

    UTC代表的是全球标准时间

    若我们使用的时间是北京时区也就是东八区,领先UTC八个小时。url的时区使用中国标准时间。也是就serverTimezone=Asia/Shangha


Respository 接口核心方法

  • 查询所有数据

    List<T> findAll()
  • 根据id查询

    Optional<T> findOne()
  • 根据实体类属性查询(需命名方法)

    findByProperty (type Property)        // 例如:findByAge(int age)
  • 分页查询

    Page<S> findAll(Example<S> example, Pageable pageable)
  • 修改 or 添加数据

    S save(S entity)

    底层逻辑为:当entity的id为null,则直接新增,不为null,则先select,如果数据库存在,则update。如果不存在,则insert

  • 批量保存

    List<T> saveAll(Iterable<S> list)

    注意:当批量保存大量数据时,效率会很慢!因为 saveAll 本质是循环集合调用save方法。优化方案见 批量保存优化

  • 删除

    void delete(T entity)
  • 计数 查询

    long count()

    或者 根据某个属性的值查询总数

    countByAge(int age)
  • 是否存在

    boolean existsById(ID primaryKey)


查询API

自定义命名查询及查询关键字

通过方法名来指定查询逻辑,而不需要自己实现查询的SQL逻辑,示例:List<Student> getByName(String name)

方法名解析原理

对方法名中除了保留字(findBy、top、within等)外的部分以 and 为分隔符提取出条件单词,然后解析条件获取各个单词并看是否和Entity中的属性对应(不区分大小写进行比较)。

注意:get/find 与 by之间的会被忽略,所以 getNameByIdgetById 是等价的,会根据id查出整个Entity而不会只查name字段(指定部分字段的查询见后面条目)。


查询条件解析原理

假设School和Student是一对多关系,Student中有个School school字段、School有个String addressCode字段,以如下查询为例:

// 查询student表
Studetn getByNameAndSchoolAddressCode(String studentName, String addressCode)
// JPA会自动生成条件studentName和关联条件student.school.addressCode进行查询
    
// 查询student表,推荐写法
Studetn getByNameAndSchool_AddressCode(String studentName, String addressCode)
  1. 由And分割得到studentNameSchoolAddressCode
  2. 分别看Student中是否有上述两属性,显然前者有,后者没有,则后者需要进一步解析;
  3. JPA 按驼峰命名格式从后往前尝试分解 SchoolAddressCode

    先得到 SchoolAdressCode,由于 Student 没有SchoolAddress属性,故继续尝试分解,得到SchoolAdressCode

    由于 Student 有 School 属性且 School 有 addressCode 属性,故满足,最终得到条件student.school.addressCode

    注:若Student中有个 SchoolAdress schoolAddress 属性,但 schoolAddress 中没有 code 属性,则会因找不到 student.schoolAdress.code 而报错,所以可通过下划线显示指定分割关系,即写成:getByNameAndSchool_AddressCode


查询字段解析原理

默认会查出 Entity 的所有字段且返回类型为该Entity类型,有两种情况可查询部分字段(除此外都会查出所有字段):

  1. 通过 @Query 写自定义查询逻辑中只查部分字段。这种不属于直接通过方法名指定查询(详见后面查询指定部分字段的条目)。
  2. 返回类型为自定义接口或该接口列表,接口中仅包含部分字段的get方法,此时会根据接口方法名查询部分字段。示例:

    /**
     * 此方法在 CourseRepository 中
     * 注:find和By间的部分在解析时会被忽略。但为了见名知意,最好加上字段信息,如 findVersionByGroupId
     */
    List<MyCustomColumns> findCustomColumnsByGroupId(String groupId);
    
    /**
     * 封装查询结果的接口
     */
    public interface MyCustomColumns {
        //JPA生成查询语句时只会查下面get方法中指定的字段名。需要确保Entity中有该字段名否则会报错
        public String getId();
        public String getVersion();
        public String getGroupId();
    }


JPA 集合类型查询参数

List<StudentEntity> getByIdInAndSchoolId(Collection<String> studentIdList, String schoolId); 

关键在于 In 关键字。参数用Collection类型,当然也可以用List、Set等,但用Collection更通用,因为此时实际调用可以传List、Set等实参。


查询关键字

在查询时,通常需要同时根据多个属性进行查询,且查询的条件也格式各样(大于某个值、在某个范围等等),SpringDataJPA 为此提供了一些表达条件查询的关键字,官方文档如下:

keyword Sample JPQL snippet
Distinct findDistinctByLastnameAndFirstname select distinct … where x.lastname = ?1 and x.firstname = ?2
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, Null findByAge(Is)Null() … 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 ages) … where x.age in ?1
NotIn findByAgeNotIn(Collection 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)


Example(动态实例)查询

Example查询翻译过来叫 ” 按例查询(QBE)”。是一种用户界面友好的查询技术。 它允许动态创建查询,并且不需要编写包含字段名称的查询。 而且按示例查询不需要使用特定的数据库的查询语言来编写查询语句。

优势:

  • 可以使用动态或者静态的限制去查询
  • 在重构你的实体的时候,不用担心影响到已有的查询
  • 可以独立地工作在数据查询API之外

劣势:

  • 不支持组合查询,比如:firstname = ?0 or (firstname = ?1 and lastname = ?2)
  • 只支持字符串的starts/contains/ends/regex匹配,对于非字符串的属性,只支持精确匹配。

    换句话说,并不支持大于、小于、between等匹配。

  • 对一个要进行匹配的属性(如:姓名 name),只能传入一个过滤条件值


Example(动态实例)查询的原理

从生成的SQL语句可以看到,它的判断条件是根据实体的属性来生成查询语句的。如果实体的属性是null,它就会忽略它;如果不是,就会取其值作为匹配条件。

注意:如果一个字段是不是包装类型,而是基本类型,它也会参与where条件中,其值是默认值。所以在定义实体时,基本数据类型的字段应尽量使用包装类型。

使用示例

@Test
public void test01() {
    User user = User.builder().name("Bob").build();
    Example<User> example = Example.of(user);
    
    List<User> list = userRepository.findAll(example);
    list.foreach(System.out::println)
    
    Optional<User> userOptional = userRepository.findOne(Example.of(user));
    userOptional.ifPresent(x -> System.out.println(x.getName()).isEqualTo("Bob"));
}


Example(动态实例)查询的概念定义介绍:

// 示例:Example对象,由customer和matcher共同创建
Example<Customer> ex = Example.of(customer, matcher);
  • 实体对象:在持久化框架中与Table对应的域对象,一个对象代表数据库表中的一条记录,如上例中Customer对象。在构建查询条件时,一个实体对象代表的是查询条件中的“数值”部分。

    如:要查询名字是“Dave”的客户,实体对象只能存储条件值“Dave”

  • 匹配器:ExampleMatcher对象,它是匹配“实体对象”的,表示了如何使用“实体对象”中的“值”进行查询,它代表的是“查询方式”,解释了如何去查的问题。

    如:要查询FirstName是“Dave”的客户,即名以“Dave"开头的客户,该对象就表示了“以什么开头的”这个查询方式,如上例中:withMatcher("name", GenericPropertyMatchers.startsWith())

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

实例查询:就是通过一个例子来查询。要查询的是Customer对象,查询条件也是一个Customer对象,通过一个现有的客户对象作为例子,查询和这个例子相匹配的对象


自定匹配器规则

ExampleMatcher,不传时会使用默认的匹配器。

    @Test
    public void test02() {
        //创建查询条件数据对象
        User user = new User();
        user.setUsername("y");
        user.setAddress("sh");
        user.setPassword("admin");
        
        //创建匹配器,即如何使用查询条件
        ExampleMatcher matcher = ExampleMatcher.matching()
                //模糊查询匹配开头,即{username}%
//              .withMatcher("username", ExampleMatcher.GenericPropertyMatchers.startsWith())
                .withMatcher("username", match -> match.startsWith())
                //全部模糊查询,即%{address}%
                .withMatcher("address" ,ExampleMatcher.GenericPropertyMatchers.contains())
                //忽略字段,即不管password是什么值都不加入查询条件
                .withIgnorePaths("password");
        
        //创建实例
        Example<User> example = Example.of(user ,matcher);
        
        //查询
        List<User> list = userRepository.findAll(example);
        System.out.println(list);
    }
/*
打印的sql语句如下:
select
    user0_.id as id1_0_,
    user0_.address as address2_0_,
    user0_.email as email3_0_,
    user0_.password as password4_0_,
    user0_.phone as phone5_0_,
    user0_.username as username6_0_ 
from
    t_user user0_ 
where
    (
        user0_.username like ?
    ) 
    and (
        user0_.address like ?
    )

参数如下:
2018-03-24 13:26:57.425 TRACE 5880 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [y%]
2018-03-24 13:26:57.425 TRACE 5880 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [%sh%]
*/
    
    @Test
    public void test03() {
        //创建查询条件数据对象
        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");  //忽略属性:是否关注。因为是基本类型,需要忽略掉
        //创建实例与查询
        List<Customer> ls = dao.findAll(Example.of(customer, matcher));
    }

    @Test
    public void test04() {
        List<Customer> ls = dao.findAll(Example.of(
            new Customer(), 
            ExampleMatcher.matching() //构建对象
                .withIncludeNullValues() //改变“Null值处理方式”:包括
                //忽略其他属性
                .withIgnorePaths("id","name","sex","age","focus","addTime","remark","customerType")
        ));
    }


StringMatcher 参数

Matching 生成的语句 说明
DEFAULT (case-sensitive) firstname = ?0 默认(大小写敏感)
DEFAULT (case-insensitive) LOWER(firstname) = LOWER(?0) 默认(忽略大小写)
EXACT (case-sensitive) firstname = ?0 精确匹配(大小写敏感)
EXACT (case-insensitive) LOWER(firstname) = LOWER(?0) 精确匹配(忽略大小写)
STARTING (case-sensitive) firstname like ?0 + ‘%’ 前缀匹配(大小写敏感)
STARTING (case-insensitive) LOWER(firstname) like LOWER(?0) + ‘%’ 前缀匹配(忽略大小写)
ENDING (case-sensitive) firstname like ‘%’ + ?0 后缀匹配(大小写敏感)
ENDING (case-insensitive) LOWER(firstname) like ‘%’ + LOWER(?0) 后缀匹配(忽略大小写)
CONTAINING (case-sensitive) firstname like ‘%’ + ?0 + ‘%’ 模糊查询(大小写敏感)
CONTAINING (case-insensitive) LOWER(firstname) like ‘%’ + LOWER(?0) + ‘%’ 模糊查询(忽略大小写)

说明:

  • 在默认情况下(没有调用withIgnoreCase())都是大小写敏感的。

总结

  • 在使用springdata jpa时可以通过Example来快速的实现动态查询,同时配合Pageable可以实现快速的分页查询功能
  • 对于非字符串属性的只能精确匹配,比如想查询在某个时间段内注册的用户信息,就不能通过Example来查询


ExampleMatcher的使用 :

  • 基本类型的处理。如客户Customer对象中的年龄age是int型的,当页面不传入条件值时,它默认是0,是有值的,那是否参与查询呢?

实体对象中,基本数据类型无论是否传值,都会参与查询(因为有默认值),故应避免使用基本数据类型,采用包装器类型(默认值是null)。如果已经采用了基本类型,而这个属性查询时若不需要进行过滤,则需把它添加到忽略列表(ignoredPaths)中。

  • Null值的处理。当某个条件值为Null,是应当忽略这个过滤条件呢,还是应当去匹配数据库表中该字段值是Null的记录?

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

  • 忽略某些属性值。一个实体对象,有许多个属性,是否每个属性都参与过滤?是否可以忽略某些属性?

若属性值为null,默认忽略该过滤条件;若属性值为基本数据类型,默认参与查询,若需忽略,则需添加至则需把它添加到忽略列表(ignoredPaths)中。

  • 不同的过滤方式。同样是作为String值,可能“姓名”希望精确匹配,“地址”希望模糊匹配,如何做到?

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

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

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


JPQL 与 nativeQuery (原生SQL)查询

JPQL 是专门为 Java 应用程序访问和导航实体实例设计的。Java Presistence Query Language( JPQL ),java持久性查询语言。它是JPA规范的重要组成部分,其实它就是一种查询语言,语法类似于SQL语法,但是有着本质的区别。

JPQL 与 SQL 的区别

JPQL 是面向对象的查询语言,因此它可以完全理解继承、多态和关联等特征。而且 JPQL 内置了大量函数,极大地方便了 JPQL 查询的功能。当然 JPQL 底层依然是基于SQL的,但 JPQL 到 SQL 的转换无须开发者关心,JPQL解析器会负责完成这种转换,并负责执行这种转换的SQL语句来更新数据库。

SQL是面向关系数据库的查询语言,因此SQL操作的对象是数据表、数据列;而 JQPL 操作的对象是实体对象,对象属性。

代码对比

// 原生的SQL语句。对t_user table表执行查询,查询name、age、user_id三个数据列
select name,age,user_id from t_user

// 面向对象的JPQL语句。对User实体执行查询,查询的是User实体的name、age、userId 属性
select name,age,userId from User 
比较项 SQL JPQL
面向 处理关系数据 处理JPA实体
关联实体的方式 内连接、外连接、左连接、右连接 内连接和左外连接
支持的操作 增(Insert)、删(Delete)
改(Update)、查(Select)
Delete(remove)
Update(merge)、Select(find)


JPQL基本语法

select 实体别名.属性名,实体别名.属性名……
from 实体名 [as] 实体别名
where 实体别名.实体属性 op 比较值

使用 @Query 注解创建查询,将该注解标注在Repository的方法上,然后提供一个需要的 JPQL 语句即可,如:

@Query("select p from Person p where name like %?1%")
Person findByName(String name);

JPQL 查询时,可以使用SpEL表达式:#{#entityName} (取数据库实体名称 )

好处是当修改类名后,不需要再单独去修改 JPQL 中的实体类名称

@Query("select p from #{#entityName} p where name like %?1%")
Person findByName(String name;

SpEL表达式了解:

SpEL(Spring Expression Language),即Spring表达式语言。它是一种类似JSP的EL表达式、但又比后者更为强大有用的表达式语言。SpEL表达式可以在spring容器内实时查询和操作数据,尤其是操作List列表型、Array数组型数据。所以使用SpEL可以有效缩减代码量,优化代码结构。


@Query注解查询时候,条件查询如何使用占位符:

(1)?+ 数字

若使用这种方式,则参数列表中参数的入参顺序必须与 @Query注解当中标注的顺序相同

@Query("SELECT s from Student s where s.email=?1 and s.age=?2")
Student findStudentByEmailAndAge(String email , Integer age);

(2):+ 参数名称

这种方式可以自定义参数的名称。需要在参数列表当中用 @Param 注解标注参数名称。不用考虑顺序,是根据参数名称进行绑定

@Query("SELECT s from Student s where s.email=:email and s.age=:age")
Student findStudentByEmailAndAge2(@Param("age") Integer age, @Param("email") String email);


nativeQuery(原生SQL查询)

  • Repository中应尽可能避免使用nativeQuery,使得与数据库字段的耦合限制在Entity内而不扩散到Repository内,更易于维护
  • 尽可能避免在 JPQL、nativeQuery中进行联表查询,而是在Service层通过 JPA Specification 进行动态关联查询

nativeQuery返回Entity

使用 nativeQuery 时SQL语句查询的字段名若没有取别名,则默认是数据库中的字段名(例如school_id),而API返回值通常是schoolId,可以在SQL里通过 school_id as schoolId 取别名返回。

然而若查询很多个字段值则得一个个通过as取别名,很麻烦,可以直接将返回值指定为数据库表对应的Entity,不过此法要求查询的是所有字段名,如:

// nativeQuery返回类型可以声明为Entity,会自动进行匹配,要求查回与Entitydb中字段对应的所有db中的字段
@Query(value = " select t.* from teacher t where t.school_id=?1 "// 以下为搜索字段
       + "and (?4 is NULL or name like %?4% ) "
       + "order by job_number limit ?2,  x?3 ", nativeQuery = true)
List<TeacherEntity> myGetBySchoolIdOrderByJobNumber(String schoolId, int startIndex, Integer size, 
                                                    String searchName);


排序查询、分页查询

排序查询

静态方式:直接在方法体现(如 getByNameOrderByIdDesc),也可以在 JPQL 的@Query的逻辑中使用order by进行排序

动态方式:可以在Repository的方法的最后加一个Sort 或者 Pageable 类型的参数,便可动态生成排序或分页语句(编译后会自动在语句后加order bylimit语句)

List<User> findByAndSort(String name, Sort sort);

// 调用
UserRepository.findByAndSort("bolton", Sort.by(Direction.Desc, "age"));

进阶(了解)——可以通过 JpaSort.unsafe 实现待 function(函数计算) 的 sort(排序):

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);
}


/**
 * 调用
 */
userRepository.findByAndSort("lannister", new Sort("firstname"));     

//userRepository.findByAndSort("stark", new Sort("LENGTH(firstname)"));   //报错:invalid 无效
userRepository.findByAndSort("targaryen", JpaSort.unsafe("LENGTH(firstname)")); 


分页查询

动态方式:在Repository的方法的最后加一个 Pageable 类型的参数,便可动态生成分页语句(编译后会自动在语句后加limit语句)

// 不写@Query语句也可以加Pageable。另外若这里声明为List则不会分页,总是返回所有数据
@Query("select se from StudentExperimentEntity se "
       + "where se.studentId= ?2 and se.experimentId in "
       + "( select e.id from ExperimentEntity e where e.courseId= ?1 ) ")
List<StudentExperimentEntity> myGetByCourseIdAndStudentId(String courseId, String studentId, 
                                                          Pageable pageable);


/**
 * 调用
 * 编译后会在myGetByCourseIdAndStudentId所写SQL后自动加上 order by studentexp0_.lastopertime desc limit ? 
 */
repository.myGetByCourseIdAndStudentId(courseId, studentId, 
                                       PageRequest.of(0, 10, newSort(Sort.Direction.DESC, "lastopertime")));

注:上述用法也支持nativeQuery,示例:

@Query(value = "select d.*, u.username from developer d inner join user u on d.id=u.id "
       + " where (?1 is null or d.nick_name like %?1% ) ", nativeQuery = true)
List<DeveloperEntity> myGetByNicknameOrPhoneOrEmailOrBz(String searchNicknameOrPhoneOrEmailOrBz, 
                                                        Pageable pageable);

// 如果要同时返回分页对象,则可用Page<XX>返回类型
Page<DeveloperEntity> myGetByNicknameOrPhoneOrEmailOrBz(String searchNicknameOrPhoneOrEmailOrBz, 
                                                        Pageable pageable); 

需要注意的是,只有元素是Entity类型时才支持直接将返回值声明为Page对象,否则会报错:Convert Exception。


自定义封装查询的结果集(投影)

查询一个表的部分字段,称为投影(Projection)

对于只返回一个字段的查询,方法返回类型直接声明为该字段的类型或类型列表

@Query(value = "select p.name from PserSon p where p.id=?1") 
String findNameById(String id);

@Query(value = "select name from PserSon where age=?1") 
Set<String> findNameByAge(String id);

对于返回多个字段的查询:

  • 如果是查询所有字段,则使用Entity实体类接收查询的结果集,支持 JPA,JPQL,原生sql查询。
  • 如果是查询部分字段,自定义查询的结果集有3种方法:

    1. 使用自定义接口来映射结果集,支持 JPA,JPQL,原生sql查询。
    2. 使用自定义对象来接收结果集,支持 JPQL查询。
    3. 使用List<Object[]>Map<String, Object> 来接收结果集,只支持原生sql查询。


1.使用自定义接口来映射结果集

直接通过方法名命名指定返回对象为包含部分字段getter方法的自定义接口(interface),只需要定义属性的getter方法,jdk动态代理封装数据。

注意:如果不用 @Query 则需要确保接口中getter方法名中的字段与Entity中的一致,而如果使用@Query则不需要,因为可以通过as 取别名

/**
 * Repository自定义查询方法
 */
List<IdAndLanguageType> getLanguagesTypeByCourseIdIn(Collection<String> courseIdCollection); 


/**
 * 自定义接口来映射结果
 */
public interface IdAndLanguageType {
    String getIdx(); 
    String getLanguageType();
    
    default String getAll() {
        return getIdx() + ", " + getLanguageType();
    }
}

public interface PersonSummary {
    String getFirstname();
    String getLastname();
    List<AddressSummary> getAddress();
    
    public interface AddressSummary {
        String getCity();
    }
}

/** 
 * 投影接口中的 getter 可以使用可为空的包装器来提高空安全性。
 * 如果基础投影值不是null,则使用包装器类型的当前表示返回值。如果支持值是null,则 getter 方法返回所用包装器类型的空表示。
 * 当前支持的包装器类型有:
 * java.util.Optional、com.google.common.base.Optional、scala.Option、io.vavr.control.Option
 */
interface NamesOnly {
  Optional<String> getFirstname();
}

内部原理

  1. 根据自定义接口中的getter方法解析出要查询的字段:idx、languageType
  2. JPA 内部转成了用@Query注解的查询:

    // 注意:这里第一个字段名为 idx 而不是 id ,因为是根据接口getter方法产生的。
    @Query("select new map(idx as idx, languageType as languageType) from CourseEntity where id in ?1 ") 


2.使用自定义对象来接收结果集

返回对象为自定义的class,可定义toString方法,打印日志方便。

自定义的class须包含查询的字段的属性,且要封装的字段由公开的构造方法确定,对象属性不要求名称一致,只需要构造方法参数位置正确。

JPQL 语法须为:select new + 对象全限定类名

/**
 * Repository自定义查询方法
 */
@Query(select new com.xx.yy.PersonResult(p.id,p.name,p.age) from Person p)
List<PersonResult> findPersonResult();


/**
 * 自定义class类来映射结果
 * 自定义class类中属性若全为JPQL中的查询字段且顺序一致,使用@AllArgsConstructor全参构造方法即可,否则需手写相应构造方法。
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class IdAndLanguageType {
    String id; 
    String name;
    String age;
}


3.使用Map<String, Object>List<Object[]>来接收结果集

注意:指定为Map时,实际类型是org.springframework.data.jpa.repository.query.AbstractJpaQuery$TupleConverter$TupleBackedMap,该类型只能读不能改或写

(1)nativeQuery 查询,即 原生SQL查询

直接select部分字段即可,结果集默认会自动包装为Map

缺点是sql里用的直接是数据库字段名,导致耦合大,数据库字段名一变,所有相关sql都得相应改变。

/**
 * Repository自定义查询方法
 */
@Query(value = "select g.id, g.school_id as schoolId, g.name from grade g "
       + "left join student s on g.name=s.grade "
       + " where g.school_id=(select a.school_id from admin a where a.id=?1)"
       + " and (?4 is null or g.name like %?4% or g.bz like %?4% ) "
       + " group by g.id limit ?2,?3", nativeQuery = true)
List<Map<String, Object>> myGetGradeList(String adminId, Integer page, Integer size,
                                       String searchGradeNameOrGradeBz);

(2)JPQL 查询

可以手动指定包装为map,此时map的key为字段序号,故最通过as指定key为字段名。

默认会将结果包装为List而不是Map,可以手动指定包装为map,此时map的key为字段序号(0、1、2...),也可以通过as指定key为字段名。

注意:由于声明为Map时并不知道数据的返回类型是什么,故默认会用最大的类型(例如对于数据库中的整型列,查出时Map中该字段的类型为BigInteger)

/**
 * Repository自定义查询方法
 * 注意是 'map',不是 jdk 中的 'Map' !
 */  
@Query("select new map(g.name as name, count(s.id) as stuCount) from GradeEntity g, StudentEntity s " 
       + "where g.name=s.grade and g.schoolId=?1 group by g.id")
List<Map<String, Object>> myGetBySchoolId(String schoolId);

@Query("select new map(g.name as name, count(s.id) as stuCount) from GradeEntity g, StudentEntity s " 
       + "where g.name=s.grade and g.schoolId=?1 group by g.id")
List<Object[]> myGetBySchoolId(String schoolId);


动态投影

/**
 * Repository自定义查询方法
 * 动态投影方式。泛型根据需要传入Entity实体类或封装部分字段的自定义接口
 */ 
public interface PersonRepository extends Repository<Person, UUID> {
  <T> Collection<T> findByLastname(String lastname, Class<T> type);
}


count查询、In查询

count查询

Integer countByName(String name);


In查询

不管是否是@Query都可以用 in 查询,如:

@Query( "select * from student where id in ?1", nativeQuery=true)
//@Query( "select s from StudentEntity s where s.id in ?1")
List<StudentEntity> myGetByIdIn(Collection<String> studentIds );    //复杂查询,自定义查询逻辑

List<StudentEntity> getByIdIn( Collection<String> studentIds );        //简单查询,声明语句即可

不管是否自己写查询语句、不管是否是nativeQuery,都要求调用该方法时所传的id列表必须至少有一个元素,否则执行时会报错

原因:运行时动态生成sql语句,如果id列表为null或空列表,则最终生成的sql语句中"where id in null"不符合sql语法。


联表查询

1、Entity内未定义关联实体时的联表查询,示例:

@Query("select cd from CourseDeveloperEntity cd join Developer d where d.nickName='stdeveloper'") 

2、Entity内定义的关联实体的关联查询,示例:

@Query("select cd, d from CourseDeveloperEntity cd join cd.developer d where d.nickName='stdeveloper'") 
 
 || (等价于)
 
@Query("select cd, cd.developer from CourseDeveloperEntity cd where cd.developer.nickName='stdeveloper'") 

若将一个对象的关联对象指定为延迟加载LAZY,则每次通过该对象访问关联对象时(如courseDeveloper.developer)都会执行一次SQL来查出被关联对象,显然如果被关联对象访问频繁则此时性能差。

解决:

  • 法1:改为 EAGER 加载;
  • 法2:使用 join fetch 查询,其会立即查出被关联对象。示例:

    @Query("select cd from CourseDeveloperEntity cd join fetch cd.developer where cd.id='80'") 

    join Fetch 其实就是使用 inner join,可以显示指定用其他关联方式,例如 left join fetch

    join fetch的缺点之一在于有可能导致“Duplicate Data and Huge Joins”,例如多个实验关联同一课程,则查询两个实验时都关联查出所属课程,后者重复查询。


延迟加载与立即加载(FetchType)

默认情况下,@OneToOne、@ManyToOne是LAZY,@OneToMany、@ManyToMany是EAGER。但不绝对,看具体需要。

  • FetchType.LAZY:延迟加载。在查询实体A时,不查询出关联实体B,在调用getxxx方法时,才加载关联实体。但是注意,查询实体A时和getxxx必须在同一个 Transaction 中,不然会报错:“no session”,即会表现为两次单独的SQL查询(非联表查询)
  • FetchType.EAGER:立即加载。在查询实体A时,也查询出关联的实体B。即会表现为一次查询且是联表查询

有两个地方用到延迟加载:relationship(@OneToMany等)、attribute(@Basic)。后者一般少用,除非非常确定字段很少访问到。


增删改API

保存 与 更新

Repository方法核心方法

// 添加 or 修改数据  
S save(S entity)

底层逻辑为:当entity的id为null,则直接新增,不为null,则先select,如果数据库存在,则update。如果不存在,则insert


注意:若 JPA 启用了逻辑删除(软删除)功能,使用save方法则可能会出现 主键冲突 或 唯一索引冲突 等问题

原因:若数据库启用了逻辑删除功能,记录逻辑删除后,该条记录实际仍存在于数据库中,但是 JPA 根据Entity的主键查询数据库判断该执行insert还是update时,查询语句会自动加上逻辑删除的判断条件,从而查不到数据而最终执行insert,进而可能会导致报主键冲突或唯一索引冲突。


update

  • 方式1:通过Repository的save方法
  • 方式2:通过Repository中注解@@Query、@Modifying、@Query组合自定义方法(注:update不支持直接通过方法名声明


删除 与 逻辑删除

1、删除记录

Repository接口核心方法

void delete(T entity)


Repository自定义删除方法

需要加@Modefying、@Transactional

@Transactional     //也可以只标注在上层调用者方法上
@Modifying
@Query("delete from EngineerServices es where es.engineerId = ?1")//update与此类似
int deleteByEgId(String engineerId);

// 直接通过方法名声明(注:update不支持这样写)
@Transactional
@Modifying
int deleteByEgId(String engineerId);

注:JPA中非nativeQuery的删除操作(如deleteByName)内部实际上是先分析出方法名中的条件、接着按该条件查询出所有Entity,然后根据这些Entity的id执行SQL删除操作。

也正因为这样,软删除功能中指定 @SQLDelete("update student set is_delete='Y' where id=? ") 即可对所有非nativeQuery起作用。

方法名包含条件的删除操作(例如 Integer deleteByNameAndSId(String name, String uuid); ),其执行时与save类似,也是先根据条件查出目标Entity再执行删除操作。对于 void delete(T entity);, 则直接根据Entity的主键操作而不用先查。


2、逻辑删除

使用org.hibernate.annotations(不是JPA的标准)@Where、@SQLDelete、@SQLDeleteALL 三个注解来实现。

// 对非nativeQuery 旳delete起作用,包括形如deleteByName等,下同。
@SQLDelete(sql = "update " + StudentEntity.tableName 
           + " set " + constant.ISDELETE_COLUMN_NAME + " =true where sid=?") 
@SQLDeleteAll(sql = "update " + StudentEntity.tableName + 
              " set " + constant.ISDELETE_COLUMN_NAME + " =true where sid=?")
// 对非nativeQuery的select起作用(如count、非nativeQuery的String myGetNameByName等,前者本质上也是select)
@Where(clause = constant.ISDELETE_COLUMN_NAME + " = false") 
@Data
@Entity
@Table(name = StudentEntity.tableName)
public class StudentEntity extends BaseEntity {
    
    public static final String tableName = "student";
    ...
        
    @Column(name = constant.ISDELETE_COLUMN_NAME, nullable = false)
    private Boolean isDelete = false;

}

需要注意的是:

  • @Where会自动在查询语句后拼接@Where中指定的条件;该注解对所有的非nativeQuery的查询起作用,如count、自己写的非nativeQuery的查询语句(例如:myGetByName)等。
  • @SQLDelete会自动将删除语句替换为@SQLDelete中指定的sql操作;该注解对所有非nativeQuery的删除操作起作用,如delete(StudenEntity entity)、deleteBySId、deleteByName等,但由于指定的sql操作中条件不得不写死,所以要按期望起作用的话,@SQLDelete中的sql操作应以Entity的主键为条件,且自定义的删除方法必须按delete(StudenEntity entity)、deleteBySId两种写法写(delete(StudenEntity entity)会自动取entity的主键给sid),而不能用deleteByName(会将name参数值传给sid)
  • 通过 JPQL 的方法名指定删除操作(如 Integer deleteByName(String name))时背后是先根据条件查出Entity然后根据Entity的主键删除该Entity。所以通过@SQLDelete、@SQLDeleteALL实现逻辑删除时,由于其语句是写死的,故:

    • @SQLDelete、@SQLDeleteALL同时存在时会按后者来执行软删除逻辑
    • @SQLDeleteALL并不会批量执行软删除逻辑(因为一来不知具体有几个数据,二来in中只指定了一个元素),而是一个个删,即有多条待删除时也会一条条执行软删除逻辑,每条语句中in中只有一个元素。故其效果与@SQLDelete的一样,然而 “in” 操作效率比 “=” 低,故推荐使用@SQLDelete

关于软删除:对于关联表(一对一、一对多、多对多),若要启用软删除,则须为多对多关联表定义额外的主键字段而不能使用联合外键作为主键,否则软删除场景下删除关联关系再重新关联时会主键冲突。另外,特殊情况下多对多关联表可以不启用软删除(被关联表、一对多或多对一关联表则需要,因为它们侧重的信息往往不在于关联关系而是重要的业务信息)


批量保存优化

原生的saveAll()方法可以保证程序的正确性,但是如果数据量比较大时效率低。

源码逻辑原理是:for 循环集合调用save方法;save方法逻辑为,当entity的id为null,则直接新增,不为null,则先select,如果数据库存在,则update。如果不存在,则insert。

    @Transactional
    public <S extends T> List<S> saveAll(Iterable<S> entities) {
        Assert.notNull(entities, "Entities must not be null!");
        List<S> result = new ArrayList();
        Iterator var3 = entities.iterator();

        while(var3.hasNext()) {
            S entity = var3.next();
            result.add(this.save(entity));    //save方法是核心逻辑
        }

        return result;
    }

    @Transactional
    public <S extends T> S save(S entity) {
        if (this.entityInformation.isNew(entity)) {
            this.em.persist(entity);
            return entity;
        } else {
            return this.em.merge(entity);
        }
    }

解决方案:

  • 批量插入

优化方案为:当保存大量数据时,直接使用em进行持久化插入,省了一步查询操作。并且考虑到如果最后才提交所有数据,数据库的负载可能会比较大,故每100条记录就提交(flush)一次。

    @Autowired
    private EntityManager entityManager;

    private final int BATCH_SIZE = 1000;

    @Transactional(rollbackFor = Exception.class)
    public void addBatch(List<S> list) {
        int num = 0;
        
        for (S s : list) {
            entityManager.persist(s);    // insert插入操作(变成托管状态)
            int num += 1;
            
            if (i % BATCH_SIZE == 0) {
                entityManager.flush(); // 变成持久化状态
                entityManager.clear(); // 变成游离状态
            }
        }
    }
  • 批量更新

在确保数据已经存在的情况下,如果是批量更新可以如下代码代替上面的entityManager.persist(projectApplyDO);语句:

entityManager.merge(projectApplyDO);    //update更新操作


自动提交问题

JPA事务内Entity变更会自动更新到数据库

若启用了事务,则对于managed状态的entity,若在事务内该entity有字段的值发生了变化,则即使未调save方法,该entity的变化最后也会被自动同步到数据库,即sql update操作。即相当于在Persist Context flush时自动对各engity执行 save 方法。(org.hibernate.event.internal.AbstractFlushingEventListener中)

详情可参阅:https://blog.csdn.net/qq_38658642/article/details/90729827


实现自定义的 Repository 实现类

1.写一个与Repository接口同名的类,加上后缀 Impl,标注@Repository注解;这个类不需要实现任何接口,可以自动被扫描到。

2.在Repository接口中加入自定义的方法,比如:

public interface MyRepository extends JpaRespository<UserEntity, String>{
    // 自定义的方法
    public Page<Object[]> getByCondition(UserQueryModel u);
}

3.在实现类中,去实现在Repository接口中加入的自定义方法,会被自动找到

@Repository
public class MyRepositoryImpl{
    @Autowired 
    private EntityManager em; 

    // 实现在Repository接口中加入的自定义方法
    public Page<Object[]> getByCondition(UserQueryModel u){ 
        String hql = "select o.uuid,o.name from UserEntity o where 1=1 and o.uuid=:uuid"; 
        Query q = em.createQuery(hql); 
        q.setParameter("uuid", u.getUuid());         
        q.setFirstResult(0); 
        q.setMaxResults(1);      
        Page<Object[]> page = new PageImpl<Object[]>(q.getResultList(), new PageRequest(0,1), 3);  
        return page; 
    }
}


Repository 方式调用存储过程

Repository方式调用存储过程需要基于Entity实体类,在实体类上使用@NamedStoredProcedureQuery注解(需要数据库中有对应的表,可自动映射结果集)。详解 Hibernate EntityManger专题-JPA调用存储过程 条目。

import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.query.Procedure;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import com.labofjet.entity.A;
import com.labofjet.entity.APK;

@Repository
public interface ARepository extends JpaRepository<A, APK>{
    
    // 方式1。若用这种方式,方法名要与存储过程名一样。【推荐】
    @Procedure
    Integer plus1inout(Integer arg);
    
    @Procedure
    Object[] mytest();
    
    // 方式2。Procedure的name为实体类上@NamedStoredProcedureQuery注解中name的值
    @Procedure(name="User.plus1")
    Integer alias2(@Param("arg")Integer argAlias);    // @Param必须匹配@StoredProcedureParameter注释的name参数
    
    // 方式3。Procedure的procedureName参数必须匹配实体类上@NamedStoredProcedureQuery的procedureName的值
    @Procedure(procedureName="plus1inout")
    Integer alias3(Integer arg);
}

注意:返回类型必须匹配。in_only类型的存储过程返回是void,in_and_out类型的存储过程返回相应数据类型


JpaSpecificationExecutor 接口

spring data jpa 提供了 JpaSpecificationExecutor 接口,只要简单实现toPredicate方法就可以实现复杂的动态查询。

Specification是Spring对Criteria的封装。

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);
    // root:根参数,代表了可以查询和操作的实体对象的根,如果将实体对象比喻成表名,那root里面就是这张表里面的字段,是JPQL的实体字段,通过Path<Y>get(String var0)来获得操作的字段
    // criteriaQuery:代表一个specific的顶层查询对象,它包含着查询的各个部分,如: select、form、where、group by、order by 等,它提供了查询的的方法,常用的有 where、select、having
    // criteriaBuilder:用来构建CriteriaQuery的构建器对象,其实就相当于条件或条件组合
}

提供唯一的一个方法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


Repository 继承 JpaSpecificationExecutor接口

public interface TaskResposity extends JpaRespository<Task, Long>, JpaSpecificationExecutor<Task>{
}


调用

@Service
public class TaskService {
 
    @Autowired 
    private TaskRepository taskRepository ;
 
    /**
     * 多条件 + 分页排序 查询
     */
    public Page<Task> findBySepc(Task task, int page, int size){
         // 分页排序请求
        PageRequest pageReq = new PageRequest(page, size, new Sort(Direction.DESC,"createTime"));
        
        Page<Task> tasks = taskRepository.findAll(new Specification<Task>(){
            // 匿名内部类
            @Override
            public Predicate toPredicate(Root<Task> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
                //1.混合条件查询
                Path<String> exp1 = root.get("taskName");
                Path<Date>  exp2 = root.get("createTime");
                Path<String> exp3 = root.get("taskDetail");
                Predicate pre = builder.and(
                    builder.like(exp1, "%" + task.getTaskName + "%"),
                    builder.lessThan(exp2, new Date()));
                return builder.or(pre, builder.equal(exp3, task.getTaskDetail));
              
                /* 生成的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, "%"+ task.getProjectName +"%");

                /* 生成的sql语句为:
                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 ?
                   */       
        }, pageReq);
        return tasks;
    }
    
    // 多条件 + 排序 查询
    // 将多个的条件封装成数组的形式传递给接收多个参数的方法完成多条件查询
    public List<Task> findBySepc(Task task){
        /*Specification<UserEntity> spc = (root, query, builder)->{
            ArrayList<Predicate> list = new ArrayList<>();
            list.add(builder.equal(root.get("username"), task.getUsername));
            list.add(builder.equal(xrootget("password"), task.getPassword));
            return builder.and(list.toArray(new Predicate[list.size()]));    
        };*/
        
        Specification<UserEntity> spc = (root, query, builder) -> builder.and(
            builder.equal(root.get("username"), task.getUsername),
            builder.equal(xrootget("password"), task.getPassword)
        );
        
        List<UserEntity> list = userRepository.findAll(spc, Sort.by(Sort.Direction.DESC,"createTime"));
        
        list.forEach(System.out::println);
        
        return list;
    }

每次都要写一个类来实现Specification比较麻烦,可以将查询条件封装在专门的一个类中,使用时调用静态方法

public class TaskSpec {
 // 封装查询条件的静态方法
    public static Specification<Task> method1(Task task){
        return new Specification<Task>(){
            @Override
            public Predicate toPredicate(Root<Task> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                // 示例,未写具体的查询条件,入参从 task 中获取
                return null;
            }
        };
    }
}

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


相关文章
|
2天前
|
前端开发 Java
|
8月前
|
存储 Java 关系型数据库
JPA 注解及主键生成策略使用指南
JPA 注解及主键生成策略使用指南
133 0
|
2天前
|
Java 数据库连接 API
SpringBoot【问题 01】借助@PostConstruct解决使用@Component注解的类用@Resource注入Mapper接口为null的问题(原因解析+解决方法)
SpringBoot【问题 01】借助@PostConstruct解决使用@Component注解的类用@Resource注入Mapper接口为null的问题(原因解析+解决方法)
101 0
|
8月前
|
存储 Java 关系型数据库
JPA 注解及主键生成策略使用指南2
JPA 注解及主键生成策略使用指南2
|
JSON Java 数据格式
SpringBoot 实战:在 RequestBody 中优雅的使用枚举参数(原理篇)
在优雅的使用枚举参数(原理篇)中我们聊过,Spring对于不同的参数形式,会采用不同的处理类处理参数,这种形式,有些类似于策略模式。
564 0
SpringBoot 实战:在 RequestBody 中优雅的使用枚举参数(原理篇)
|
Java 网络架构
【SpringBoot】Rest映射及自定义_method
【SpringBoot】Rest映射及自定义_method
|
缓存 前端开发 安全
SpringBoot2.x系列教程19--利用InitBinder处理请求参数的绑定(一)
前言 我们在开发项目时,前后端之间需要密切配合,才能确保项目的稳定高效。前端通过URL接口给后端传递请求参数,后端根据这些不同的请求参数返回对应的响应信息。那么在这个传参和返回响应信息的过程中,会发生什么事情呢?我们能不能在这个过程中做一些干预操作呢?比如我们要添加一个学生的年龄信息,前端传递过来一个负数作为年龄参数,这个明显不合法,那么后端能不能对这个非法的参数做出一些处理呢? 所以今天 壹哥 就带大家在SpringBoot中,学习一些可以处理请求参数的技巧,确保我们的后端接口更加的安全健壮。 一. SpringMVC请求参数绑定流程 1. 请求参数绑定流程 我们在开发的时候,经常会从ht
295 0
|
IDE 前端开发 Java
为你Springboot项目自定义一个通用的异常(实用干货)
我们的项目通常来讲都是一个比较大的项目,包含了各种各样的服务。如果每个服务都以不同的方式返回异常信息,这样排查的时候就会比较凌乱。如果我们定义一个标准的异常处理体系。并在所有的服务中使用。那样开发起来就可以快速定位。页面也会更加的简单和直观。 本文开发环境基于springboot2.4,IDE环境是IDEA。这里从一个最简单的异常案例。逐步过渡到完全自定义自己的异常。 案例:Springboot查询数据库数据,发现返回的是null,就抛出异常。 OK,基于这个思想,看一下实现的思路。
321 0
为你Springboot项目自定义一个通用的异常(实用干货)
|
Java 数据库连接 程序员
深入了解数据校验(Bean Validation):ValidatorFactory和Validator等核心API【享学Java】(下)
深入了解数据校验(Bean Validation):ValidatorFactory和Validator等核心API【享学Java】(下)
|
Java 数据库连接 API
深入了解数据校验(Bean Validation):ValidatorFactory和Validator等核心API【享学Java】(上)
深入了解数据校验(Bean Validation):ValidatorFactory和Validator等核心API【享学Java】(上)
深入了解数据校验(Bean Validation):ValidatorFactory和Validator等核心API【享学Java】(上)