SpringDataJPA(一) 配置整合与基本使用

SpringDataJPA(一) 配置整合与基本使用

JPA 的全称是 Java Persistence API , 即 Java 持久层 API。Spring Data JPA 是 Spring 生态中提出的一套数据库 ORM (对象关系映射)规范、抽象标准,或者说它是对ORM框架实现的顶层抽象封装,目的是整合不同的ORM技术以遵循统一的规范,并允许开发者以面向对象的方式实现数据库操作。其特点如下:

  • 简化开发: 更好的无缝集成Spring生态,提供声明式的数据访问方式,以面向对象的概念实现数据库操作,以简化对数据库的CRUD操作;
  • 统一规范: 现有的ORM框架技术(Hibernate、MyBatis等)如雨后春笋,其使用方法、特性架构等各不相同,这极大提高了开发者的开发门槛;Spring Data JPA 的目标是提供一套标准及接口,以统一不同的ORM技术,使得开发者无需关心底层ORM框架的实现细节,只需遵循JPA的标准接口开发即可;
  • Hibernate: 现 JPA 的底层ORM框架基于的是全自动ORM框架Hibernate(MyBatis是半自动),期待以后会有更多的全自动ORM框架出现吧,以集成进Spring Data JPA;

一. SpringBoot 配置

1. 配置说明

配置项 说明 属性值
spring.jpa.generate-ddl 是否在SpringBoot项目启动时初始化库表(正向工程) - true:SpringBoot项目在启动时将实体类映射并创建到数据库表中。注意: 只会检测新增字段映射,但不会删除旧字段/实体类中已经没有的字段;
- false:默认值(不写该配置默认false),不开启数据库表的正向工程;
- 注意: 若通过此注解生成数据库表,则务必在@Column中指明每个字段的约束(columnDefinition:主键规则、非空、长度等,否则全部按默认值生成);
spring.jpa.show-sql 是否开启SQL打印 - true:开启,注意开启后可能会降低性能;
- false:默认值(不写该配置默认false);
spring.jpa.hibernate.naming.implicit-strategy 逻辑名称映射策略(一般不用) - ImplicitNamingStrategyJpaCompliantImpl:默认的命名策略,兼容JPA 2.0的规范,后者均继承自它;
- ImplicitNamingStrategyLegacyHbmImpl:兼容Hibernate老版本中的命名规范;
- ImplicitNamingStrategyLegacyJpaImpl:兼容JPA 1.0规范中的命名规范
- ImplicitNamingStrategyComponentPathImpl:大部分与ImplicitNamingStrategyJpaCompliantImpl相同,但是它在逻辑属性名称中包含了复合名称;
spring.jpa.hibernate.naming.physical-strategy 物理名称映射策略 - org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy:驼峰命名转下划线命名;
- org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl:直接映射,不做处理(大小写都不变);

2. 映射策略

Hibernate 5版本,引入了两种新策略来提供对命名规则的深度定制即 hibernate.naming.implicit-strategyhibernate.naming.physical-strategy ,二者都是Hibernate数据库配置项中默认命名映射策略,是对数据库表和实体属性字段映射的默认处理方式。实体名称映射到数据库中时,其分为两个步骤:

  • 第一个阶段:从对象模型中提取合适的逻辑名称,该逻辑名称可由用户指定,即通过@Column和@Table等注解完成(显式命名,非必要);也可以通过被Hibernate的ImplicitNamingStrategy指定,即表名隐式处理为类名,列名隐式处理为字段名(隐式命名,默认)。
  • 第二个阶段:将上述提取的逻辑名称进一步解析成数据库对应字段物理名称,物理名称是由Hibernate中的PhysicalNamingStrategy决定;

PhysicalNamingStrategyImplicitNamingStrategy的区别如下,一般只用一种即可:

  • 从处理的效果上来看:没有什么区别,但是从程序设计分层的角度来看,类似于MVC的分层,ImplicitNamingStrategy只管模型对象层次的处理,PhysicalNamingStrategy只管映射成真实的数据名称的处理,但是为了达到相同的效果,比如将userName映射成数据列时,在PhysicalNamingStrategy决定映射成user_name,但是在ImplicitNamingStrategy也可以做到。
  • 从处理的场景来看:无论对象模型中是否显式地指定列名或者已经被隐式决定,PhysicalNamingStrategy都会被应用; 但是对于ImplicitNamingStrategy,仅仅只有当没有显式地提供名称时才会使用,也就是说当对象模型中已经指定了@Table或者@Entity、@Column等name时,设置的ImplicitNamingStrategy并不会起作用。

二. 基本使用流程

1. 类与库表映射

1.1 实体定义

(1)@Entity

标注在实体类上,Spring会扫描被@Entity标注的实体类并交由JPA管理,在程序运行时识别并映射到对应的数据库表。

属性 含义 示例
name 指定实体类名称,默认为当前实体类的非限定类名。注意此处不是直接映射到数据库表名。 @Entity(name=“XXX”)
对应的JQPL中的实体类名是XXX。

注意: 经测试,不写Getter/Setter方法对JPA生成无影响,但在实际工作中为了代码可读性最好还是加上@Data。

(2)@Table

标注在实体类上,@Table与@Entity并列使用,用于指定实体类映射的数据库表名。默认不使用@Table情况下,JPA将通过配置的 hibernate.naming.physical-strategy 自动映射策略来将类名映射到数据库表。

属性 含义 示例
name 指定表名 @Entity
@Table(name=“order_info”)
对应的表名是order_info。
indexes Index数组类型,指定表的索引,Index属性如下:
- name:索引的名称
- columnList:参与索引的列名,多字段用’,'分隔
- unique:是否是唯一索引,默认false
@Entity
@Table(name = “order_info”, indexes = {
@Index(name = “idx_c”, columnList = “c”),
@Index(name = “idx_a_b”, columnList = “a, b”, unique = true)
})

1.2 主键定义

(1)@Id

标注在属性上,@Id 用于标记实体类中的属性映射为数据库主键(显式主键),该注解无特定属性值。注意: 实体必须声明主键@Id,但可以不声明生成策略 @GeneratedValue(无策略则新增数据必须持有主键)。

(2)@GeneratedValue

标注在属性上,@GeneratedValue 配合 @Id 一同使用,用于通过 strategy 属性值指定主键的生成策略。常用策略如下:

策略 含义
GenerationType.IDENTITY 主键由数据库自动生成,主键自动增长型,其中Mysql支持,Oracle 不支持这种方式
GenerationType.AUTO 默认主键生成策略,由JPA根据数据库类型自动选择合适的主键生成策略(Oracle 默认是序列化的方式,Mysql 默认是主键自增的方式)
GenerationType.SEQUENCE 根据底层数据库的序列来生成主键,条件是数据库支持序列,其中 Oracle支持,Mysql不支持

1.3 属性映射

(1)@Column

标注在属性上,@Column 用于指定实体属性与数据库表列的映射关系,并配置数据库字段的具体特征;默认不使用任何注解的属性会基于属性名自动通过配置的 hibernate.naming.physical-strategy 全局策略映射到数据库字段上。

属性 含义
name 指定映射到数据库表中的字段名
unique 指定是否唯一,默认为false
nullable 指定是否允许为null,默认为true
insertable 指定是否允许插入,默认为true
updatetable 指定是否允许更新,默认为true
length 指定字段存储的最大字符长度,仅对String类型(VARCHAR)的字段有效,默认值为255
columnDefinition 在创建表时,指定该字段创建的SQL语句,一般用于通过Entity生成表定义时使用,例如:@Column(columnDefinition = “VARCHAR(255) NOT NULL”);也就是说,如果DB中表已建好,则该属性没有必要使用

(2)@Enumerated

标注在属性上,@Enumerated 只能用于枚举类型的属性上,用于通过 value 属性值指定枚举类型的数据库存储方式。

存储方式 含义
EnumType.ORDINAL 将枚举值映射为整数(通常是枚举声明的顺序,从0开始),这是默认选项
EnumType.STRING 将枚举值映射为其名称(即枚举常量的字符串表示)

注意: 当使用 EnumType.ORDINAL时,如果枚举的顺序发生变化(例如,在枚举中插入或删除枚举常量),则可能会导致数据不一致或查询错误。因此,在枚举常量可能发生变化的情况下,建议使用 EnumType.STRING

(3)@Transient

用于属性上,@Transient 用于标记不映射到数据库表的属性。

1.4 映射规则

Spring Data JPA 按照实体类中定义的映射规则生成执行SQL,其生成格式如下:

select demoinfo0_.id as id1_0_, demoinfo0_.name as name2_0_ from demo_info demoinfo0_
insert into demo_info (name, id,......) values (?, ?,.....)

(1)映射匹配性

  • 数据库字段多于实体类属性时: 数据操作可以正常读写,只不过实体类中缺少映射的字段在数据库中为空,注意数据库中对应字段约束须允许为空才行;
  • 数据库字段与实体类属性存在不匹配或数据库字段少于实体类属性: findAllsave等系列方法都会报错,提示存在字段无法映射(手动选定操作字段也不行);
    • 解释如下:“即使你在查询时没有显式地使用不匹配列,Hibernate 仍然会在加载实体对象时尝试映射所有定义在实体类中的字段。如果实体类中定义了一个字段,但在数据库表中不存在对应的列,Hibernate 会在执行查询时抛出 SQLGrammarException”,因此如果实体类中存在不映射的字段,应该使用@Transient标注才会避免在CRUD中出现未知错误。

(2)默认映射关系

在默认情况下,JPA按照驼峰命名法对属性名称/实体名称进行拆分(小写形式),并使用“_”下划线命名转换。驼峰命名法(Camel Case) 是一种常用的命名约定,主要用于变量、方法和类的命名。它有两种主要形式:

  • 小驼峰命名法(lower camel case):第一个单词首字母小写,后续单词首字母大写。例如:firstName, lastName, userAddress
  • 大驼峰命名法(upper camel case):所有单词首字母大写。例如:FirstName, LastName, UserAddress

(3)数据类型选择

在 Spring Data JPA 中,实体类的属性数据类型可以是基本数据类型(如 int,long,boolean 等),也可以是对应的包装类(如 Integer,Long,Boolean 等);选择哪种类型取决于你的具体需求和偏好:

  • 基本数据类型: 内存占用小,但不能表示 null 值,如果数据库中的某个字段允许为 null,则使用基本数据类型会导致插入或读取数据时出现问题。
  • 包装类型: 可以表示 null 值,适用于数据库中允许 null 的字段,包装类为null插入不允许为null的表字段会报错(有默认值除外),但性能稍差(需要装箱/拆箱操作)。

(4)@Column 的属性配置

  • 初始化建表时:数据库表会根据配置属性值建立物理表规则,比如 nullable、length 等;
  • 已有表但属性值与表不匹配时:按照实体类中的约束进行校验,哪怕是数据库表中没有约束(比如允许为null),但实体类中存在约束(不允许为null),则按照实体类中约束进行判断通过后再映射执行SQL(ConvertException);
  • insertable与updatable行为:当该属性值为true时,则在执行insert和update语句时,生成的sql中不会包含该属性字段;该属性作用于生成SQL之前,nullable等作用于执行SQL时对字段的校验,因此当该属性与@Column其他属性产生冲突时,会先通过该属性过滤掉nullable的字段,nullable此时不会生效判断;

2. CRUD 数据操作

Spring Data JPA 通过 Repository 接口来实现数据持久化的 CRUD 操作,其中泛型T表示数据库映射的实体类、泛型ID表示主键的数据类型。Repository 接口的继承关系树如下:

在这里插入图片描述

  • Repository: Spring Data JPA 用于标识数据库抽象的顶层接口,聚合对实体的 CRUD;
  • CrudRepository: 继承了 Repository 接口的子接口,定义了基本的 CRUD 方法(比如保存、更新、查询、删除);
  • PagingAndSortingRepository: 继承自 CrudRepository 接口的子接口,除了具有CrudRepository基本功能外,还定义了基本的分页和排序查询方法;
  • QueryByExampleExecutor: 支持基于实例的动态查询的方法、允许通过传递一个示例对象 Example 来执行查询,而不需要手动编写查询语句;
  • JpaRepository: 同时继承了PagingAndSortingRepository和QueryByExampleExecutor的子接口,聚合功能的同时,对某些操作进行了拓展并将其他接口的方法返回值做了适配处理(List);

Spring Data JPA 通过 Repository 来简化数据库的持久化操作,大多数情况下我们只需要定义继承 JpaRepository 的Dao层子接口,指明其需要关联操作的实体类型和主键类型(泛型),就可以实现对数据库表的操作,甚至不需要提供任何注解(比如MyBatis的@Mapper);Spring 容器在启动时默认会扫描项目中的所有 Repository 接口,并将其注册到 Spring 容器中管理,并通过动态代理和AOP解析执行方法、生成SQL语句,使用 EntityManager 来执行数据库操作。Spring Data JPA 支持通过保留方法、约定方法命名规则的方式实现不写SQL即可操作数据,也支持通过原生SQL、JPQL的方式实现数据操作,接下来我们将分别介绍。

public interface IDemoInfoDao extends JpaRepository<DemoInfo, Long> {
    
    
    //crud
}

2.1 使用保留方法

Spring Data JPA 的 Repository 接口家族中预定义了通用场景下的基本 CRUD 方法,对于一些简单的数据库操作,我们的Dao层子接口在继承 JpaRepository 接口后,甚至不用添加任何其他方法就可以实现开箱即用。本质上,这些接口都是由默认实现类 SimpleJpaRepository 实现的,可以到该类下查看保留方法默认实现源码。常用的基本保留方法如下:

Return Method Description
List findAll() Returns all instances of the type T.
List findAllById(Iterable<ID> ids) Returns all instances of the type T with the given IDs. If some or all ids are not found, no entities are returned for these IDs(results unordered).
Optional findById(ID id) Retrieves an entity by its id.
S save(S entity) Saves a given entity. Return the saved entity.
S saveAndFlush(S entity) Saves an entity and flushes changes instantly. Return the saved entity.
List saveAll(Iterable<S> entities) Saves all given entities. Return the saved entities.
List saveAllAndFlush(Iterable<S> entities) Saves all entities and flushes changes instantly. Return the saved entities.
void deleteById(ID id) Deletes the entity with the given id. It is ignored if not found.
void delete(T entity) Deletes a given entity.
void deleteAll() Deletes all entities managed by the repository.
void deleteAllById(Iterable<? extends ID> ids) Deletes all instances of the type T with the given IDs. Entities that not found will be ignored.
void deleteAll(Iterable<? extends T> entities) Deletes the given entities.
void deleteAllInBatch() Deletes all entities managed by the repository in a batch call.
void deleteAllByIdInBatch(Iterable<ID> ids) Deletes the entities identified by the given ids using a single query.
void deleteAllInBatch(Iterable<T> entities) Deletes the given entities in a batch which means it will create a single query.
2.1.1 find 系列
// Hibernate: select demoinfo0_.id as id1_0_, demoinfo0_.des_info as des_info2_0_, demoinfo0_.name as name3_0_ from demo_info demoinfo0_
List<DemoInfo> demoAllList = demoInfoDao.findAll();
System.out.println(demoAllList);

// Hibernate: select demoinfo0_.id as id1_0_, demoinfo0_.des_info as des_info2_0_, demoinfo0_.name as name3_0_ from demo_info demoinfo0_ where demoinfo0_.id in (? , ? , ? , ?)
List<DemoInfo> demoAllListById = demoInfoDao.findAllById(Arrays.asList(1L,2L,3L,5L));
System.out.println(demoAllListById);

// Hibernate: select demoinfo0_.id as id1_0_0_, demoinfo0_.des_info as des_info2_0_0_, demoinfo0_.name as name3_0_0_ from demo_info demoinfo0_ where demoinfo0_.id=?
Optional<DemoInfo> demoById = demoInfoDao.findById(4L);
System.out.println(demoById.get());
2.1.2 save 系列

(1)save(S entity) 与 saveAll(Iterable entities)

//1、entity传递主键id,数据库对应主键数据不存在
// Hibernate: select demoinfo0_.id as id1_0_0_, demoinfo0_.des_info as des_info2_0_0_, demoinfo0_.name as name3_0_0_ from demo_info demoinfo0_ where demoinfo0_.id=?
// Hibernate: insert into demo_info (des_info, name, id) values (?, ?, ?)
demoInfoDao.save(new DemoInfo(2L, "jerry", "save test"));

//2、entity传递主键id,数据库对应主键数据存在,但数据未更改
// Hibernate: select demoinfo0_.id as id1_0_0_, demoinfo0_.des_info as des_info2_0_0_, demoinfo0_.name as name3_0_0_ from demo_info demoinfo0_ where demoinfo0_.id=?
demoInfoDao.save(new DemoInfo(2L, "jerry", "save test"));

//3、entity传递主键id,数据库对应主键数据存在,且数据已更改
// Hibernate: select demoinfo0_.id as id1_0_0_, demoinfo0_.des_info as des_info2_0_0_, demoinfo0_.name as name3_0_0_ from demo_info demoinfo0_ where demoinfo0_.id=?
// Hibernate: update demo_info set des_info=?, name=? where id=?
demoInfoDao.save(new DemoInfo(2L, "jerry2", "save test"));

//4、entity不传递主键id(null),未配置主键生成策略 GeneratedValue
// IdentifierGenerationException: ids for this class must be manually assigned before calling save()
demoInfoDao.save(new DemoInfo(null, "jack", "save test"));

//5、entity不传递主键id(null),配置主键生成策略 GeneratedValue(数据库同时配置)
// Hibernate: insert into demo_info (des_info, name) values (?, ?)
demoInfoDao.save(new DemoInfo(null, "jack", "save test"));

//6、saveAll 批量插入
// (1)主键+存在数据更改:select+update
// (2)主键+不存在数据更改:select
// (3)无主键插入:insert
//Hibernate: select demoinfo0_.id as id1_0_0_, demoinfo0_.des_info as des_info2_0_0_, demoinfo0_.name as name3_0_0_ from demo_info demoinfo0_ where demoinfo0_.id=?
//Hibernate: select demoinfo0_.id as id1_0_0_, demoinfo0_.des_info as des_info2_0_0_, demoinfo0_.name as name3_0_0_ from demo_info demoinfo0_ where demoinfo0_.id=?
//Hibernate: insert into demo_info (des_info, name) values (?, ?)
//Hibernate: update demo_info set des_info=?, name=? where id=?
demoInfoDao.saveAll(Arrays.asList(new DemoInfo(3L, "tom", "gogo"), new DemoInfo(4L, "jack", "save test"),
        new DemoInfo(null, "lisa", "save test test")));

List<DemoInfo> demoAllList = demoInfoDao.findAll();
System.out.println(demoAllList);

由上可以看出,save方法同时用于实现数据插入(insert)和数据更新(update)操作,其判断标准为entity对象是否已存在,而saveAll方法的实现则对entities进行遍历,并逐个调用save方法保存。逻辑如下:

  • 若保存entity传递了主键:先通过select查找数据实例是否存在:
    • 若存在:判断数据是否更新,若发生了修改则执行update更新操作;
    • 若不存在:执行insert插入操作;
  • 若保存entity未传递主键:直接执行insert插入操作(主键必须配置生成策略);

我们来进一步看一下save(S entity)saveAll(Iterable entities)SimpleJpaRepository 中的实现源码,save(S entity)saveAll(Iterable entities)本质上也是属于同根同源:

// 默认事务
@Transactional
public <S extends T> S save(S entity) {
    
    
	Assert.notNull(entity, "Entity must not be null.");
	if (this.entityInformation.isNew(entity)) {
    
    
    	this.em.persist(entity);
    	return entity;
	} else {
    
    
    	return this.em.merge(entity);
	}
}
// 默认事务
@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));
    }

    return result;
}

(2)saveAndFlush(S entity) 与 saveAllAndFlush(Iterable entities)

saveAndFlush(S entity) saveAllAndFlush(Iterable entities)在使用效果上跟普通的save方法基本一致,那么我们接着来看一下 saveAndFlush(S entity) saveAllAndFlush(Iterable entities)的源码如下:

// 默认事务
@Transactional
public <S extends T> S saveAndFlush(S entity) {
    
    
    S result = this.save(entity);
    this.flush();
    return result;
}
// 默认事务
@Transactional
public <S extends T> List<S> saveAllAndFlush(Iterable<S> entities) {
    
    
    List<S> result = this.saveAll(entities);
    this.flush();
    return result;
}

可以看出,saveAndFlush(S entity) saveAllAndFlush(Iterable entities)在实现中也是直接调用了save(S entity) saveAll(Iterable entities),只是在执行完成后主动调用了EntityManagerflush方法。flush的作用是直接将执行的sql发送至数据库服务器,否则执行语句将被暂存在 JPA 持久化上下文中,需等到事务提交的时才会真正发送执行SQL语句(第四节原理部分会进行分析)。

2.1.3 delete 系列

Spring JPA Ⅲ delete方法详解 :需要注意的是 delete 系列保留方法上也均添加了默认事务 @Transactional

(1)delete(T entity) 与 deleteById(ID id)

// Hibernate: select demoinfo0_.id as id1_0_0_, demoinfo0_.des_info as des_info2_0_0_, demoinfo0_.name as name3_0_0_ from demo_info demoinfo0_ where demoinfo0_.id=?
// Hibernate: delete from demo_info where id=?
demoInfoDao.deleteById(2L);


// Hibernate: select demoinfo0_.id as id1_0_0_, demoinfo0_.des_info as des_info2_0_0_, demoinfo0_.name as name3_0_0_ from demo_info demoinfo0_ where demoinfo0_.id=?
// Hibernate: delete from demo_info where id=?
// 注意:通过实体 entity 主键 id 进行删除操作,只设置主键即可
demoInfoDao.delete(new DemoInfo(4L,null,null)); 

delete(T entity)deleteById(ID id)方法同根同源,二者都是先通过主键查询(select)实体 entity ,然后基于查询结果执行删除操作(delete)。区别在于:

  • delete :若存在则删除,否则直接返回;
  • deleteById:若存在则调用 delete 方法删除,否则抛出异常EmptyResultDataAccessException(此时删除操作不会提交到数据库);

(2)deleteAll()、deleteAll(Iterable entities)与deleteAllById(Iterable ids)

//Hibernate: select demoinfo0_.id as id1_0_, demoinfo0_.des_info as des_info2_0_, demoinfo0_.name as name3_0_ from demo_info demoinfo0_
//Hibernate: delete from demo_info where id=?
//Hibernate: delete from demo_info where id=?
//Hibernate: delete from demo_info where id=?
demoInfoDao.deleteAll();

//Hibernate: select demoinfo0_.id as id1_0_0_, demoinfo0_.des_info as des_info2_0_0_, demoinfo0_.name as name3_0_0_ from demo_info demoinfo0_ where demoinfo0_.id=?
//Hibernate: select demoinfo0_.id as id1_0_0_, demoinfo0_.des_info as des_info2_0_0_, demoinfo0_.name as name3_0_0_ from demo_info demoinfo0_ where demoinfo0_.id=?
//Hibernate: delete from demo_info where id=?
//Hibernate: delete from demo_info where id=?
demoInfoDao.deleteAll(Arrays.asList(new DemoInfo(1L,"name_test","des_test"), new DemoInfo(3L,"name_test","des_test")));

//Hibernate: select demoinfo0_.id as id1_0_0_, demoinfo0_.des_info as des_info2_0_0_, demoinfo0_.name as name3_0_0_ from demo_info demoinfo0_ where demoinfo0_.id=?
//Hibernate: select demoinfo0_.id as id1_0_0_, demoinfo0_.des_info as des_info2_0_0_, demoinfo0_.name as name3_0_0_ from demo_info demoinfo0_ where demoinfo0_.id=?
//Hibernate: select demoinfo0_.id as id1_0_0_, demoinfo0_.des_info as des_info2_0_0_, demoinfo0_.name as name3_0_0_ from demo_info demoinfo0_ where demoinfo0_.id=?
//EmptyResultDataAccessException: No class com.study.springdatajpademo.pojo.DemoInfo entity with id 5 exists!
demoInfoDao.deleteAllById(Arrays.asList(1L,2L,5L));

List<DemoInfo> demoAllList = demoInfoDao.findAll();
System.out.println(demoAllList);

deleteAll()deleteAll(Iterable entities)deleteAllById(Iterable ids)方法都不是批量删除,在实现中三者都是先执行查询(select),然后遍历查询结果逐条执行删除操作(delete),因此大数据量下的删除效率并不高。三者区别如下:

  • deleteAll():通过findAll求出所有实体对象,然后遍历循环调用delete(entity)方法;
  • deleteAll(Iterable entities):遍历 entities 列表,然后循环调用 delete(T entity) 方法(逐条查询并删除);
  • deleteAllById(Iterable ids):遍历 ids 列表,然后循环调用 deleteById(Id id) 方法(逐条查询并删除),注意对于不存在的id会抛出异常EmptyResultDataAccessException(此时所有的删除操作不会提交到数据库);

(3)deleteAllInBatch()、deleteAllInBatch(Iterable entities)与deleteAllByIdInBatch(Iterable ids)

// Hibernate: delete from demo_info
demoInfoDao.deleteAllInBatch();

// Hibernate: delete from demo_info where id=? or id=?
demoInfoDao.deleteAllInBatch(Arrays.asList(new DemoInfo(1L,"name_test","des_test"), new DemoInfo(3L,"name_test","des_test")));

// Hibernate: delete from demo_info where id in (? , ? , ?)
demoInfoDao.deleteAllByIdInBatch(Arrays.asList(1L,2L,5L));

List<DemoInfo> demoAllList = demoInfoDao.findAll();
System.out.println(demoAllList);

可以看出InBatch系列的方法不再是逐条删除,而是批量删除,并且不再执行查询语句,在大数据量下的执行效率更高。在实现上,deleteAllInBatchdeleteAllByIdInBatch都是遍历entities或ids,对执行SQL字符串进行了拼接,使用or以及in关键字实现数据记录定位,而不再依靠find方法。

2.2 基于方法命名规则

除了上述已定义好的保留方法外,Spring Data JPA 还支持通过方法命名规则来创建自定义数据库操作,而无需声明任何SQL语句;我们只需要在 Respository 接口中按照方法命名规则创建数据库执行接口方法,程序在执行时会自动对符合规则的方法名进行关键词解析和映射,从而创建数据库执行操作。常用的执行关键字以及逻辑连接词分别如下:

Execute Keyword Description
find…By, read…By, get…By, query…By, search…By, stream…By General query method returning typically the repository type, a Collection or Streamable subtype or a result wrapper such as Page, GeoResults or any other store-specific result wrapper. Can be used as findBy…, findMyDomainTypeBy… or in combination with additional keywords.
exists…By Exists projection, returning typically a boolean result.
count…By Count projection returning a numeric result.
delete…By, remove…By Delete query method returning either no result (void) or the delete count(int). Warn:need to display declaration transactions
…First<number>…, …Top<number>… Limit the query results to the first <number> of results. This keyword can occur in any place of the subject between find (and the other keywords) and by.
…Distinct… Use a distinct query to return only unique results. Consult the store-specific documentation whether that feature is supported. This keyword can occur in any place of the subject between find (and the other keywords) and by.
Logic 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 is not null
Like findByFirstnameLike … where x.firstname like ?1
NotLike findByFirstnameNotLike … where x.firstname not like ?1
StartingWith findByFirstnameStartingWith … where x.firstname like ?1 (参数需加前缀 %)
EndingWith findByFirstnameEndingWith … where x.firstname like ?1 (参数需加后缀 %)
Containing findByFirstnameContaining … where x.firstname like ?1 (参数需被 % 包裹)
OrderBy findByAgeOrderByLastnameDesc … where x.age = ?1 order by x.lastname desc
Not findByLastnameNot … where x.lastname <> ?1
In 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.firstname) = UPPER(?1)

通过组合这些关键词来定义方法名就能实现数据库交互在一定程度上降低了开发者的使用门槛,这也是Sprng Data JPA的初衷和特点之一。关于这部分,我们将会在第四节中进一步分析方法名规则解析与构造的源码实现,需要注意的是在实际使用该方式时:

  • 实体类属性名称对应: 命名规则定义方法中声明的查询属性名称必须与实体类 Entity 中的类属性名称对应,而非数据库字段名或@Column映射名;

  • 小驼峰方式命名: 实体类中定义的类属性名称最好是以小写字符开始(JPA会自动将识别属性首字符转换为小写进行映射,但若是连续两个大写字母则不做处理),按照小驼峰方式命名;

  • 声明顺序一致: 命名规则方法在绑定查询参数时默认使用方法形参位置占位,因此方法参数的个数、顺序及数据类型均需要与定义的方法声明顺序一致;

2.3 引入SQL与JPQL

2.3.1 查询操作

(1)SQL与JPQL

JPA 同样支持通过直接数据库操作语句来操作数据,在 JPA 中数据库操作语句有两种表现形式:

  • JPQL:JPQL(Java Persistence Query Language)是 JPA 提供的一种面向对象的查询语言,其语法类似于 SQL,但它操作的是实体对象而不是数据库表,更加符合面向对象编程的理念,并且具有更好的跨数据库可移植性;

    SELECT <实体别名> FROM <实体类名> <实体别名> WHERE <条件表达式包含命名参数>
    
  • SQL:直接编写数据库特定的原生 SQL 语句,其依赖于特定数据库的 SQL 语法(后文将以SQL为主进行展开);

    SELECT <列名1>, <列名2> FROM <表名> WHERE <条件表达式包含命名参数>
    

在JPA中要使用直接数据库操作语句进行查询,我们只需在 Repository 自定义接口方法上声明 @Query 注解即可,JPQL 和 SQL 的声明方法区别如下:

// JPQL
@Query("select u FROM User u where u.name = ?1 and u.age > ?2")
List<User> findByNameAndAge(String name, int age);
// SQL
@Query("select * from user where user_name = ?1 and user_age > ?2", nativeQuery = true)
List<User> findByNameAndAge(String name, int age);

(2)参数位置绑定

在 @Query 中进行方法参数绑定时,支持通过索引参数绑定和命名参数绑定两种方式:

  • 索引参数绑定:?index指定占位符index,需要方法传递参数的个数与顺序均保持一致;
  • 命名参数绑定::paramName指定@Param(paramName)注解中的参数别名paramName,不要求顺序;
// 索引参数绑定
@Query("select * from user where user_name = ?1 and user_age > ?2", nativeQuery = true)
List<User> findByNameAndAge(String name, int age);
 		
// 命名参数绑定
@Query("select * from user where user_name = :name and user_age > :age", nativeQuery = true)
List<User> findByNameAndAge(@Param("name") String name, @Param("age") int age);

(3)部分查询

在Spring Data JPA 查询场景下,我们有时只需查询部分字段而非整个实体,尤其在数据库非常复杂或数据库字段较多的时候;但是这种情况下,直接使用实体接收来部分字段会产生接收异常,目前有两种常用方式如下:

  • 使用容器Map接收
// 查询单数据记录
@Query(value = "SELECT name,des_info FROM demo_info WHERE id = ?1",nativeQuery = true)
public Map<String, Object> findRawMapByObject(Long id);


// 查询多数据记录
@Query(value = "SELECT name,des_info FROM demo_info WHERE id >= ?1",nativeQuery = true)
List<Map<String, Object>> findRawMapByList(Long id);
  • 使用JPQL与投影(Projections)

Projections :: Spring Data JPA

投影(Projections)是指实体在部分属性字段上查询结果的聚合,其实现方式包括基于接口的投影(Interface-based Projections)和基于类的投影(Class-based Projections ,DTOs)。本节以基于类的投影为主,投影类的使用方式与接口投影完全相同,只是不会发生代理,也不能用嵌套投影。

在投影类实现中需要包含目标查询字段,并由公开的构造方法的参数名确定。在 JPQL 中,处理 DTO 的方法是使用 new 关键字来调用 DTO 的构造函数,JPQL 会将查询的结果映射到 DTO 对象中。其使用方法如下:

// User 实体的 DTO 投影类
@Data
public class UserDTO implements Serializable{
    
    
	private String dtoName;
	private int dtoAge;

	public UserDto(String dtoName, int dtoAge) {
    
    
		this.dtoName = dtoName;
		this.dtoAge = dtoAge;
	}
    
	// Getters and Setters
}

// Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    
	// 需要指定全限定类名的初始化构造方法
	@Query("SELECT new com.example.demo.UserDTO(u.name, u.age) FROM User u WHERE u.id > :id")
	List<UserDTO> findUserDtoByList(@Param("id") Long id);
}

需要注意的是:

  • 在 JPQL 中需要使用new关键字指定全限定类名的初始化构造方法,并将查询结果传递给构造函数;
  • 投影类(DTO)的构造函数需要匹配查询结果中字段的类型和顺序,投影类属性名称无需一致
2.3.2 数据修改操作

Spring Data JPA 提供了 @Query 注解实现通过 JPQL 或原生 SQL 来执行数据库查询操作,而如果需要修改数据而非查询数据,则需要再结合 @Modifying 注解来标识该方法属于数据修改类操作,否则 JPA 将无法识别并抛出异常SQLException: Can not issue data manipulation statements with executeQuery(),即无法使用Query的方式发出DML操作。需要注意的是:

  • @Modifying 注解的作用仅是与 @Query 注解结合使用来实现数据更新与修改,而JPA保留方法或基于命名规则的方法完全由JPA接管从而不需要此注解;
  • @Modifying 注解标识的数据修改类方法的返回值应该是voidint(表示修改语句所影响的数据记录数);
  • @Modifying 注解支持更新UPDATE及删除DELETE操作。除此之外,在JPQL场景中不支持插入INSERT操作( JPQL 语句不支持 INSERT,数据保存将由save完成),而在SQL场景中是支持INSERT操作的;
  • @Modifying 注解方法在调用时必须显式声明事务,否则将抛出TransactionRequiredException: Executing an update/delete query的异常(Spring Data JPA 默认是只读事务),事务可以在 Service 层的方法或直接在注解方法上添加;
// Hibernate: update demo_info set des_info = ? where id = ?
@Transactional
@Modifying
@Query(value = "update demo_info set des_info = ?1 where id = ?2", nativeQuery = true)
int updateDemoInfoById(String des, Long id);

// Hibernate: insert into demo_info(name,des_info) values(?, ?)
@Transactional
@Modifying
@Query(value = "insert into demo_info(name,des_info) values(?1, ?2)", nativeQuery = true)
int insertDemoInfo(String name, String desInfo);