Hank Blog

热爱生活,拥抱开源,乐于分享

0%

git 常用命令

一些认知

git 是一个复杂的软件,命令参数组合起来有上百种,掌握常用的就好了

阅读全文 »

一些认知

debian Debian计划是一个致力于创建一个自由操作系统的合作组织,我们所创建的这个操作系统名为Debian,
内核采用 linux 或 FreeBSD(类型unix),debian gnu 创建了大量的应用软件供使用者使用,并提供了apt,
软件包管路工具,伊恩·默多克 是 debian 创始人,ubuntu 是基于debian 的发行版

阅读全文 »

guava

Guava 项目是 Google 公司开源的 Java 核心库,它主要是包含一些在 Java 开发中经常使用到的功能,如数据校验、不可变集合、计数集合,集合增强操作、I/O、缓存、字符串操作等。并且 Guava 广泛用于 Google 内部的 Java 项目中,也被其他公司广泛使用,甚至在新版 JDK 中直接引入了 Guava 中的优秀类库,所以质量毋庸置疑。

阅读全文 »

范型的历史机源

Java 5之前 Java的设计者没有考虑范型引入jvm,后果是 Java 已经发展了很多年,形成历史技术债务。

Java 语言以及 JVM 推出后,由于它的跨平台性,通用的字节码,虚拟机,它能够运行在 Web 上,GC 等特性,迅速在全球流行开来,同时也吸引了很多学界和产业界人士的关注,其中就包括当时在德国卡尔斯鲁厄大学任教的 Martin Odersky(也是 scala 之父)。他和其在英国格拉斯哥大学的研究伙伴 Phil Wadler 于 1996 年共同开发了 Pizza 语言,该语言运行在 JVM 上,支持泛型,高阶函数和模式匹配等函数式编程功能。由于 Pizza 的目标是填补 Java 欠缺的功能,因此受到 Sun 公司的关注,于是 Sun 的工程师和 Martin Odersky、Phil Wadler 合作,借鉴 Pizza 的泛型实现开发了支持泛型的 Java 扩展版本:GJ,即 Generic Java。

GJ 所支持的泛型支持同类转化(homogeneous,指编译器把泛型中的类型参数都翻译为相同的 Object),异类转化(heterogeneous,指编译器把不同类型参数的泛型翻译为不同参数化的类),支持值类型范型。但由于要兼容不支持泛型的旧版本,Java 最终采用了类型擦除(type erasure)这一同类转化的方案,并引入了通配符,这样就构成了 2004 年发布的 Java 5 的泛型的基础

Java 的泛型仍在进化,JEP218 即 Project Valhalla 旨在支持值类型范型,并引入泛型可 reifiable 机制,即在运行时可读取类型参数的类型信息,但发布时间尚未确定。

阅读全文 »

YAML 简介

发音 ‘jæməl’ 是一种表达数据结构的语言,可以配合目前大部分的语言进行使用,不同于 xml 用嵌套的方式和
json 用{}大括号[]方括号的方式,采用类似 python 的空格缩进方式,是一种极简的方式进行描述数据结构,可以用
.yaml 或 .yml 进行结尾。

yaml 官方网站
yaml 语法在线校验1
yaml 语法在线校验2

官方有提供 java 的库 snakeyaml 来操作 .yml 文件的读取和写入.

基本语法

  • 大小写敏感
  • 使用缩进表示层级关系,缩进的空格数不重要,只要相同层级的元素能够对齐就行
  • 不要使用 Tab 代替空格,因为在不同的操作系统 Tab 表示的空格个数不能,导致层次混乱
  • /# 开头的是注释
阅读全文 »

aop 代理

aop 是对 oop 的一种补充。

代理的好处:

  • 控制本来的实现类的可访问性,透过代理对象访问,可以在代理类内创建原本的实现类,对用户而言减少创建的复杂性
  • 做横向拦截,给实现类在不修改代码的情况下,增强功能
    1. 业务日志切面:可以记录业务方法调用的痕迹
    2. 事务控制:通过切面可以声明式控制事务
    3. 权限校验:执行方法之前先校验当前登录用户是否有权调用
    4. 数据缓存:执行方法之前先从缓存中取,取到则直接返回不走业务方法
阅读全文 »

基于xml 的注入

application.xml

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<bean id="getServiceC" class="com.hank.beans.ServiceImplC" ></bean>
</beans>
阅读全文 »

事务的概念

事务就是一组逻辑操作的组合,它们执行的结果要么全部成功,要么全部失败。

事务有4个特性:

  • 原子性:一个事务就是一个不可再分解的单位,事务中的操作要么全部做,要么全部不做(事务中的任务要么全部提交要么全部回滚,不可能出现一部分提
    交一部分回滚。例如:用户 A 给用户 B 转账 100 元,A 账户减100,那么 B 账户一定加 100)
  • 一致性:事务执行后,所有的数据都应该保持一致状态。(还是以用户 A 给用户 B 转账 100 元为例,转账后 A 和 B 账户的总额和转账前是一致的)
  • 隔离性:多个数据库操作并发执行时,一个请求的事务操作不能被其它操作干扰,多个并发事务执行之间要相互隔离。隔离性强调的是并发的隔离,一个事
    务所做的操作,在最终提交前,对其他事务是不可见的,保证事务与事务之间不会冲突
  • 持久性:事务执行完成后,它对数据的影响是永久性的。持久性强调的是操作的结果,只要事务提交,数据就不会丢失,即使系统崩溃,事务也已经完成

数据库事务

针对数据库的并发操作,可能会出现一些事务的并发问题。事务并发操作中会出现三种问题:

  • 丢失更新:两个事务同时更新一行数据,最后一个事务的更新会覆盖掉第一个事务的更新,从而导致第一个事务更新的数据丢失,这是由于没有加锁造成的
  • 脏读:一个事务A 看到了另一个事务B 没有提交的数据
  • 不可重复读:一个事务读到了另一个事务已提交的修改的数据,对同一行数据查询两次,结果不一致
  • 幻读:一个事务读到了另一个事务已提交新增的数据,对同一张表查询两次,出现新增的行,导致结果不一致,出现了幻觉

针对上述三个问题,由此引出了数据库事务的隔离级别来解决,也就是事务4个特性里的隔离性:

  • read uncommitted 读未提交,所有事务都可以看到其他未提交事务的执行结果 —— 不解决任何问题
  • read committed 读已提交,一个事务只能看见已经提交事务所做的改变 —— 解决丢失更新,脏读
  • repeatable read 可重复读,从当前事务开始读取的数据到事务结束的这段时间,数据内容不受其他事务的影响 —— 解决丢失更新,脏读、不可重复读
  • serializable 可串行化,它通过强制事务排序执行(加锁),使之不可能相互冲突 —— 解决丢失更新,脏读、不可重复读、幻读,但可能导致大量的超时
    现象和锁竞争.

四种隔离级别,自上而下级别逐级增高,但并发性能逐级降低:

  • MySQL 中默认的事务隔离级别是 repeatable read
  • Oracle 、PostgresSQL 的默认事务隔离级别是 read committed

隔离级别越高,数据库事务并发执行性能越差,能处理的操作越少。因此在实际项目开发中为了考虑并发性能一般使用提交读隔离级别,它能避免丢失更新和脏读,尽管不可重复读和幻读不能避免,但可以在可能出现的场合使用应用层面的悲观锁或乐观锁来解决这些问题。

隔离级别 丢失更新 脏读(Dirty Read) 不可重复读(NonRepeatable Read) 幻读(Phantom Read)
未提交读(Read uncommitted) 可能 可能 可能 可能
已提交读(Read committed) 不可能 不可能 可能 可能
可重复读(Repeatable read) 不可能 不可能 不可能 可能
可串行化(Serializable ) 不可能 不可能 不可能 不可能

MySQL数据库是如何做到事务的原子性的呢

我们知道如果想要保证事务的原子性,就需要在异常发生时,对已经执行的操作进行回滚,在 MySQL 中,恢复机制是通过 回滚日志(undo log) 实现的,所有事务进行的修改都会先>先记录到这个回滚日志中,然后再执行相关的操作。如果执行过程中遇到异常的话,我们直接利用 回滚日志 中的信息将数据回滚到修改之前的样子即可!并且,回滚日志会先于数据持>久化到磁盘上。这样就保证了即使遇到数据库突然宕机等情况,当用户再次启动数据库的时候,数据库还能够通过查询回滚日志来回滚将之前未完成的事务。

数据库层面的锁

锁用来对数据进行锁定,我们可以从锁定对象的粒度大小来对锁进行划分,分别为行锁、页锁和表锁。

  • 行级锁是数据库中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。特点:开销大,加锁
    慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
  • 表级锁是数据库中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少。特点:开销小,加锁快;不会出现死锁;锁定粒度大,发出锁冲突的概率最
    高,并发度最低。
  • 页级锁是数据库中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。特点:开销和加
    锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般,时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般

不同数据库支持的锁力度不同,甚至同一种数据库不同的引擎支持的锁力度都不同,如下表所示

应用层面的乐观锁和悲观锁处理

无论是乐观锁和悲观锁,并非是数据库自身持有的锁类型(虽然悲观锁形式上很像独占锁),而是程序设计的一种思想,是一种类似数据库锁机制保护数据一致性的策略。

程序编写过程中,操作数据无论采用哪个类型的锁,都需要注意死锁的发生,一个死锁有可能对整个应用是致命的。死锁的本质是对资源竞争的一种失败表现,所以sql语句的编写过程中对于多表的操作最好采用一致的顺序来进行,或者尽量减少多表操作

乐观锁,其实没有加锁

认为一般情况下数据不会造成冲突,所以在数据进行提交更新时才会对数据的冲突与否进行检测。如果没有冲突那就OK;如果出现冲突了,则返回错误信息并让用户决定如何去做.
乐观锁是一种程序的设计思想,通过一个标识的对比来决定数据是否可以操作,现在普遍的做法是给数据加一个版本号或者时间戳的方式来实现乐观锁.

操作过程:在表中设计一个版本字段 version,第一次读的时候,会获取 version 字段的取值。然后对数据进行更新或删除操作时,会执行UPDATE … SET version=version+1 WHERE version=version。此时如果已经有事务对这条数据进行了更改,修改就不会成功

乐观锁比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。

悲观锁

每次获取数据的时候,都会担心数据被修改,所以每次获取数据的时候都会进行加锁,确保在自己使用的过程中数据不会被别人修改,使用完成后进行数据解锁。由于数据进行加锁,期间对该数据进行读写的其他线程都会进行等待

操作过程:在代码里对修改数据的代码进行加锁,synchronized

悲观锁比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。

事务的类型

数据库事务类型有本地事务和分布式事务:

  • 本地事务:就是普通事务,能保证单台数据库上的操作的ACID,被限定在一台数据库上
  • 分布式事务:涉及两个或多个数据库源的事务,即跨越多台同类或异类数据库的事务(由每台数据库的本地事务组成的),分布式事务旨在保证这些本地事务的所有操作的ACID,使事务可以跨越多台数据库

Java事务类型有JDBC事务和JTA事务:

  • JDBC事务:就是数据库事务类型中的本地事务,通过Connection对象的控制来管理事务
  • JTA事务:JTA指Java事务API(Java Transaction API),是Java EE数据库事务规范, JTA只提供了事务管理接口,由应用程序服务器厂(WebSphere Application Server)提供实现,JTA事务比JDBC更强大,支持分布式事务

Java EE事务类型有本地事务和全局事务:

  • 本地事务:使用JDBC编程实现事务
  • 全局事务:由应用程序服务器提供,使用JTA事务

按是否通过编程实现事务有声明式事务和编程式事务:

  • 声明式事务: 通过注解或XML配置文件指定事务信息
  • 编程式事务:通过编写代码实现事务。

Spring 支持两种方式的事务管理

Spring框架最核心功能之一就是事务管理,而且提供一致的事务管理抽象,这能帮助我们:
*提供一致的编程式事务管理API,不管使用Spring JDBC框架还是集成第三方框架使用该API进行事务编程;
*无侵入式的声明式事务支持。

Spring支持声明式事务和编程式事务事务类型。
Spring 关于事务的接口定义在这个包里

1
2
3
4
5
<dependency>-->
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.3.22</version>
</dependency>

Spring 框架中,事务管理相关最重要的 3 个接口如下:

  • PlatformTransactionManager:(平台)事务管理器,Spring 事务策略的核心。
    1
    2
    3
    4
    5
    6
    7
    8
    public interface PlatformTransactionManager extends TransactionManager {
    //返回一个已经激活的事务或创建一个新的事务
    TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;
    //提交
    void commit(TransactionStatus status) throws TransactionException;
    //回滚
    void rollback(TransactionStatus status) throws TransactionException;
    }
  • TransactionDefinition: 事务定义信息(事务隔离级别、传播行为、超时、只读、回滚规则)。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    public interface TransactionDefinition {

    //获取事务的传播性
    default int getPropagationBehavior() {
    return 0;
    }

    //获取事务的隔离级别
    default int getIsolationLevel() {
    return -1;
    }

    //获取事务的超时时间
    default int getTimeout() {
    return -1;
    }

    //获取事务是否只读
    default boolean isReadOnly() {
    return false;
    }

    //获取事务的名称
    @Nullable
    default String getName() {
    return null;
    }

    static TransactionDefinition withDefaults() {
    return StaticTransactionDefinition.INSTANCE;
    }
    }
  • TransactionStatus: 事务运行状态
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable {
    //获取是否存在保存点
    boolean hasSavepoint();
    //用于刷新底层会话中的修改到数据库,一般用于刷新如Hibernate/JPA的会话,可能对如JDBC类型的事务无任何影响;
    void flush();
    }
    public interface TransactionExecution {
    //获取是否是新事务
    boolean isNewTransaction();
    //设置事务回滚
    void setRollbackOnly();
    //获取事务是否回滚
    boolean isRollbackOnly();
    //获取事务是否完成
    boolean isCompleted();
    }

我们可以把 PlatformTransactionManager 接口可以被看作是事务上层的管理者,而 TransactionDefinition 和 TransactionStatus 这两个接口可以看作是事务的描述。>PlatformTransactionManager 会根据 TransactionDefinition 的定义比如事务超时时间、隔离级别、传播行为等来进行事务管理 ,而 TransactionStatus 接口则提供了
一些方法来获取事务相应的状态比如是否新事务、是否可以回滚等等。

PlatformTransactionManager:
通过这个接口,Spring 为各个平台如 JDBC(DataSourceTransactionManager)、Hibernate(HibernateTransactionManager)、JPA(JpaTransactionManager) JtaTransactionManager(分布式事务管理的支持,并将事务管理委托给Java EE应用服务器事务管理器)等都提供了对应的事务管理器

DataSourceTransactionManager 或 JdbcTransactionManager 用于Spring JDBC抽象框架、iBATIS或MyBatis框架的事务管理 的实现在如下 jar 包里

1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.23</version>
</dependency>

JdbcTransactionManager 类继承关系
TransactionManager 它是 SpringFramework 5.2 才新加的接口,由于响应式 jdbc 在 SpringFramework 中引入,在 SpringFramework 5.2 之后引入了响应式事务,由此产生了新的根接口

classDiagram
    I_TransactionManager <|--   I_PlatformTransactionManager
    I_PlatformTransactionManager <|-- I_ResourceTransactionManager
    I_PlatformTransactionManager <|..  C_AbstractPlatformTransactionManager
    C_AbstractPlatformTransactionManager <|--  C_DataSourceTransactionManager
    I_Serializable    <|..  C_AbstractPlatformTransactionManager
    I_InitializingBean <|..  C_ DataSourceTransactionManager
    I_ResourceTransactionManager <|.. C_DataSourceTransactionManager
    C_ DataSourceTransactionManager --|> C_JdbcTransactionManager

演示准备的例子
postgresql 数据库测试需要引入的包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!-- spring 对 jdbc 的封装  -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.23</version>
</dependency>

<!-- postgres 数据库驱动 -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.3.7</version>
<scope>runtime</scope>
</dependency>

<!-- spring-jdbc 需要的连接池 -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.0.1</version>
</dependency>

<!-- org.slf4j 做日志记录 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Configuration
@Order(1)
public class ConfigerDB {
//spring jdbc
@Bean("jdbcTemplate")
public JdbcTemplate getJdbcTemplate() {
var jdbcTemplate= new JdbcTemplate();
jdbcTemplate.setDataSource(getDataSource());
return jdbcTemplate;
}

//配置数据源
@Bean("hikariDataSource")
public HikariDataSource getDataSource() {
var dataSource = DataSourceBuilder.create();
dataSource.url("jdbc:postgresql://localhost:5432/postgres");
dataSource.driverClassName("org.postgresql.Driver");
dataSource.username("postgres");
dataSource.password("admin@123");
//连接池,演示目的不设置参数了
var pool= new HikariDataSource( );
pool.setDataSource(dataSource.build());
return pool;
}
}

对事务执行的方法进行 AOP 日志错误记录

1
2
3
4
5
6
7
8
9
10
11
12
@Aspect
@Component
public class MyAnnotationAOPAspect {
@Around("execution(* com.hank.postgres.Service*.*(..))")
public void around(ProceedingJoinPoint pjp) {
try {
pjp.proceed();
} catch (Throwable e) {
System.out.println(pjp.getTarget().getClass().getName() + "." + pjp.getSignature().getName() + ",执行错误,错误信息是" + e.getMessage());
}
}
}

编程事务-即类似于JDBC编程实现事务管理

主要依赖的实现:

  • JdbcTransactionManager 事务管理器
  • TransactionTemplate 事务模板,使用它可以完成编程式事务

TransactionTemplate 事务模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
@Order(2)
public class ConfigerTransantion {

private HikariDataSource hikariDataSource;

public ConfigerTransantion(HikariDataSource hikariDataSource){
this.hikariDataSource=hikariDataSource;
}

@Bean("transactionTemplate")
public TransactionTemplate getTransactionTemplate() {
return new TransactionTemplate(getJdbcTransactionManager());
}

@Bean("jdbcTransactionManager")
public JdbcTransactionManager getJdbcTransactionManager() {
return new JdbcTransactionManager(hikariDataSource);
}
}

编程式事务,利用事务模板去完成 TransactionTemplate transactionTemplate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@Component
public class Service {
private final TransactionTemplate transactionTemplate;
private final JdbcTemplate jdbcTemplate;

//注入
public Service(TransactionTemplate transactionTemplate,JdbcTemplate jdbcTemplate){
this.transactionTemplate = transactionTemplate;
this.jdbcTemplate=jdbcTemplate;
}

public void insert() throws Exception {
final Exception[] exception = {new Exception()};
var result = transactionTemplate.execute(p->{
var i= jdbcTemplate.update("insert into \"order\" (id, name)values (?,?)",new Object[]{200,"name1"});
var j = 0;
//回滚点
var savePoint= p.createSavepoint();
try {
j=jdbcTemplate.update("insert into \"order\" (id, name)values (?,?)",new Object[]{201,"name2"});
var bar =1/0;
}catch (Exception e){
j=0;
//回滚代码到 savePoint 对象定义执行的代码,
p.rollbackToSavepoint(savePoint);
//rollbackToSavepoint 意思是无论有无错误,都只执行回调点前的代码
//不能 throw e ,这样回滚效果就会失效
exception[0] =e;
}finally {
p.releaseSavepoint(savePoint);
}
return i+j;
});
System.out.println("更新成功次数:"+result);
if(exception.length==1) throw exception[0];
}
}

//测试
@ComponentScan({"com.hank"})
@EnableAspectJAutoProxy(exposeProxy = true)
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
var bean =context.getBean(Service.class);
bean.insert();
context.close();
}
}

声明式事务 注解或xml 配置的方式

主要依赖的实现:

  • @Transactional(“jdbcTransactionManager”) 一句注解,注解的参数是事务管理器的 beanName
  • @EnableTransactionManagement 使能 @EnableTransactionManagement

注解的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 声明式事务 需要有事务管理器,事务模板内部代码自己生成
@Configuration
@Order(2)
public class ConfigerTransantion {

private HikariDataSource hikariDataSource;

public ConfigerTransantion(HikariDataSource hikariDataSource){
this.hikariDataSource=hikariDataSource;
}

@Bean("jdbcTransactionManager")
public JdbcTransactionManager getJdbcTransactionManager() {
return new JdbcTransactionManager(hikariDataSource);
}
}

@Component
public class Service1 {
private final JdbcTemplate jdbcTemplate;

//注入
public Service1(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}

//注解声明式事务
@Transactional("jdbcTransactionManager")
public void insert() {
var i = jdbcTemplate.update("insert into \"order\" (id, name)values (?,?)", new Object[]{200, "name1"});
var j = 0;
var savePoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint();
try {
j = jdbcTemplate.update("insert into \"order\" (id, name)values (?,?)", new Object[]{201, "name2"});
//var bar =1/0;
} catch (Exception e) {
j = 0;
TransactionAspectSupport.currentTransactionStatus().rollbackToSavepoint(savePoint);
//编程式事务抛出错误,回滚会失效,注解不会,因为此时的事务访问是方法,而不是编程式事务的回调内的事务范围
throw e;
} finally {
TransactionAspectSupport.currentTransactionStatus().releaseSavepoint(savePoint);
System.out.println("更新成功次数:" + (i + j));
}
}
}

@ComponentScan({"com.hank"})
@EnableAspectJAutoProxy()
@EnableTransactionManagement
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
var bean =context.getBean(Service1.class);
bean.insert();
context.close();
}
}

事务属性

@Transactional配置详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
@AliasFor("transactionManager")
String value() default "";
//指定事务管理器名字,默认为transactionManager,当使用其他名字时需要明确指定
@AliasFor("value")
String transactionManager() default "";

String[] label() default {};
//指定事务传播行为,默认为Required,使用Propagation.REQUIRED指定
Propagation propagation() default Propagation.REQUIRED;
//指定事务隔离级别,默认为“DEFAULT”,使用Isolation.DEFAULT指定;
Isolation isolation() default Isolation.DEFAULT;
//指定事务超时时间,以秒为单位,默认-1表示事务超时将依赖于底层事务系统
int timeout() default -1;

String timeoutString() default "";
//指定事务是否只读,默认false表示事务非只读
boolean readOnly() default false;
//指定一组异常类,遇到该类异常将回滚事务
Class<? extends Throwable>[] rollbackFor() default {};
//和 rollbackFor()功能一样
String[] rollbackForClassName() default {};
//指定一组异常类,即使遇到该类异常也将提交事务,即不回滚事务
Class<? extends Throwable>[] noRollbackFor() default {};
//和 noRollbackFor()功能一样
String[] noRollbackForClassName() default {};
}

使用@Transactional注解事务管理需要特别注意以下几点:

  1. 在Spring代理机制下(不管是JDK动态代理还是CGLIB代理),“自我调用”同样不会应用相应的事务属性
  2. 默认只对RuntimeException异常回滚;
  3. 在使用Spring代理时,默认只有在public可见度的方法的@Transactional 注解才是有效的,其它可见度(protected、private、包可见)的方法上即使有@Transactional 注解也不会应用这些事务属性的,Spring也不会报错,如果你非要使用非公共方法注解事务管理的话,可考虑使用AspectJ。

与其他AOP通知协作

Spring声明式事务实现其实就是Spring AOP+线程绑定实现,利用AOP实现开启和关闭事务,利用线程绑定(ThreadLocal)实现跨越多个方法实现事务传播。

由于我们不可能只使用一个事务通知,可能还有其他类型事务通知,而且如果这些通知中需要事务支持怎么办?这就牵扯到通知执行顺序的问题上了,因此如果可能与其他AOP通知协作的话,而且这些通知中需要使用声明式事务管理支持,事务通知应该具有最高优先级。

事务隔离级别

用来解决并发事务时出现的问题,其使用TransactionDefinition中的静态变量来指定:

  • ISOLATION_DEFAULT:默认隔离级别,即使用底层数据库默认的隔离级别;
  • ISOLATION_READ_UNCOMMITTED:未提交读;
  • ISOLATION_READ_COMMITTED:提交读,一般情况下我们使用这个;
  • ISOLATION_REPEATABLE_READ:可重复读;
  • ISOLATION_SERIALIZABLE:序列化
1
@Transactional(transactionManager="jdbcTransactionManager",isolation = Isolation.READ_COMMITTED)

事务传播行为

事务的传播指的的是如下代码 f1 方法支持事务,在f1方法内部调用另一个事务方法f2,此时是吧 f1 和 f2 这俩个方法的事务合并成一个事务。还是分开呢,如果分开:

  • f2 执行失败 要不要影响到 f1
  • 反过来 f1 失败了还要不要执行 f2
1
2
3
4
5
6
7
class Bar{
@Transactional("jdbcTransactionManager")
public void f1(){ f2()}
@Transactional("jdbcTransactionManager")
public void f2(){
}
}

事务传播行为的7种策略:

事务的默认名称和方法名一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class OutService {
@Autowired
InnerService innerService;
// 外层事务 do
public void doo() {
//事务1 do1
innerService.do1(
//TransactionSynchronizationManager.getCurrentTransactionName() 可以获取当前的事务名称
System.out.println(TransactionSynchronizationManager.getCurrentTransactionName());
);
//事务2 do2
innerService.do2();

throw new Exception("");
}
}
  1. REQUIRED: spring 事务的 默认值 ,支持当前事务,如果不存在,就创建一个
    do1 ,do2 都是 REQUIRED,举例:

    • 如果当前 doo 事务不存在,则会开启俩个新的事务 do1 和 do2 ,do1 和 do2 互相不影响,do1 或 do2 出现异常不会影响到 doo
    • 如果当前 doo 事务存在,则方法会运行在当前事务 doo 中,其中任何一个有异常, doo, do1 ,do2 方法会一起回滚
  2. REQUIRES_NEW :如果有事务存在,挂起当前事务,创建一个新的事务。
    do1 ,do2 是 REQUIRES_NEW,举例:

    • 如果当前 doo 事务不存在,开启俩个新的事务 do1 和 do2 ,do1 和 do2 互相不影响
    • 如果当前 doo 事务存在,挂起当前的事务 doo,并开启俩个新的事务 do1 和 do2 ,doo, do1 和 do2 事务互相不影响,
  3. SUPPORTS :如果当前有事务运行,则方法会运行在当前事务中;如果当前没有事务运行,则不会创建新的事务(即不运行在事务中)

  4. NOT_SUPPORTED :不支持事务,如果当前有事务运行,则会将该事务挂起(暂停);如果当前没有事务运行,则它也不会运行在事务中

  5. MANDATORY :如果没有事务,则直接抛出异常

  6. NEVER :当前方法不允许运行在事务中,如果当前已经有事务运行,则抛出异常

  7. NESTED :嵌套
    这个 NESTED 是最特殊的,它就是基于保存点 SavePoint 的传播行为。它的定义是:如果当前没有事务运行,则开启一个新的事务;如果当前已经有事务运行,则会记录一个保存点,并继续运行在当前事务中。如果子事务运行中出现异常,则不会全部回滚,而是回滚到上一个保存点。可以发现,由于在 NESTED 的执行需要依赖关系型数据库的 SavePoint 机制,所以这种传播行为只适用于 DataSourceTransactionManager (即基于数据源的事务管理器)。而且 NESTED 通常都在同一个数据源中实现,对于多数据源,或者分布式数据库的话,NESTED 是搞不定的(假设两个 Service 依赖的 Dao 分别操作不同的数据库,那实际上已经形成分布式事务了,Spring 搞不定的)

spring boot

url 路径匹配

阅读全文 »

hibernate-基础

hibernate 作为 ORM 解决方案,非常强调领域模型的概念,是非侵入式的,Hibernate不要求持久化类实现任何接口或继承任何类,POJO即可

hibernate 官方文档-https://docs.jboss.org/hibernate/orm/5.6/userguide/html_single/Hibernate_User_Guide.html

hibernate 总体的架构图
如下图所示,hibernate 实现了java persistence api(JPA),同时也自己扩展了接口 hibernate native api

graph LR
 userDataAccessLayer(user data access layer)
 javaPersistenceApi(java persistence api)
 hibernateNativeApi(hibernate native api)
 hibernate
 jdbc
 database
 
 userDataAccessLayer -.-> javaPersistenceApi
 userDataAccessLayer -.-> hibernateNativeApi

 subgraph hibernate-core
  javaPersistenceApi -.-> hibernate
  hibernateNativeApi -.-> hibernate
 end

 subgraph database-jdbc
   hibernate-.->jdbc
   jdbc-.->database
 end

 style hibernate-core fill: #f9f, stroke: #333, stroke-width: 1px;
 style database-jdbc fill: #ee9f, stroke: #333, stroke-width: 1px;

Hibernate API 最基础的 API

  1. SessionFactory(会话工厂)
    是一个线程安全的,immutable(终态的),它是一个代理,表示应用程序域,SessionFactory 的建立代价很大,所以一个 应用只能有一个 SessionFactory,SessionFactory 维护 ,Hibernate 的所有 Session(会话),二级缓冲,连接池,事务等等。如果应用需要使用Hibernate访问多个数据库,则需要对每一个数据库使用一个SessionFactory。

  2. Session(会话)
    Session(会话)是一个单线程,短生命周期的对象,是按”Unit of Work(工作单元),Session 维护了一级缓存,在第一次发送SQL语句查询后第二次直接使用缓存中的数据,不会再发送SQL。除非session缓存被清空,平常的 crud 就通过它。session是一个轻量级对象,Session实例并不是线程安全的,因此应该被设计为每次只能在一个线程中使用。通常将每一个Session实例和一个数据库事务绑定,也就是说,每执行一个数据库事务,都应该先创建一个新的Session实例

  3. Transaction(事务)
    那么每个Session的操作,是一个独立的事务,是Hibernate的数据库事务接口,它对底层的事务接口做了封装,底层事务接口包括JDBC事务和JTA(Java Transaction API)事务

  4. Query (接口)
    是Hibernate的查询接口,用于向数据库查询对象,以及控制执行查询的过程。Query实例包装了一个HQL(Hibernate Query Language)查询语句,HQL查询语句与SQL查询语句有些相似,但HQL查询语句是面向对象的,它引用类名及类的属性名,而不是表名及表的字段名。
    Query接口让你方便地对数据库及持久对象进行查询,它可以有两种表达方式:HQL语言或本地数据库的SQL语句。Query经常被用来绑定查询参数、限制查询记录数量,并最终执行查询操作。

  5. Criteria(接口)

  6. Configuration(配置类)
    Configuration 类的作用是对Hibernate 进行配置,以及对它进行启动。在Hibernate 的启动过程中,Configuration 类的实例首先定位映射文档的位置,读取这些配置,然后创建一个SessionFactory对象。虽然Configuration 类在整个Hibernate 项目中只扮演着一个很小的角色,但它是启动hibernate 时所遇到的第一个对象。

如图中的根接口都是在 jpa 中定义的

classDiagram
    I_EntityManagerFactory <|--  I_SessionFactory
     I_SessionFactory <|..    C_SessionFactoryImpl
    I_EntityManager  <|--  I_Session
    I_Session  <|..   C_SessionImpl
    I_EntityTransaction  <|--  I_Transaction
    I_Transaction  <|..  C_TransactionImpl

环境准备

mysql 测试通过,postgres 用的是 42.5.1 版本 测试没有通过 ,无法加载 postgres drive

1
2
3
4
5
6
7
8
9
10
11
12
13
<!--数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>

<!--hibernate-->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.6.14.Final</version>
</dependency>

基础类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
class Config {
/**
* 输出所有管理的实体
* @param context
* @return
*/
public static Map.Entry<Object, EntityEntry>[] getManagedEntities(PersistenceContext context) {
var map = context.reentrantSafeEntityEntries();
return map;
}

public void extracted(Consumer<Configuration> configurer, Consumer<Session> handler) {

var cfg = new Configuration();

//.configure() 在classpath根路径下搜索名为hibernate.cfg.xml的文件,如果没有找到将会抛出异常, 也可以指定 configure 方法参数为指定的路径
//var cfg = new Configuration().configure();

/*
hibernate.cfg.xml
<hibernate-configuration>
<session-factory>
<property name="xxx">yyy</property>
//数据库和类映射 xml 文件
<mapping resource="xxx.hbm.xml"/>
</session-factory>
</hibernate-configuration>
*/

// dialect 指明具体联系的数据库类型,因为同一个数据库因为版本的不同,标准也会有差异, SQL 标准各个厂家完全遵守的
cfg.setProperty("hibernate.dialect", "org.hibernate.dialect.MySQL8Dialect");
cfg.setProperty("hibernate.connection.driver_class", "com.mysql.cj.jdbc.Driver");
cfg.setProperty("hibernate.connection.url", "jdbc:mysql://localhost:3306/MyDB");
cfg.setProperty("hibernate.connection.username", "root");
cfg.setProperty("hibernate.connection.password", "root");
cfg.setProperty("hibernate.show_sql", "true");
cfg.setProperty("format_sql", "true");
cfg.setProperty("use_sql_comments", "true");

cfg.setProperty("hibernate.hbm2ddl.auto","update");

//设置全局拦截器
cfg.setInterceptor(new MyInterceptor());


//外部继续配置
configurer.accept(cfg);

SessionFactory factory = null;
Session session = null;

//ServiceRegistry 是 Service 的注册表, 它为Service提供了一个统一的加载 / 初始化 / 存放 / 获取机制.
var serviceRegistry=new StandardServiceRegistryBuilder().applySettings(cfg.getProperties()).build();

try {
factory = cfg.buildSessionFactory(serviceRegistry);
var ii= factory.unwrap(SessionFactoryImplementor.class);
ii.getServiceRegistry().getService(EventListenerRegistry.class).appendListeners(EventType.FLUSH,new MyEvent());


session = factory.openSession();

if (session.isConnected()) {
System.out.println("连接已通");
//关于 session 的测试
handler.accept(session);
}
} catch (Exception ex) {
ex.printStackTrace();
} finally {
if (session != null) {
session.close();
}
if (factory != null) {
factory.close();
}
}
}


}

class MyInterceptor extends EmptyInterceptor {
/**
* 脏数据检查,可以知道那些字段发什么了什么变化
* @param entity
* @param id
* @param currentState
* @param previousState
* @param propertyNames
* @param types
* @return
*/
@Override
public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState, String[] propertyNames, Type[] types) {
for (int i = 0; i < propertyNames.length; i++) {
var type = types[i];
var newValue= currentState[i];
var oldValue= previousState[i];
System.out.println("Cargo 的属性:"+propertyNames[i] +
", 属性类型:"+type.getName()+
", id:"+id+
", old value:"+oldValue+
", new value:"+newValue);
//在持久化到数据库之前,还可以再次修改属性值
}

//返回 true ,在此方法里对 currentState 的修改会持久化到数据库.
//返回 false, 不会把对 currentState 修改持久化到数据库
return false;
}
}

简单的测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Programmer {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
var config =new Config();
config.extracted(cfg -> {
/*
可以代替 xml 方式的映射文件 <mapping resource="xxx.hbm..xml" /> 持久化类
如果有多个实体类,一个个添加是不是很麻烦
只能是自己去封装了
*/
cfg.addAnnotatedClass(Cargo.class);
cfg.addAnnotatedClass(Person.class);
},

//业务逻辑
session -> {
session.beginTransaction();
var cargo = session.get(Cargo.class,1);

var context = session.unwrap(SessionImplementor.class).getPersistenceContext();
//查询当前可管理实体
System.out.println("管理的实体个数:"+context.getNumberOfManagedEntities());
Arrays.stream(Config.getManagedEntities(context)).forEach(p->{
System.out.println(p.getKey());
System.out.println(p.getValue());
});

session.getTransaction().commit();
});
}

脏数据

对被 session 管理的数据进行更改后的数据是脏数据,也就是和数据库的数据不同步了,当提交事务的时候,脏数据会自动同步到数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
session.beginTransaction();

var cargo = session.get(Cargo.class,1);
var context = session.unwrap(SessionImplementor.class).getPersistenceContext();

//查询当前可管理实体
System.out.println("管理的实体个数:"+context.getNumberOfManagedEntities());
Arrays.stream(Config.getManagedEntities(context)).forEach(p->{
System.out.println(p.getKey());
System.out.println(p.getValue());
});

System.out.println("数据是否改变"+session.isDirty());
//false

cargo.setName("dirt");

System.out.println("数据是否改变"+session.isDirty());
//true

session.getTransaction().commit();

System.out.println("数据是否改变"+session.isDirty());
//false

/*
连接已通
Hibernate: select cargo0_.id as id1_0_0_, cargo0_.name as name2_0_0_ from Cargo cargo0_ where cargo0_.id=?
管理的实体个数:1
com.hank.entity.Cargo@3d1f558a
EntityEntry[com.hank.entity.Cargo#1](MANAGED)
数据是否改变false
Cargo 的属性:name, 属性类型:string, id:1, old value:bag, new value:dirt
数据是否改变true
Cargo 的属性:name, 属性类型:string, id:1, old value:bag, new value:dirt
Hibernate: update Cargo set name=? where id=?
数据是否改变false
*/

JPA 和 Hibernate API 通过 unwrap 互相转

1
2
3
var context = session.unwrap(SessionImplementor.class).getPersistenceContext();
var entityManager =session.unwrap(EntityManager.class);
var sessionFactory=session.getEntityManagerFactory().unwrap(SessionFactory.class);

对象的状态

session 的主要工作就是让实体对象在不同的状态之间进行互相切换

  1. 瞬时状态:实体-没有ID,没有与Session关联

  2. 持久化状态:实体-有ID,与Session关联,特点:持久化状态对象的任何改变都会同步到数据库中
    在Hibernate中,持久对象是具有数据库标识且在Session中的实例,持久对象有一个主键值设为数据库标识符。持久对象可能来自瞬时对象被save或saveOrUpdate之后形成,也可能是通过get()或load()等方法从数据库中检索出来的对象。

  3. 游离态:实体-有ID,没有与Session关联.游离态实例表示曾经与某个持久化上下文发生过关联,不过那个上下文被关闭了。它拥有持久化标识,并且在数据库中通常还存在一个对应的行,只是它已经不在持久化层的管理之下。

  4. 删除状态

EntityEntry 上下文跟踪的实体信息对象

脏数据及对象的缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
session.beginTransaction();
var context=session.unwrap(SessionImplementor.class).getPersistenceContext();
//查询当前可管理实体
System.out.println("管理的实体个数:"+context.getNumberOfManagedEntities());
Arrays.stream(Config.getManagedEntities(context)).forEach(p->{
System.out.println(p.getKey());
System.out.println(p.getValue());
});

var cargo = session.get(Cargo.class,1);
cargo.setName("tool");
System.out.println("session 是否有脏数据:"+session.isDirty());
//属性缓存的作用,根据缓存的状态,提前生成SQL 语句,但是不会执行,执行要提交事务的时候
//会生成 更新语句,同时会更新缓存的状态
session.flush();

System.out.println("session 是否有脏数据:"+session.isDirty());

//会在缓存里找数据
var cargo1 = session.get(Cargo.class,1);
System.out.println(cargo.equals(cargo1));


System.out.println("管理的实体个数:"+context.getNumberOfManagedEntities());
Arrays.stream(Config.getManagedEntities(context)).forEach(p->{
System.out.println(p.getKey());
System.out.println(p.getValue());
});


session.getTransaction().commit();

清除管理的实体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
session.beginTransaction();
var context=session.unwrap(SessionImplementor.class).getPersistenceContext();
//查询当前可管理实体
System.out.println("管理的实体个数:"+context.getNumberOfManagedEntities());
Arrays.stream(Config.getManagedEntities(context)).forEach(p->{
System.out.println(p.getKey());
System.out.println(p.getValue());
});

var cargo = session.get(Cargo.class,1);
cargo.setName("tools");
System.out.println("session 是否有脏数据:"+session.isDirty());

//清除 session 管理的所有实体
session.clear();

System.out.println("管理的实体个数:"+context.getNumberOfManagedEntities());
Arrays.stream(Config.getManagedEntities(context)).forEach(p->{
System.out.println(p.getKey());
System.out.println(p.getValue());
});

var cargo1 = session.get(Cargo.class,1);
System.out.println(cargo1.getName());
System.out.println("是否是缓存:"+cargo.equals(cargo1));

System.out.println("管理的实体个数:"+context.getNumberOfManagedEntities());
Arrays.stream(Config.getManagedEntities(context)).forEach(p->{
System.out.println(p.getKey());
System.out.println(p.getValue());
});
/*
管理的实体个数:0
Hibernate: select cargo0_.id as id1_0_0_, cargo0_.name as name2_0_0_ from Cargo cargo0_ where cargo0_.id=?
session 是否有脏数据:true
管理的实体个数:0
Hibernate: select cargo0_.id as id1_0_0_, cargo0_.name as name2_0_0_ from Cargo cargo0_ where cargo0_.id=?
tool
是否是缓存:false
管理的实体个数:1
com.hank.entity.Cargo@4f4c88f9
EntityEntry[com.hank.entity.Cargo#1](MANAGED)
*/

evict 在 session 缓存中清除一个持久化对象

1
2
3
4
5
6
7
8
9
10
var cargo=session.get(Cargo.class,1);
session.evict(cargo);
//jpa 方法 detach 同样的效果
//session.detach(cargo);
System.out.println("管理的实体个数:"+context.getNumberOfManagedEntities());
Arrays.stream(Config.getManagedEntities(context)).forEach(p->{
System.out.println(p.getKey());
System.out.println(p.getValue());
});
//1

session 对象

get , load 区别

get 立即加载,load 延迟加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
session.beginTransaction();
var context=session.unwrap(SessionImplementor.class).getPersistenceContext();

//查询当前可管理实体
System.out.println("管理的实体个数:"+context.getNumberOfManagedEntities());
Arrays.stream(Config.getManagedEntities(context)).forEach(p->{
System.out.println(p.getKey());
System.out.println(p.getValue());
});


//立即加载
var cargo1=session.get(Cargo.class,1);
//懒加载,在使用属性的时候才会加载
var cargo2=session.load(Cargo.class,2);


//查询当前可管理实体
System.out.println("管理的实体个数:"+context.getNumberOfManagedEntities());
Arrays.stream(Config.getManagedEntities(context)).forEach(p->{
System.out.println(p.getKey());
System.out.println(p.getValue());
});

//使用属性的时候才加载
System.out.println(cargo2);

//查询当前可管理实体
System.out.println("管理的实体个数:"+context.getNumberOfManagedEntities());
Arrays.stream(Config.getManagedEntities(context)).forEach(p->{
System.out.println(p.getKey());
System.out.println(p.getValue());
});

session.getTransaction().commit();
/*
管理的实体个数:0
Hibernate: select cargo0_.id as id1_0_0_, cargo0_.name as name2_0_0_ from Cargo cargo0_ where cargo0_.id=?
管理的实体个数:1
com.hank.entity.Cargo@4f327096
EntityEntry[com.hank.entity.Cargo#1](MANAGED)
Hibernate: select cargo0_.id as id1_0_0_, cargo0_.name as name2_0_0_ from Cargo cargo0_ where cargo0_.id=?
com.hank.entity.Cargo@75e09567
管理的实体个数:2
com.hank.entity.Cargo@4f327096
EntityEntry[com.hank.entity.Cargo#1](MANAGED)
com.hank.entity.Cargo@75e09567
EntityEntry[com.hank.entity.Cargo#2](MANAGED)
*/

update , merge 区别

注意点

  1. update
    更新 dispatch 对象,此对象会变成持久对象
    更新 persistent 对象没有意义
    更新 transient 对象,如果此对象有id,更新后除了设定更新的字段,其他字段会重置成null,无id 不能执行会报错

  2. merge
    更新 transient 对象,如果此对象有id,数据库有记录就进行更新数据库,无记录插入数据
    更新 persistent 对象没有意义

save , persist 区别

persist 不会立即执行sql,所以拿不到数据库返回的 id ,save 会立即执行 sql,可以立即拿到数据库返回的 id

批量更新

批量更新,并且不会再访问的对象。具体的做法是在处理完一个对象或小批量对象后,立刻调用flush()方法清理缓存,然后再调用clear()方法 或 evict 方法清空缓存

普通的批量更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
 session.beginTransaction();
var context=session.unwrap(SessionImplementor.class).getPersistenceContext();

//查询当前可管理实体
System.out.println("管理的实体个数:"+context.getNumberOfManagedEntities());
Arrays.stream(Config.getManagedEntities(context)).forEach(p->{
System.out.println(p.getKey());
System.out.println(p.getValue());
});


//批量更新,如果批量数据比较大,缓存里不清空是比较危险的
for (int i = 1; i < 9; i++) {
var cargo=session.get(Cargo.class,i);
cargo.setName(LocalDateTime.now().toString());
//生成 sql 语句
session.flush();
// 删除缓存里的记录
session.evict(cargo);
}

//查询当前可管理实体
System.out.println("管理的实体个数:"+context.getNumberOfManagedEntities());
Arrays.stream(Config.getManagedEntities(context)).forEach(p->{
System.out.println(p.getKey());
System.out.println(p.getValue());
});

//一次性把所有 sql 语句发给数据库
session.getTransaction().commit();
});

通过 HQL 的批量操作,是在数据库完成 .scroll 方法返回的是游标,只有真正访问对象的时候才会去查询对象

1
2
3
4
5
6
7
8
9
//返回的是游标
var scroll= session.createQuery("from Cargo ").scroll(ScrollMode.FORWARD_ONLY);
while (scroll.next()){
//查询对象
var cargo =(Cargo) scroll.get()[0];
cargo.setName(LocalDateTime.now().toString());
session.flush();
session.clear();
}

spring data jpa

需要的依赖

父模块引入 spring-data-bom 方便子模块的版本管理

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 在父级声明需要的依赖,通过这种关系可以引入多个父类  parent 标签只能引入一个父类   -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-bom</artifactId>
<version>2021.1.10</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>

子模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>

<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.0.1</version>
</dependency>

<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.6.14.Final</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.7.5</version>
</dependency>

实体类

1
2
3
4
5
6
7
8
9
10
11
12
@Entity
@Getter
@Setter
public class Person {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;

@Column(name = "name", nullable = true)
String name;
}

spring data jpa 配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
@Component
public class ConfigerDB {

/**
* 配置数据源
*
* @return
*/
@Bean("dataSource")
public DataSource getDataSource() {
var dataSource = DataSourceBuilder.create();
dataSource.url("jdbc:mysql://localhost:3306/MyDB");
dataSource.driverClassName("com.mysql.cj.jdbc.Driver");
dataSource.username("root");
dataSource.password("root");
//连接池,演示目的不设置参数了
var pool = new HikariDataSource();
pool.setDataSource(dataSource.build());
return pool;
}

/**
* 配置 jpa 属性
* @return
*/
@Bean("jpaProperties")
public JpaProperties getJpaProperties() {
var jpaProperties = new JpaProperties() {
};
jpaProperties.setShowSql(true);
//设置属性
jpaProperties.setProperties(new HashMap<>() {
{
put("hibernate.hbm2ddl.auto", "update");
put("hibernate.dialect", "org.hibernate.dialect.MySQL8Dialect");
put("hibernate.show_sql", "true");
}
});
return jpaProperties;
}

/**
* 提供 jpa 的实现者
* spring boot 默认就包含了 Hibernate 的包
* Hibernate对Jpa的实现
* @return
*/
@Bean("jpaVendorAdapter")
public JpaVendorAdapter getJpaVendorAdapter() {
return new HibernateJpaVendorAdapter();
}

/**
* 实体管理工厂
* @param dataSource
* @param jpaProperties
* @param jpaVendorAdapter
* @return
*/
@Bean("myEntityManagerFactory")
@Primary
public LocalContainerEntityManagerFactoryBean getEntityManagerFactory(
DataSource dataSource, JpaProperties jpaProperties, JpaVendorAdapter jpaVendorAdapter) {

var localContainerEntityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();

localContainerEntityManagerFactoryBean.setDataSource(dataSource);
localContainerEntityManagerFactoryBean.setJpaVendorAdapter(jpaVendorAdapter);
localContainerEntityManagerFactoryBean.setJpaPropertyMap(jpaProperties.getProperties());
localContainerEntityManagerFactoryBean.setPersistenceUnitName("myPersistenceUnitName");

//指定扫描的实体所在的包路径
localContainerEntityManagerFactoryBean.setPackagesToScan("com.hank.entityModel");

return localContainerEntityManagerFactoryBean;
}

/**
* spring 需要的事务
* @param myEntityManagerFactory
* @return
*/
@Bean("jdbcTransactionManager")
public JpaTransactionManager getJdbcTransactionManager(LocalContainerEntityManagerFactoryBean myEntityManagerFactory) {
return new JpaTransactionManager(myEntityManagerFactory.getObject());
}
}

按照 JPA 的方式使用

如下代理在 spring jpa 里可以完全按照 jpa 规范使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@ComponentScan({"com.hank"})
@EnableTransactionManagement
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
{
//这里不能通过字符串获取 bean ,因为这样获取到的是代理对象
var localContainerEntityManagerFactoryBean = context.getBean(LocalContainerEntityManagerFactoryBean.class);


//读取数据并在事务里修改数据
{
var entityManager =localContainerEntityManagerFactoryBean.getObject().createEntityManager();
var transaction=entityManager.getTransaction();
transaction.begin();

System.out.println(entityManager);

var person= entityManager.find(Person.class,1);
System.out.println(person.getName());
person.setName(LocalDateTime.now().toString());
transaction.commit();
entityManager.close();
}

//读取上面事务代码修改的数据
{
var entityManager =localContainerEntityManagerFactoryBean.getObject().createEntityManager();
System.out.println(entityManager);

var person= entityManager.find(Person.class,1);
System.out.println(person.getName());
entityManager.close();
}
}
}
}
/*
SessionImpl(518349613<open>)
2022-12-07T11:14:06.420353
SessionImpl(5998675<open>)
2022-12-07T11:30:04.118294800
*/

测试 spring data 里的事务

注意点:

  1. @PersistenceContext 获取 EntityManager 代理,是线程安全的
  2. @PersistenceUnit 获取 EntityManagerFactory

测试用例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
@Component
class Dao {

/**
* 这里获取的是 EntityManager 代理
* Shared EntityManager proxy for target factory
* [org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean@401c4250]
* 如果有多个持久化单元,需要指定持久化单元的名字 --myEntityManagerFactory
*/
@PersistenceContext(unitName = "myEntityManagerFactory")
private EntityManager entityManager;

/**
* org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean@401c4250
* 如果有多个持久化单元,需要指定持久化单元的名字 --myEntityManagerFactory
*/
@PersistenceUnit(unitName = "myEntityManagerFactory")
private EntityManagerFactory entityManagerFactory;

@Transactional("jdbcTransactionManager")
public void func1() {
System.out.println(entityManager);
//entityManager 是代理对象
//entityManager 代理对象不能像 func2 那样使用事务,需要按照 spring 事务的使用方式使用
var person = entityManager.find(Person.class, 1);
person.setName(LocalDateTime.now().toString());
System.out.println("设置数值:" + person.getName());
entityManager.close();
}

/**
* 验证 func1 事务
* 在本方法里手动开启事务
*/
public void func2() {
System.out.println(entityManagerFactory);
var entityManager = entityManagerFactory.createEntityManager();
System.out.println(entityManager);
//SessionImpl(1337346642<open>)
//此处不是代理对象,不能使用 spring 的事务需要自己开启事务
var transaction=entityManager.getTransaction();
transaction.begin();

var person = entityManager.find(Person.class, 1);
System.out.println("读取数值:" + person.getName());

person.setName(LocalDateTime.now().toString());
System.out.println("设置数值:" + person.getName());

transaction.commit();

entityManager.close();
}

/**
* 验证 func2 手动开启的事务
*/
public void func3() {
System.out.println(entityManagerFactory);
var entityManager = entityManagerFactory.createEntityManager();
System.out.println(entityManager);
//SessionImpl(789367604<open>)

var person = entityManager.find(Person.class, 1);
System.out.println("读取数值:" + person.getName());

entityManager.close();
}
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@ComponentScan({"com.hank"})
@EnableTransactionManagement
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);

context.getBean(Dao.class).func1();

context.getBean(Dao.class).func2();

context.getBean(Dao.class).func3();
}

/*
Shared EntityManager proxy for target factory [org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean@401c4250]
设置数值:2022-12-07T11:14:06.381306600
org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean@401c4250
SessionImpl(1337346642<open>)
读取数值:2022-12-07T11:14:06.381306600
设置数值:2022-12-07T11:14:06.420353
org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean@401c4250
SessionImpl(789367604<open>)
读取数值:2022-12-07T11:14:06.420353
*/
}

spring data jpa 拓展

Repository 及其子类

Repository<T,ID> 位于Spring Data Common的lib里面,是Spring Data 里面做数据库操作的最底层的抽象接口、最顶级的父类,源码里面其实什么方法都没有,仅仅起到一个标识作用。管理域类以及域类的id类型作为类型参数,此接口主要作为标记接口捕获要使用的类型,并帮助你发现扩展此接口的接口。能够在类路径扫描期间发现扩展该类型的接口.Spring底层做动态代理的时候发现只要是它的子类或者实现类,都代表储存库操作

Repository<T,ID> 类型关系图

注意:

  1. @NoRepositoryBean 在接口或对象上加此注解的 jpa 存储库,不会注入 spring ioc
  2. @Repository 如果在加了 @NoRepositoryBean 接口的实现类注入 spring ioc ,要把 @Repository 加载类或接口上
  3. 为什么存储库的方法只用会有事务,因为 SimpleJpaRepository 实现类上加了注解 @Transactional

Repository 大类:

  1. ReactiveCrudRepository,响应式编程,主要支持当前 NoSQL 方面的操作,因为这方面大部分操作都是分布式的,目前 Reactive 主要有 Cassandra、MongoDB、Redis 的实现。
  2. CoroutineCrudRepository :为了支持 Kotlin 语法而实现的。
  3. CrudRepository :JPA 相关的操作接口,也是我们主要用到的接口

更详细一点,我们需要掌握和使用到的7 大 Repository 接口如下所示:

  1. Repository(org.springframework.data.repository),没有暴露任何方法;
  2. CrudRepository(org.springframework.data.repository),简单的 Curd 方法;
  3. PagingAndSortingRepository(org.springframework.data.repository),带分页和排序的方法;
  4. QueryByExampleExecutor(org.springframework.data.repository.query),简单 Example 查询;
  5. JpaRepository(org.springframework.data.jpa.repository),JPA 的扩展方法;
  6. JpaSpecificationExecutor(org.springframework.data.jpa.repository),JpaSpecification 扩展查询;

两大 Repository 实现类:

  1. SimpleJpaRepository(org.springframework.data.jpa.repository.support),JPA 所有接口的默认实现类;

第三方的 QueryDsl 在 JPA 和 spring data 的基础上扩展的第三方查询库:

  1. 接口 QueryDslPredicateExecutor(org.springframework.data.querydsl),QueryDsl 的封装
  2. 实现 QueryDslJpaRepository(org.springframework.data.jpa.repository.support),QueryDsl 的实现类

spring 扩展的方法

@NamedQuery、@Query和方法定义查询的对比:

  1. Spring JPA里面的优先级,@Query > @NameQuery > 方法定义查询。
  2. 推荐使用的优先级:@Query > 方法定义查询 > @NameQuery。
  3. 相同点是都不支持动态条件查询。

方法的查询策略设置
@EnableJpaRepositories 注解来配置方法的查询策略,详细配置方法如下:
@EnableJpaRepositories(queryLookupStrategy= QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND)

其中,QueryLookupStrategy.Key 的值共 3 个,具体如下:

  1. Create:直接根据方法名进行创建,规则是根据方法名称的构造进行尝试,一般的方法是从方法名中删除给定的一组已知前缀,并解析该方法的其余部分。如果方法名不符合规则,
    启动的时候会报异常,这种情况可以理解为,即使配置了 @Query 也是没有用的。
  2. USE_DECLARED_QUERY:声明方式创建,启动的时候会尝试找到一个声明的查询,如果没有找到将抛出一个异常,可以理解为必须配置 @Query 或 @NameQuery
  3. CREATE_IF_NOT_FOUND:这个是默认的,除非有特殊需求,可以理解为这是以上 2 种方式的兼容版。先用声明方式(@Query 或 @NameQuery)进行查找,如果没有找到与方法相
    匹配的查询,那用 Create 的方法名创建规则创建一个查询;这两者都不满足的情况下,启动就会报错。

通过 @EnableJpaRepositories 注解需要提供 Repositories 包路径,entityManagerFactory 的beanName,transactionManager 的 beanName
此注解会扫描 Repository 及其子接口,并为其生成代理类,我们操作的是代理类

Declared Queries -方法定义查询 ,如果返回实体,不能返回实体的部分属性,而是全部属性

方法定义查询的方法要定义在仓储接口里,有代理类用相应的接口的实现类去实现接口内自定义方法

1
2
3
4
5
6
7
8
9
10
//@Indexed 提升 @CompoentScan 扫描的效率
//spring jpa 定义的 Repository 是个空接口
@Indexed
public interface Repository<T, ID> {
}

public interface MyPersonRepository extends Repository<Person, Integer> {
//方法定义查询,要以固定的前缀开头加属性表达式结尾
public Person findDistinctByName(String name);
}

这种方法个人感觉不好用,属于死记硬背的做法,而且写错也没办法在编译的时候,检查出错误

属性表达式(Property Expressions)(findByAddressZipCode) 只能引用托管(泛化)实体的直接属性,如前一个示例所示。在查询创建时,你已经确保解析的属性是托管实体的属性。同时,还可以通过遍历嵌套属性定义约束。假设一个Person实体对象里面有一个Address属性里面包含一个ZipCode属性。在这种情况下,方法名为:

1
2
3
List<Person> findByAddressZipCode(string zipCode)
//或
List<Person> findByAddress_ZipCode(string zipCode)

声明的查询支持的前缀

关键词 说明
find, get, query, stream 查询所有的字段
count 统计数量
exists 是否存在
delete , remove 移除
save 保存或更新

* 声明的查询支持的关键字, 在类 org.springframework.data.repository.query.parser.PartTree 定义*

关键词 举例
Distinct findDistinctByLastnameAndFirstname
And findByLastnameAndFirstname
Or findByLastnameOrFirstname
Is,Equals findByFirstname, findByFirstnameIs,findByFirstnameEquals
Between findByStartDateBetween
LessThan findByAgeLessThan
LessThanEqual findByAgeLessThanEqual
GreaterThan findByAgeGreaterThan
GreaterThanEqual findByAgeGreaterThanEqual
After findByStartDateAfter
Before findByStartDateBefore
IsNull,Null findByAge(Is)Null
IsNotNull,NotNull findByAge(Is)NotNull
Like findByFirstnameLike
NotLike findByFirstnameNotLike
StartingWith findByFirstnameStartingWith
EndingWith findByFirstnameEndingWith
Containing findByFirstnameContaining
OrderBy findByAgeOrderByLastnameDesc
Not findByLastnameNot
In findByAgeIn(Collection ages)
NotIn findByAgeNotIn(Collection ages)
True findByActiveTrue()
False findByActiveFalse()
IgnoreCase findByFirstnameIgnoreCase

综上,总结 3 点经验:

  1. 方法名的表达式通常是实体属性连接运算符的组合,如 And、or、Between、LessThan、GreaterThan、Like 等属性连接运算表达式,不同的数据库(NoSQL、MySQL)可能产生的效果不一样,如果遇到问题,我们可以打开 SQL 日志观察。
  2. IgnoreCase 可以针对单个属性(如 findByLastnameIgnoreCase(…)),也可以针对查询条件里面所有的实体属性忽略大小写(所有属性必须在 String 情况下,如 findByLastnameAndFirstnameAllIgnoreCase(…))。
  3. OrderBy 可以在某些属性的排序上提供方向(Asc 或 Desc),称为静态排序,也可以通过一个方便的参数 Sort 实现指定字段的动态排序的查询方法(如 repository.findAll(Sort.by(Sort.Direction.ASC, “myField”)))。

通过 @EnableJpaRepositories 注解需要提供 Repositories 包路径,entityManagerFactory 的beanName,transactionManager 的 beanName
此注解会扫描 Repository 及其子接口,并为其生成代理类,我们操作的是代理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@ComponentScan({"com.hank"})
@EnableTransactionManagement
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)
public class Programmer {
public static void main(String[] args) throws Exception {

var context = new AnnotationConfigApplicationContext(Programmer.class);

var repository = context.getBean(MyPersonRepository.class);

// true
System.out.println(Proxy.isProxyClass(repository.getClass()));
//class com.sun.proxy.$Proxy62
System.out.println(repository.getClass());
//org.springframework.data.jpa.repository.support.SimpleJpaRepository@2fac80a8
//代理对象代理的实现类是 SimpleJpaRepository
System.out.println(repository);

//方法 findByName 的实现在 SimpleJpaRepository 类里
var person =repository.findDistinctByName("ok");
}
}

@Query -JPQL注解查询

@Query 属于JPQL 查询,同时也可以指定原生的 sql 查询方式(nativeQuery = true),除过查询其他方法要加 @Modifying 注解
@Modifying(clearAutomatically = true) 作用是在做更新后立即清除当前缓存,并做一次数据库查询
参数的指定方式有:

  1. ?1 传入的参数是方法的第一个参数 ,序号参数
  2. :name 传入的参数是方法的参数上加 @Param(“name”) 注解的,命名参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public interface MyPersonRepository extends Repository<Person, Integer> {

//查询部分字段,相比较声明查询的优点
@Query(" select u.name from Person u where u.id=1 and u.name=?1")
public String findByName1(String name);

@Query("from Person u where u.id=1 and u.name=:name")
public Person findByName2(@Param("name") String name);

//原生查询
@Query(value = "select * from Person u where u.id=1 and u.name=:name", nativeQuery = true)
public Person findByName3(@Param("name") String name);

//还可以传入对象 :#{#person.name} 属于 SpEL 表达式
@Query(value = "from Person u where u.id=1 and u.name=:#{#person.name}")
public Person findByName4(@Param("person") Person person);

//更新
@Modifying
@Transactional
@Query(value = "update Person u set u.name=?1 where u.id=?2 ")
public int updatePerson(String name,int id);

//删除
@Modifying
@Transactional
@Query(value = "delete from Person u where u.id=?1")
public int deletePerson(int id);

//新增
@Modifying
@Transactional
@Query(value = "insert into Person (name) values(?1) ",nativeQuery = true)
public int insetPerson(String name);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@ComponentScan({"com.hank"})
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)
@EnableTransactionManagement
public class Programmer {
public static void main(String[] args) throws Exception {

var context = new AnnotationConfigApplicationContext(Programmer.class);

var repository = context.getBean(MyPersonRepository.class);

// true
System.out.println(Proxy.isProxyClass(repository.getClass()));
//class com.sun.proxy.$Proxy62
System.out.println(repository.getClass());
//org.springframework.data.jpa.repository.support.SimpleJpaRepository@2fac80a8
//代理对象代理的实现类是 SimpleJpaRepository
System.out.println(repository);

System.out.println(repository.findByName1("ok"));
System.out.println(repository.findByName2("ok"));
System.out.println(repository.findByName3("ok"));
System.out.println(repository.findByName4(new Person(){{setName("ok");}}));

System.out.println(repository.updatePerson("ok",1));
System.out.println(repository.deletePerson(1));
System.out.println(repository.insetPerson("bad"));
}
/*
true
class com.sun.proxy.$Proxy66
org.springframework.data.jpa.repository.support.SimpleJpaRepository@5112b7
ok
com.hank.entityModel.Person@333d44f6
com.hank.entityModel.Person@32b112a1
com.hank.entityModel.Person@678586f0
1
1
1
*/
}
SpEL 表达式

在Spring Data JPA 1.4以后,支持在@Query中使用SpEL表达式(简介)来接收变量,这为使用 @Query 提供了更大的便利性

1
2
3
4
 //还可以传入对象 :#{#person.name} 属于 SpEL 表达式
//#{#entity} 表示的是 @Entity 指定的名字会 实体的类名
@Query(value = "from #{#entity} u where u.id=1 and u.name=:#{#person.name}")
public Person findByName4(@Param("person") Person person);

@NamedQuery-命名查询

@NamedQuery注解定义的查询里写的也是 jpql,之相对应的还有@NamedNativeQuery 书写的是原生 sql
这种把查询方法定义在实体的方式用起来不优雅,一般不使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Entity
@Getter
@Setter
//name = "Person.myNamedQuery" 用实体名称开头
@NamedQuery(name = "Person.myNamedQuery",query = "select c from Person c where c.id=?1")
public class Person {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;

@Column(name = "name", nullable = true)
String name;
}

@NamedStoredProcedureQueries , @Procedure 命名查询的存储过程

@NamedStoredProcedureQueries 必须加载一个实体上
@Procedure 加载方法上

QueryByExampleExecutor 简单化的动态查询

QueryByExampleExecutor 接口实现动态查询创建,并且不需要编写包含字段名称的查询,所谓动态查询是查询的字段不一定有几个,也可以传入排序和分页

提供一个实例作为动态查询条件
Query by Example 也有几个限制:

  1. 不支持嵌套或分组的属性约束,例如firstname = ?0 or (firstname = ?1 and lastname = ?2)
  2. 仅支持字符串的 starts/contains/ends/regex 匹配和其他属性类型的精确匹配
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public interface QueryByExampleExecutor<T> {
/**
* Returns a single entity matching the given {@link Example} or {@link Optional#empty()} if none was found.
*
* @param example must not be {@literal null}.
* @return a single entity matching the given {@link Example} or {@link Optional#empty()} if none was found.
* @throws org.springframework.dao.IncorrectResultSizeDataAccessException if the Example yields more than one result.
*/
<S extends T> Optional<S> findOne(Example<S> example);

/**
* Returns all entities matching the given {@link Example}. In case no match could be found an empty {@link Iterable}
* is returned.
*
* @param example must not be {@literal null}.
* @return all entities matching the given {@link Example}.
*/
<S extends T> Iterable<S> findAll(Example<S> example);

/**
* Returns all entities matching the given {@link Example} applying the given {@link Sort}. In case no match could be
* found an empty {@link Iterable} is returned.
*
* @param example must not be {@literal null}.
* @param sort the {@link Sort} specification to sort the results by, may be {@link Sort#unsorted()}, must not be
* {@literal null}.
* @return all entities matching the given {@link Example}.
* @since 1.10
*/
<S extends T> Iterable<S> findAll(Example<S> example, Sort sort);

/**
* Returns a {@link Page} of entities matching the given {@link Example}. In case no match could be found, an empty
* {@link Page} is returned.
*
* @param example must not be {@literal null}.
* @param pageable the pageable to request a paged result, can be {@link Pageable#unpaged()}, must not be
* {@literal null}.
* @return a {@link Page} of entities matching the given {@link Example}.
*/
<S extends T> Page<S> findAll(Example<S> example, Pageable pageable);

/**
* Returns the number of instances matching the given {@link Example}.
*
* @param example the {@link Example} to count instances for. Must not be {@literal null}.
* @return the number of instances matching the {@link Example}.
*/
<S extends T> long count(Example<S> example);

/**
* Checks whether the data store contains elements that match the given {@link Example}.
*
* @param example the {@link Example} to use for the existence check. Must not be {@literal null}.
* @return {@literal true} if the data store contains elements that match the given {@link Example}.
*/
<S extends T> boolean exists(Example<S> example);

/**
* Returns entities matching the given {@link Example} applying the {@link Function queryFunction} that defines the
* query and its result type.
*
* @param example must not be {@literal null}.
* @param queryFunction the query function defining projection, sorting, and the result type
* @return all entities matching the given {@link Example}.
* @since 2.6
*/
<S extends T, R> R findBy(Example<S> example, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction);
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public interface MyPersonRepository extends Repository<Person, Integer>, QueryByExampleExecutor<Person> {
@Override
List<Person> findAll(Example example);
}


@ComponentScan({"com.hank"})
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)
@EnableTransactionManagement
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
var repository = context.getBean(MyPersonRepository.class);

//动态匹配的实体对象
var person=new Person();
person.setFirstName("ok3");

// 因为 id 有默认值,需要排除
var matcher= ExampleMatcher.matching().withIgnorePaths(Person_.ID);

//字符串 Starting 匹配,也能对所有的字符串属性指定字符串匹配
matcher= matcher.withMatcher(Person_.FIRST_NAME, ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.STARTING,true));
// 方法2 matcher= matcher.withMatcher(Person_.FIRST_NAME, p->p.endsWith().ignoreCase());

//用动态查询去匹配
var list=repository.findAll(Example.of(person,matcher));
list.forEach(p->{
System.out.println(p);
});
}
}

流畅的动态化 findBy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@Entity
@Getter
@Setter
@ToString
public class Person {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;

@Column(name = "firstName", nullable = true)
String firstName;

@Column(name = "lastName", nullable = true)
String lastName;

@Column(name = "personAge", nullable = true)
int age;
}


@ComponentScan({"com.hank"})
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)
@EnableTransactionManagement

public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
var repository = context.getBean(MyPersonRepository.class);


var person=new Person();
person.setFirstName("k1");
person.setLastName("dA1");

//数字类型的字段忽略掉,最好是用包装类型就不用在这里忽略了,因为 null 类型, Example B不会进行匹配
var exampleMatcher= ExampleMatcher.matching().
withIgnorePaths(Person_.ID).withIgnorePaths(Person_.AGE);

var sort= Sort.by(Person_.FIRST_NAME).ascending();

//可以在第二个参数里选取第一个数据
var result= repository.findBy(Example.of(person,exampleMatcher),p->p.sortBy(sort).first());

System.out.println(result.isPresent()?result.get():null);
}
}

JpaSpecificationExecutor-可组合的规约查询

规约的最大好处就是可组合可复用

hibernate 的使用方式

1
2
3
4
5
6
7
8
CriteriaBuilder cb = session.getCriteriaBuilder();
CriteriaQuery<Person> criteriaQuery = cb.createQuery(Person.class);

Root<Person> root = criteriaQuery.from(Person.class);
criteriaQuery.select(root).where(cb.equal(root.get("firstName"), "ok1"));

Query<Person> query = session.createQuery(criteriaQuery);
List<Person> results = query.getResultList();

Spring Data JPA 采用 Eric Evans 的书“领域驱动设计”中的规范概念,遵循相同的语义并提供 API 以使用 JPA 标准 API 定义此类规范。为了支持规范,您可以使用接口扩展您的存储库接口JpaSpecificationExecutor,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public interface JpaSpecificationExecutor<T> {

/**
* Returns a single entity matching the given {@link Specification} or {@link Optional#empty()} if none found.
*
* @param spec can be {@literal null}.
* @return never {@literal null}.
* @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one entity found.
*/
Optional<T> findOne(@Nullable Specification<T> spec);

/**
* Returns all entities matching the given {@link Specification}.
*
* @param spec can be {@literal null}.
* @return never {@literal null}.
*/
List<T> findAll(@Nullable Specification<T> spec);

/**
* Returns a {@link Page} of entities matching the given {@link Specification}.
*
* @param spec can be {@literal null}.
* @param pageable must not be {@literal null}.
* @return never {@literal null}.
*/
Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable);

/**
* Returns all entities matching the given {@link Specification} and {@link Sort}.
*
* @param spec can be {@literal null}.
* @param sort must not be {@literal null}.
* @return never {@literal null}.
*/
List<T> findAll(@Nullable Specification<T> spec, Sort sort);

/**
* Returns the number of instances that the given {@link Specification} will return.
*
* @param spec the {@link Specification} to count instances for. Can be {@literal null}.
* @return the number of instances.
*/
long count(@Nullable Specification<T> spec);
}

规约的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public interface Specification<T> extends Serializable {

long serialVersionUID = 1L;

/**
* Negates the given {@link Specification}.
*
* @param <T> the type of the {@link Root} the resulting {@literal Specification} operates on.
* @param spec can be {@literal null}.
* @return guaranteed to be not {@literal null}.
* @since 2.0
*/
static <T> Specification<T> not(@Nullable Specification<T> spec) {

return spec == null //
? (root, query, builder) -> null //
: (root, query, builder) -> builder.not(spec.toPredicate(root, query, builder));
}

/**
* Simple static factory method to add some syntactic sugar around a {@link Specification}.
*
* @param <T> the type of the {@link Root} the resulting {@literal Specification} operates on.
* @param spec can be {@literal null}.
* @return guaranteed to be not {@literal null}.
* @since 2.0
*/
static <T> Specification<T> where(@Nullable Specification<T> spec) {
return spec == null ? (root, query, builder) -> null : spec;
}

/**
* ANDs the given {@link Specification} to the current one.
*
* @param other can be {@literal null}.
* @return The conjunction of the specifications
* @since 2.0
*/
default Specification<T> and(@Nullable Specification<T> other) {
return SpecificationComposition.composed(this, other, CriteriaBuilder::and);
}

/**
* ORs the given specification to the current one.
*
* @param other can be {@literal null}.
* @return The disjunction of the specifications
* @since 2.0
*/
default Specification<T> or(@Nullable Specification<T> other) {
return SpecificationComposition.composed(this, other, CriteriaBuilder::or);
}

/**
* Creates a WHERE clause for a query of the referenced entity in form of a {@link Predicate} for the given
* {@link Root} and {@link CriteriaQuery}.
*
* @param root must not be {@literal null}.
* @param query must not be {@literal null}.
* @param criteriaBuilder must not be {@literal null}.
* @return a {@link Predicate}, may be {@literal null}.
*/
@Nullable
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder);
}

测试代码

为了在规约中使用强类型的属性名编程, JPA 提供了 metemodel 的概念在编译时生成代码,为了使用此代码需要把生成的代码转变为源代码

1
2
3
4
5
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>5.6.3.Final</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

//使用规约,继承 JpaSpecificationExecutor 接口
public interface MyPersonRepository extends Repository<Person, Integer>, JpaSpecificationExecutor<Person> {
@Override
List<Person> findAll(Specification<Person> spec);
}

@ComponentScan({"com.hank"})
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)
@EnableTransactionManagement
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
var repository = context.getBean(MyPersonRepository.class);

//生成规约表达式,因为是函数式接口,此处也可以用 lambda 进行简写
var spec= new Specification<Person>() {
/**
*
* @param Root<Person> root 查询的实体
* @param CriteriaQuery<?> query 代表一个specific的顶层查询对象,它包含着查询的各个部分,比如:select、from、where、group by、order by等。它提供了查询ROOT的方法
* @param criteriaBuilder 用来构建CritiaQuery的构建器对象,其实就相当于条件或者是条件组合,即以Predicate的形式返回,可以构建简单的查询
复杂的查询还有要通过 CriteriaQuery<?> 生成
* @return
*/
@Override
public Predicate toPredicate(Root<Person> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {

//获取的实体属性值,此处用了 metemodel 强类型
var path1= root.get(Person_.firstName);
var path2= root.get(Person_.lastName);

//谓词判断
var predicate1 =criteriaBuilder.equal(path1,"ok1");
var predicate2 =criteriaBuilder.equal(path2,"ok2");
var and= criteriaBuilder.and(predicate1,predicate2);

//排序条件
var order1=criteriaBuilder.asc(path1);
var order2=criteriaBuilder.asc(path2);

//通过 query 组合
return query.where(and).orderBy(order1,order2).getRestriction();

//不带排序
//return criteriaBuilder.and(predicate1,predicate2);
}
};

var result = repository.findAll(spec);
System.out.println(result.size());
}
}

QueryDSL - 解决多表查询,动态查询的通用类型安全的查询利器,代替JPQL

QueryDSL 有多个模块,可以支持 JPA、Mongodb、Lucene 等,使用它们的 API 是统一的平台,这里我们用到的 module 是 JPA。

不可否认的是 JPA 使用是非常方便的,极简化的配置,只需要使用注解,无需任何 xml 的配置文件,语义简单易懂,但是,以上的一切都建立在单表查询的前提下的,我们可以使用 JPA 默认提供的方法,简单加轻松的完成 CRUD 操作。
但是如果涉及到多表动态查询, JPA 的功能就显得有些捉襟见肘了,虽然我们可以使用注解 @Query ,在这个注解中写 SQL 或者 HQL 都是在拼接字符串,并且拼接后的字符串可读性非常的差,当然 JPA 还为我们提供了 Specification 来做这件事情

QuerydslPredicateExecutor 接口提供的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
public interface QuerydslPredicateExecutor<T> {

/**
* Returns a single entity matching the given {@link Predicate} or {@link Optional#empty()} if none was found.
*
* @param predicate must not be {@literal null}.
* @return a single entity matching the given {@link Predicate} or {@link Optional#empty()} if none was found.
* @throws org.springframework.dao.IncorrectResultSizeDataAccessException if the predicate yields more than one
* result.
*/
Optional<T> findOne(Predicate predicate);

/**
* Returns all entities matching the given {@link Predicate}. In case no match could be found an empty
* {@link Iterable} is returned.
*
* @param predicate must not be {@literal null}.
* @return all entities matching the given {@link Predicate}.
*/
Iterable<T> findAll(Predicate predicate);

/**
* Returns all entities matching the given {@link Predicate} applying the given {@link Sort}. In case no match could
* be found an empty {@link Iterable} is returned.
*
* @param predicate must not be {@literal null}.
* @param sort the {@link Sort} specification to sort the results by, may be {@link Sort#unsorted()}, must not be
* {@literal null}.
* @return all entities matching the given {@link Predicate}.
* @since 1.10
*/
Iterable<T> findAll(Predicate predicate, Sort sort);

/**
* Returns all entities matching the given {@link Predicate} applying the given {@link OrderSpecifier}s. In case no
* match could be found an empty {@link Iterable} is returned.
*
* @param predicate must not be {@literal null}.
* @param orders the {@link OrderSpecifier}s to sort the results by, must not be {@literal null}.
* @return all entities matching the given {@link Predicate} applying the given {@link OrderSpecifier}s.
*/
Iterable<T> findAll(Predicate predicate, OrderSpecifier<?>... orders);

/**
* Returns all entities ordered by the given {@link OrderSpecifier}s.
*
* @param orders the {@link OrderSpecifier}s to sort the results by, must not be {@literal null}.
* @return all entities ordered by the given {@link OrderSpecifier}s.
*/
Iterable<T> findAll(OrderSpecifier<?>... orders);

/**
* Returns a {@link Page} of entities matching the given {@link Predicate}. In case no match could be found, an empty
* {@link Page} is returned.
*
* @param predicate must not be {@literal null}.
* @param pageable may be {@link Pageable#unpaged()}, must not be {@literal null}.
* @return a {@link Page} of entities matching the given {@link Predicate}.
*/
Page<T> findAll(Predicate predicate, Pageable pageable);

/**
* Returns the number of instances matching the given {@link Predicate}.
*
* @param predicate the {@link Predicate} to count instances for, must not be {@literal null}.
* @return the number of instances matching the {@link Predicate}.
*/
long count(Predicate predicate);

/**
* Checks whether the data store contains elements that match the given {@link Predicate}.
*
* @param predicate the {@link Predicate} to use for the existence check, must not be {@literal null}.
* @return {@literal true} if the data store contains elements that match the given {@link Predicate}.
*/
boolean exists(Predicate predicate);

/**
* Returns entities matching the given {@link Predicate} applying the {@link Function queryFunction} that defines the
* query and its result type.
*
* @param predicate must not be {@literal null}.
* @param queryFunction the query function defining projection, sorting, and the result type
* @return all entities matching the given {@link Predicate}.
* @since 2.6
*/
<S extends T, R> R findBy(Predicate predicate, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction);
}

准备工作

1
2
3
4
5
6
<!-- querydsl-jpa -->
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>5.0.0</version>
</dependency>

此插件需要 querydsl-apt 依赖,此插件会在编译时生成强类型的实体类型,特征是生成在在实体前加Q的强类型元素据,类似于 JPA 的 metamodel,需要把生成的代码加到源码中

如下代码就是强类型的类型变量

1
2
3
QPerson person=QPerson.person;
//或
QPerson person= new QPerson("myPerson");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 <plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<dependencies>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>5.0.0</version>
</dependency>
</dependencies>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources/java</outputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>
仓储继承 QuerydslPredicateExecutor 接口

QuerydslPredicateExecutor 接口是谓词查询,缺点是只能是单表查询,返回的实体的所有字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public interface MyPersonRepository extends Repository<Person, Integer>, QuerydslPredicateExecutor<Person> {
@Override
List<Person> findAll(Predicate predicate);
}


@ComponentScan({"com.hank"})
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)
@EnableTransactionManagement
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
var repository = context.getBean(MyPersonRepository.class);

var person=QPerson.person;

var booleanExpression1=person.firstName.in("k1", "k2");
var booleanExpression2=person.lastName.containsIgnoreCase("a1");
var booleanExpression3=person.id.goe(4);

var list= repository.findAll(booleanExpression1.and(booleanExpression2).and(booleanExpression3));
System.out.println(list.get(0).toString());
}
}
仓储注入 JPAQueryFactory 对象链式查询,多表,多字段查询

JPAQueryFactory 用来生成 JPAQuery,当然也可以自己 new JPAQuery

1
2
3
4
5
var myPerson = QPerson.person;
//可以自己构建 JPAQuery
JPAQuery<Person> query = new JPAQuery<Person>(entityManager);
//或
JPAQuery<Person> query = jPAQueryFactory.selectFrom(myPerson)

配置 JPAQueryFactory 工厂在仓储里使用

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class QueryDSLConfig {

@PersistenceContext
private EntityManager entityManager;

@Bean
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public JPAQueryFactory getJPAQueryFactory(){
return new JPAQueryFactory(entityManager);
}
}

链式写法的 jpaQuery 用java 语法写SQL 非常的方便

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public  interface MyPersonRepository extends Repository<Person, Integer> {
default List<Person> findDSL(JPAQueryFactory jPAQueryFactory){
var person=QPerson.person;
var jpaQuery=jPAQueryFactory.select(person)
.from(person)
.where(person.firstName.containsIgnoreCase("ok"));

var list= jpaQuery.fetch();
return list;
}
}

@ComponentScan({"com.hank"})
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)
@EnableTransactionManagement
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
var repository = context.getBean(MyPersonRepository.class);
var jPQLQueryFactory= context.getBean(JPAQueryFactory.class);
//调用存储方法
System.out.println(repository.findDSL(jPQLQueryFactory));
}
}
更新删除
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface MyPersonRepository extends Repository<Person, Integer> {

@Transactional
default long updateDSL(JPAQueryFactory jPAQueryFactory) {
var person = QPerson.person;
var updateClause = jPAQueryFactory.update(person)
.set(person.firstName, "name good")
.where(person.id.eq(1));
return updateClause.execute();
}

@Transactional
default long deleteDSL(JPAQueryFactory jPAQueryFactory) {
var person = QPerson.person;
var deleteClause = jPAQueryFactory.delete(person)
.where(person.firstName.eq("K2"));
return deleteClause.execute();
}
}
查询部分字段

返回一个字段

1
2
3
4
5
6
7
8
9
10
public interface MyPersonRepository extends Repository<Person, Integer> {
default String queryDSL(JPAQueryFactory jPAQueryFactory) {
var person = QPerson.person;
//选择映射的字段
var jpaQuery = jPAQueryFactory.select(person.lastName)
.from(person)
.where(person.id.eq(1));
return jpaQuery.fetchOne();
}
}

返回多个字段组合的是元组

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface MyPersonRepository extends Repository<Person, Integer> {
default Tuple queryDSL(JPAQueryFactory jPAQueryFactory) {
var person = QPerson.person;
var jpaQuery = jPAQueryFactory.select(person.lastName,person.lastName)
.from(person)
.where(person.id.eq(1));
var tuple=jpaQuery.fetchOne();

System.out.println(tuple.get(person.lastName));
System.out.println(tuple.get(person.lastName));
return tuple;
}
}

返回多个字段组合的的元组不方便使用,还可以进行投影

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//投影的类
@Value
public class MyReturnDto {
String firstNameDto;
String lastNameDto;
public String getFullName(){
return firstNameDto+","+lastNameDto;
}
}

public interface MyPersonRepository extends Repository<Person, Integer> {
default MyReturnDto queryDSL(JPAQueryFactory jPAQueryFactory) {
var person = QPerson.person;
var jpaQuery = jPAQueryFactory.select(
//对返回结果进行投影
Projections.constructor(MyReturnDto.class,
person.firstName.as("firstNameDto"),person.lastName.as("lastNameDto")))
.from(person)
.where(person.id.eq(1));
var myReturnDto=jpaQuery.fetchOne();
System.out.println(myReturnDto.getFullName());
return myReturnDto;
}
}
关联和子查询

关联查询,支持 内联结、联结、左联结和右联结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//增加的实体
@Entity
@Data
public class OrderCargo {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;

private int PersonId;

private String CargoName;
}

public interface MyPersonRepository extends Repository<Person, Integer> {
default List<Tuple> queryDSL(JPAQueryFactory jPAQueryFactory) {
var person = QPerson.person;
var orderCargo = QOrderCargo.orderCargo;

var jpaQuery= jPAQueryFactory.select(person,orderCargo).
from(person).join(orderCargo).on(person.id.eq(orderCargo.PersonId));
var result =jpaQuery.fetch();

//显示结果
result.forEach(p->{
var i= p.get(person);
var j=p.get(orderCargo);
System.out.println(i);
System.out.println(j);
});

return result ;
}
}

子查询,提过静态方法 JPAExpressions 提供子查询

1
2
3
4
5
6
7
8
9
10
//增加实体
@Entity
@Data
public class CargoType {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String CargoName;
}

通过子查询实现 orderCargo 的 CargoName 出现在 cargoType 的 CargoName 里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface MyPersonRepository extends Repository<Person, Integer> {
default List<OrderCargo> queryDSL(JPAQueryFactory jPAQueryFactory) {

var orderCargo = QOrderCargo.orderCargo;
var type= QCargoType.cargoType;

var jpaQuery= jPAQueryFactory.selectFrom(orderCargo)
.where(orderCargo.CargoName.in(JPAExpressions.select(type.CargoName).from(type)));

var result= jpaQuery.fetch();

result.forEach(p->{
System.out.println(p);
});

return result ;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
@Entity
@Getter
@Setter
@ToString
public class Person {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;

@Column(name = "firstName", nullable = true)
String firstName;

@Column(name = "lastName", nullable = true)
String lastName;

@Column(name = "personAge", nullable = true)
int age;
}

//获取 person 里年龄大于平均年龄的集合
public interface MyPersonRepository extends Repository<Person, Integer> {
default List<Person> queryDSL(JPAQueryFactory jPAQueryFactory) {

var person = QPerson.person;
var jpaQuery= jPAQueryFactory.selectFrom(person)
.where(person.age.goe(JPAExpressions.select(person.age.avg()).from(person)));

var result=jpaQuery.fetch();

result.forEach(p->{
System.out.println(p);
});
return result ;
}
}

// 或者
public interface MyPersonRepository extends Repository<Person, Integer> {
default List<Person> queryDSL(JPAQueryFactory jPAQueryFactory) {

var person = QPerson.person;
var q=new QPerson("q");
var jpaQuery= jPAQueryFactory.selectFrom(person)
.where(person.age.goe(JPAExpressions.select(q.age.avg()).from(q)));

var result=jpaQuery.fetch();

result.forEach(p->{
System.out.println(p);
});
return result ;
}
}
分组和分页及排序

按照某个属性进行分组,获取也只能获取分组的属性,可以多次分组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//单个属性的分组
public interface MyPersonRepository extends Repository<Person, Integer> {
default List<Integer> queryDSL(JPAQueryFactory jPAQueryFactory) {

var person = QPerson.person;
var list = jPAQueryFactory.select(person.age).from(person).groupBy(person.age).fetch();
list.forEach(p->{
System.out.println(p);
});
return list ;
}
}

//可以对多个元素的组合进行分组 groupBy(person.firstName,person.lastName)
public interface MyPersonRepository extends Repository<Person, Integer> {
default List<Object> queryDSL(JPAQueryFactory jPAQueryFactory) {

var person = QPerson.person;
var list = jPAQueryFactory.select(person.firstName,person.lastName).from(person).groupBy(person.firstName,person.lastName).fetch();
list.forEach(p->{
System.out.println(p);
});
return Collections.singletonList(list);
}
}

//分组过滤 having
public interface MyPersonRepository extends Repository<Person, Integer> {
default List<Integer> queryDSL(JPAQueryFactory jPAQueryFactory) {

var person = QPerson.person;

var list=jPAQueryFactory.select(person.age).from(person)
//对分组的元素进行过滤 having(person.age.gt(20))
.groupBy(person.age).having(person.age.gt(20)).fetch();

list.forEach(p->{
System.out.println(p);
});

return list;
}
}

分页 通过 offset 设置页码,limit 设置每页记录数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface MyPersonRepository extends Repository<Person, Integer> {
default QueryResults<Person> queryDSL(JPAQueryFactory jPAQueryFactory) {

var person = QPerson.person;
var pageable= PageRequest.of(0,2);
var queryResults=jPAQueryFactory.selectFrom(person)
.offset(pageable.getOffset()).limit(pageable.getPageSize()).fetchResults();

System.out.println(queryResults.getTotal());
queryResults.getResults().forEach(p->{
System.out.println(p);
});

return queryResults;
}
}

排序 orderBy

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface MyPersonRepository extends Repository<Person, Integer> {
default List<Person> queryDSL(JPAQueryFactory jPAQueryFactory) {

var person = QPerson.person;

var list=jPAQueryFactory.selectFrom(person)
//多个排序条件 asc 升序 ,
//或 orderBy(person.age.asc(),person.id.asc())
.orderBy(person.age.asc()).orderBy(person.id.asc()).fetch();

return list;
}
}
聚合函数,本质还是调用数据库的聚合函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public interface MyPersonRepository extends Repository<Person, Integer> {
default List<String> queryDSL(JPAQueryFactory jPAQueryFactory) {

var person = QPerson.person;

var max=jPAQueryFactory.select(person.age.max()).from(person).fetchOne();
System.out.println(max);

var min=jPAQueryFactory.select(person.age.min()).from(person).fetchOne();
System.out.println(min);

var avg=jPAQueryFactory.select(person.age.avg()).from(person).fetchOne();
System.out.println(avg);

var sum=jPAQueryFactory.select(person.age.sum()).from(person).fetchOne();
System.out.println(sum);

//数字的减法,需要的有加法等
var list=jPAQueryFactory.select(person.age.subtract(1)).from(person).fetch();
list.forEach(p->{
System.out.println(p);
});

//字符可以插入前缀或后缀
var list1=jPAQueryFactory.select(person.firstName.prepend("hi,")).from(person).fetch();
list1.forEach(p->{
System.out.println(p);
});

return list1;
}
}

spring 扩展的 参数分页和排序-Pageable/Sort

Pageable 分页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface MyPersonRepository extends Repository<Person, Integer> {
Page<Person> findPersonByName(String name, Pageable pageable);
}

@ComponentScan({"com.hank"})
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)
@EnableTransactionManagement
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
var repository = context.getBean(MyPersonRepository.class);

Pageable pageable= PageRequest.of(0,2 );
var pages= repository.findPersonByName("ok", pageable);
System.out.println("可查询的总总页数"+pages.getTotalPages());
System.out.println("可查询的总条目数"+pages.getTotalElements());
System.out.println("本次查询到的集合"+pages.get().collect(Collectors.toList()));
}
}

Sort 排序

推荐 Sort.TypedSort<> 排序,而不是硬编码的排序

public interface MyPersonRepository extends Repository<Person, Integer> {
List findPersonByName(String name, Sort sort);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@ComponentScan({"com.hank"})
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)

@ComponentScan({"com.hank"})
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)
@EnableTransactionManagement
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
var repository = context.getBean(MyPersonRepository.class);

//硬编码的排序
Sort sort=Sort.by("name").ascending();

//强类型引用的排序
Sort.TypedSort<Person> sortPerson=Sort.sort(Person.class);
sortPerson.by(Person::getName).ascending();
var list= repository.findPersonByName("ok", sortPerson);
System.out.println(list);
}
}

分页和排序一起使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public interface MyPersonRepository extends Repository<Person, Integer> {
Page<Person> findPersonByName(String name, Pageable pageable);
}


@ComponentScan({"com.hank"})
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)
@EnableTransactionManagement
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
var repository = context.getBean(MyPersonRepository.class);


Sort.TypedSort<Person> sortPerson=Sort.sort(Person.class);
sortPerson.by(Person::getName).ascending();

//分页和排序
Pageable pageable= PageRequest.of(0,2 ,sortPerson);

var pages= repository.findPersonByName("ok", pageable);
System.out.println("可查询的总总页数"+pages.getTotalPages());
System.out.println("可查询的总条目数"+pages.getTotalElements());
System.out.println("本次查询到的集合"+pages.get().collect(Collectors.toList()));
}
}

查询返回结果

常见的返回结果:

  1. List
  2. Slice
  3. Page
  4. Stream
  5. CompletableFuture
  6. Optional

Stream 流的传输需要数据库的配合,数据库要支持数据的流式传输才行,需要在消费流的方法上加 @Transactional(readOnly = true),流在用完后记得要关闭,这种方式的好处是对数据量大处理比较好

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
class Bar{
private final MyPersonRepository myPersonRepository;

public Bar(MyPersonRepository myPersonRepository){
this.myPersonRepository=myPersonRepository;
}

@Transactional(readOnly = true)
public void f(){

//自动关闭流
try(var stream= myPersonRepository.findPersonByName("ok")) {
stream.forEach(p-> System.out.println(p.getId()));
}
}
}

CompletableFuture 可以使用Spring的异步方法执行功能异步的存储库查询。这意味着方法将在调用时立即返回,并且实际的查询执行将发生在已提交给Spring TaskExecutor的任务中,比较适合定时任务的实际场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public interface MyPersonRepository extends Repository<Person, Integer> {
CompletableFuture<List<Person>> findPersonByName(String name);
}

@ComponentScan({"com.hank"})
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)
@EnableTransactionManagement
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
var repository=context.getBean(MyPersonRepository.class);
var future=repository.findPersonByName("ok");

future.thenAcceptAsync(p->{
System.out.println(p.stream().collect(Collectors.toList()));
});

System.out.println("结束");
}
}

Page 继承自 Slice ,Slice 不返回分页的可以使用的总页数,关心的是是否有下一页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public interface MyPersonRepository extends Repository<Person, Integer> {
Slice<Person> findPersonByFirstName(String name,Pageable pageable);
}

@ComponentScan({"com.hank"})
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)
@EnableTransactionManagement
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
var repository = context.getBean(MyPersonRepository.class);
var pageable = PageRequest.of(0, 2);
var slice = repository.findPersonByFirstName("ok", pageable);
System.out.println(slice.getContent());

if(slice.hasNext()){
System.out.println("还有数据可以加载");
}
}
}

使用 Optional 避免空指针

如下代码会报错 ❌

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public interface MyPersonRepository extends Repository<Person, Integer> {
Person findTop1ByFirstName(String name);
}

@ComponentScan({"com.hank"})
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)
@EnableTransactionManagement
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
var repository = context.getBean(MyPersonRepository.class);
//会报空指针异常
var person = repository.findTop1ByFirstName("not exist");
System.out.println(person);
}
}

正确的方法 Optional ✅

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface MyPersonRepository extends Repository<Person, Integer> {
Optional<Person> findTop1ByFirstName(String name);
}

@ComponentScan({"com.hank"})
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)
@EnableTransactionManagement
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
var repository = context.getBean(MyPersonRepository.class);
var optionalPerson = repository.findTop1ByFirstName("not exist");
optionalPerson.ifPresent(p->{
System.out.println(p);
});
}
}

Projections(投影)对查询结果的扩展

基于接口的投影

使用方法定义查询是不能返回部分属性的,通过 Projections 可以解决
如下代码查询数据库的时候只会查询一个属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Entity
@Getter
@Setter
public class Person {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;

@Column(name = "firstName", nullable = true)
String firstName;

@Column(name = "lastName", nullable = true)
String lastName;
}

//只需要查询一个属性,接口里要写获取属性的方法
public interface MyReturn {
String getFirstName();
}

public interface MyPersonRepository extends Repository<Person, Integer> {
MyReturn findFirstByFirstName(String name);
}


@ComponentScan({"com.hank"})
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)
@EnableTransactionManagement
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
var repository = context.getBean(MyPersonRepository.class);
var result = repository.findFirstByFirstName("ok1");
System.out.println(result.getFirstName());
}
}

如下方法对返回的结果在一个方法里进行投影

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface MyReturn {
@Value("#{target.firstName+','+target.lastName}")
String getFullName();
}

//或者

@Component("myBean")
public class MyBean {
public String getFullName(Person person){
//这里可以写复杂的语句用于生成结果
return person.getFirstName()+","+person.getLastName();
}
}

public interface MyReturn {
@Value("#{@myBean.getFullName(target)}")
String getFullName();
}

基于DTO 类的投影-推荐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class MyReturnDto {
private final String firstName;
private final String lastName;

public MyReturnDto(String firstName, String lastName){
this.firstName=firstName;
this.lastName=lastName;
}

public String getFullName(){
return firstName+","+lastName;
}
}
//@Value lombok 简化
@Value
public class MyReturnDto {
private String firstName;
private String lastName;

public String getFullName(){
return firstName+","+lastName;
}
}

public interface MyPersonRepository extends Repository<Person, Integer> {
MyReturnDto findFirstByFirstName(String name);
}

jpa-hibernate 实体的继承关系

会使得表与实体之间的关系变得复杂不直观,增加复杂度,不建议使用。

@MappedSuperclass 只能用在类上

基于代码复用和模型分离的思想,在项目开发中使用JPA的@MappedSuperclass注解将实体类的多个属性分别封装到不同的非实体类中,纯粹的继承,和表没关系,对象之间的字段共享
注意:

  1. 标注为@MappedSuperclass的类将不是一个完整的实体类,他将不会映射到数据库表,但是他的属性都将映射到其子类的数据库字段中。
  2. 标注为@MappedSuperclass的类不能再标注@Entity或@Table注解,也无需实现序列化接口。

此表在数据库不会存在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Setter
@Getter
@MappedSuperclass
public abstract class Audit {
@Column(name = "createUser")
private String createBy;

@Column(name = "editUser")
private String editBy;
}

/*
继承 @MappedSuperclass 标注的类
*/
@Entity
@Data
public class Bar extends Audit {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue
private Long id;
}

@SecondaryTable 数据库把主表的部分字段分到子表 -不用,增加复杂度

子表1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Setter
@Getter
@Entity
public class Base1 {
@Id
private Long id;
public String name1;
}

子表2

@Setter
@Getter
@Entity
public class Base2 {
@Id
private Long id;
public String name2;
}

主表 @SecondaryTable(name =”base1”,pkJoinColumns = @PrimaryKeyJoinColumn(name = “id”)) 指定主表的相关字段数据保存在子表
在数据库里主表自有 id ,name 字段,其他字段都是保存在子表里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Entity
@Setter
@Getter
//定义子表的路径在哪里
@SecondaryTable(name ="base1",pkJoinColumns = @PrimaryKeyJoinColumn(name = "id"))
@SecondaryTable(name ="base2",pkJoinColumns = @PrimaryKeyJoinColumn(name = "id"))
public class Bar {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String name;

//告诉 jpa 此字段的数据保存在那个子表
@Column(table = "base1")
public String name1;

//告诉 jpa 此字段的数据保存在那个子表
@Column(table = "base2")
public String name2;
}

@Inheritance -不用,增加复杂度

  1. @Inheritance (strategy = InheritanceType.SINGLE_TABLE) 将所有父类和子类集合在一张表,只有父类有 @Table ,其他表的字段集合到父类 ,在父类里加字段进行区分子类
  2. @Inheritance (strategy = InheritanceType.TABLE_PER_CLASS) 每个子类会生成一张单独的表,父类可以查询所有子类的表数据
  3. @Inheritance (strategy = InheritanceType.JOINED) 每个类分别生成一张单独的表,但是每张表只有自己的属性,没有父类的属性,通过外键关联的形式使两张表关联起来

jpa-hibernate @Entity Callbacks-的回调方法

JPA 协议里面规定,可以通过一些注解,为其监听回调事件、指定回调方法

注解 备注
@PrePersist 在新增之前的回调
@PostPersist 在新增到数据库之前的回调
@PreRemove 在数据库删除之前的回调
@PostRemove 在数据库删除成功之后的回调
@PreUpdate 在更新到数据库之前回调
@PostUpdate 在数据库更新之后的回调
@PostLoad 在实体加载之后的回调

语法注意事项,关于上表所述的几个方法有一些需要注意的地方,如下:

  1. 回调函数都是和 EntityManager.flush 或 EntityManager.commit 在同一个线程里面执行的,只不过调用方法有先后之分,都是同步调用,所以当任何一个回调方法里面发生异常,都会触发事务进行回滚,而不会触发事务提交。
  2. Callbacks 注解可以放在实体里面,可以放在 super-class 里面,也可以定义在 entity 的 listener 里面,但需要注意的是:放在实体(或者 super-class)里面的方法,签名格式为“void ()”,即没有参数,方法里面操作的是 this 对象自己;放在实体的 EntityListener 里面的方法签名格式为“void (Object)”,也就是方法可以有参数,参数是代表用来接收回调方法的实体。
  3. 使上述注解生效的回调方法可以是 public、private、protected、friendly 类型的,但是不能是 static 和 final 类型的方法。

在标注 @super-class 注解的实体里写回调方法

在 @MappedSuperclass 标记的类里,声明的回调会在所有的实现类里起到横向拦截的功能,可以自己实现审计追踪的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
@Setter
@Getter
@MappedSuperclass
public abstract class Audit {
@Column(name = "createUser")
private String createBy;

@Column(name = "editUser")
private String editBy;

@PrePersist
public void prePersist(){
//在新增数据未到数据库前,修改的数据会到数据库
this.setCreateBy( this.getCreateBy()+"新增前");
System.out.println(this+",prePersist");
}
@PostPersist
public void postPersist(){
//数据已经新增到数据库了,此时修改的数据并不能保存到数据库
this.setCreateBy( this.getCreateBy()+"新增后");
System.out.println(this+",postPersist");
}

@PreUpdate
public void preUpdate(){
//在新增数据未到数据库前,修改的数据会到数据库
this.setCreateBy( this.getCreateBy()+"更新前");
System.out.println(this+",preUpdate");
}

@PostUpdate
public void postUpdate(){
//数据已经新增到数据库了,此时修改的数据并不能保存到数据库
this.setCreateBy( this.getCreateBy()+"更新后");
System.out.println(this+",postUpdate");
}

@PreRemove
public void preRemove(){
System.out.println(this+",preRemove");
}

@PostRemove
public void postRemove(){
System.out.println(this+",postRemove");
}

@PostLoad
public void load(){
System.out.println(this+",load");
}
}


@Entity
@Data
public class Bar extends Audit {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue
private Long id;


@Override
public String toString(){
return this.getCreateBy()+";"+this.getEditBy();
}
}

@Entity
@Data
public class Foo extends Audit {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue
private Long id;


@Override
public String toString(){
return this.getCreateBy()+";"+this.getEditBy();
}
}

自定义 EntityListener,不建议使用

此类实例化一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class MyAuditingEntityListener {
@PrePersist
public void prePersist(Audit obj){
//在新增数据未到数据库前,修改的数据会到数据库
obj.setCreateBy( obj.getCreateBy()+"新增前");
System.out.println(obj+",prePersist");
}
@PostPersist
public void postPersist(Audit obj){
//数据已经新增到数据库了,此时修改的数据并不能保存到数据库
obj.setCreateBy( obj.getCreateBy()+"新增后");
System.out.println(obj+",postPersist");
}

@PreUpdate
public void preUpdate(Audit obj){
//在新增数据未到数据库前,修改的数据会到数据库
obj.setCreateBy( obj.getCreateBy()+"更新前");
System.out.println(obj+",preUpdate");
}

@PostUpdate
public void postUpdate(Audit obj){
//数据已经新增到数据库了,此时修改的数据并不能保存到数据库
obj.setCreateBy( obj.getCreateBy()+"更新后");
System.out.println(obj+",postUpdate");
}

@PreRemove
public void preRemove(Audit obj){
System.out.println(obj+",preRemove");
}

@PostRemove
public void postRemove(Audit obj){
System.out.println(obj+",postRemove");
}

@PostLoad
public void load(Audit obj){
System.out.println(obj+",load");
}
}

通过 @EntityListeners 和回调类进行绑定

1
2
3
4
5
6
7
8
9
10
11
@Setter
@Getter
@MappedSuperclass
@EntityListeners(MyAuditingEntityListener.class)
public abstract class Audit {
@Column(name = "createUser")
private String createBy;

@Column(name = "editUser")
private String editBy;
}

jpa-hibernate 乐观锁和重试机制

@Version 原理是会在一次查询时获得 version ,做更新后,在更新到数据库前再次到数据库查询最新的 version ,对比这俩个 version 的数值是否相等,如果相等说明在此时间内数据库的此条记录没有更新,本次操作事务可以成功,否则事务执行失败抛出错误

注意点如果是通过 @Query 执行JPQL , version 需要自己控制

1
2
3
4
5
6
7
8
9
10
11
12
13
@Modifying
@Query("update Account set name=:name, version=:version+1 where id=:id and version=:version")
int updateBarByVersion(@Param("id") int id,@Param("name") String name, @Param("version") int version);

//service中加判断,抛异常,这样就自己通过数据库实现了乐观锁
@Transactional(rollbackFor = Exception.class)
public int updateBarService(Bar bar){
int i =Dao.updateBarByVersion(bar.getId(),bar.getName(),bar.getVersion());
if(i==0){
throw new ObjectOptimisticLockingFailureException("更新bar失败",new Exception());
}
return i;
}

通过JPA 仓库 实现 CRUD , version 字段不要自己控制 ,jpa 会做判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Entity
@Data
public class Bar {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue
private Long id;

private String name;

//此字段有 JPA 自己进行更新,所以设置 AccessLevel.PRIVATE
@Version
@Setter(AccessLevel.PRIVATE)
private int version;
}

如果因为 version 不一致就让事务失败了,就放弃此次操作,是不合理的(如网络问题),加入重试机制解决

@EnableRetry 让重试起作用 , @Retryable 在需要重试方法或类上加此注解

@Retryable

  • value:指定发生的异常进行重试
  • include:和value一样,默认空,当exclude也为空时,所有异常都重试
  • exclude:指定异常不重试,默认空,当include也为空时,所有异常都重试
  • maxAttemps:重试次数,默认3
  • backoff:重试补偿机制,默认没有

@Backoff注解

  • delay:指定延迟后重试
  • multiplier:指定延迟的倍数,比如delay=5000l,multiplier=2时,第一次重试为5秒后,第二次为10秒,第三次为20秒

@Recover

  • 当重试到达指定次数时,被注解的方法将被回调,可以在该方法中进行日志处理。需要注意的是发生的异常和入参类型一致时才会回调。

!参考(https://www.baeldung.com/spring-retry,https://github.com/spring-projects/spring-retry)

依赖的包

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 使用 spring-retry 包实现重试 -->
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>2.0.0</version>
</dependency>

<!-- 重试的机制是 aop 代理,需要引入此包 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.9.1</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public interface MyRepository extends JpaRepository<Bar, Long> {
}

/*
定义基类处理重试
*/
@Retryable(value = RuntimeException.class, maxAttempts = 3, backoff = @Backoff(value = 1000, multiplier = 1.5))
public abstract class BaseService {

/**
* 被此注解处理的异常,不会向上抛出,除非手动抛出异常
* @param exception
*/
@Recover
public void fun(RuntimeException exception){
System.out.println("连续重试还是无效:"+exception.getMessage());
}
}

/*
服务类
*/
@Component
public class MyService extends BaseService{
@Autowired
private MyRepository myRepository;

public void f(){
var result= myRepository.findById(58L);
result.ifPresent(p->{
p.setName(LocalDateTime.now().toString());
myRepository.save(p);
});
}
}

启动类上加 @EnableRetry 注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@ComponentScan({"com.hank"})
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)
@EnableRetry
@EnableTransactionManagement
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
var myService= context.getBean(MyService.class);
myService.f();
}
}

jpa-hibernate 提供的注解

注解 说明
@Entity 将此类标记为实体,被标注的实体,将映射到数据库
@Embedded 将此类标记为值对象
@Table 指定此实体的数据的表名,不指定默认用类名
@Id 将此类的字段标记为主键
@IdClass 利用外部类定义联合主键,标记在实体类上
@Column 将此类的字段和数据库的列名进行绑定
@Version 用于支持乐观锁版本控制
@Transient 标注的字段不会在数据库中存在,比如年龄可以自动计算
@Enumerated 如果字段是枚举类型,默认映射到数据库的是 int 类型,如果想要映射字符串 @Enumerated(EnumType.STRING),建议映射字符串
@GeneratedValue 为主键生成策略
@Basic 表示属性是到数据库表的字段的映射。如果实体的字段上没有任何注解,默认即为@Basic
@Temporal 旧的 java.util.Date 不能区分日期和时间,但是数据库默认有 date , time , datetime. 此注解就是指定映射关系
@Lob 将属性映射为数据库支持的大数据对象类型
@Access 用于指定 entity 里面的注解是写在字段上面,还是 get/set 方法上面生效,非必填。在默认不填写的情况下,当实体里面的第一个注解出现在字段上或者 get/set 方法上面,就以第一次出现的方式为准,不能一些写在字段上,一些写在方法上

@Table

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Target(TYPE) 
@Retention(RUNTIME)
public @interface Table {
/**
* 数据库里的表名
*/
String name() default "";

/**
* 数据库实例名,在驱动连接的 url 里已经指定了实例,默认不指定数据库的表出现在 url 里的实例里
* 如果在这里指定实例,这此表会在此实例里创建,如果数据库根本就没有此实例,者创建表的过程失败
*/
String catalog() default "";

/**
* 不是所有数据库都支持。mySQL 就不支持
*/
String schema() default "";

/**
* 指定此表的唯一约束
@Table(name = "users", uniqueConstraints = {
//复合字段的唯一约束
@UniqueConstraint(columnNames = {"name", "age"}),
//单个字段的唯一约束
@UniqueConstraint(columnNames = {"account"})
})
*/
UniqueConstraint[] uniqueConstraints() default {};

/**
指定此表的索引
*/
Index[] indexes() default {};
}

@IdClass , @EmbeddedId 联合主键

@Embeddable 与 @EmbeddedId 注解同样可以做到联合主键的效果,并且更方便

作为符合主键类,要满足以下几点要求。

  1. 必须实现Serializable接口。
  2. 必须有默认的public无参数的构造方法。
  3. 必须覆盖equals和hashCode方法。
    • equals方法用于判断两个对象是否相同,EntityManger通过find方法来查找Entity时是根据equals的返回值来判断的。
    • hashCode方法返回当前对象的哈希码,用于equals判断

联合主键必须定义在一个类里

1
2
3
4
5
6
7
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CityIdentity implements Serializable {
private String name;
private String address;
}

@IdClass 定义联合主键,缺点是要写字符串的硬编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Entity
@Data
//告知 jpa 此实体的的联合组建是 CityIdentity
@IdClass(CityIdentity.class)
public class City {
@Id
@Column(name="name")
//属性名必须是在 CityIdentity 里定义的属性
private String name;

@Id
@Column(name="address")
//属性名必须是在 CityIdentity 里定义的属性
private String address;

private int peopleAccount;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public interface MyPersonRepository extends CrudRepository<City, CityIdentity> {
List<City> findCityByPeopleAccount(int account);
}

@ComponentScan({"com.hank"})
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)
@EnableTransactionManagement

public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
var repository = context.getBean(MyPersonRepository.class);
{
var city=new City();
city.setName("bj");
city.setAddress("维度8");
city.setPeopleAccount(10000);
repository.save(city);
}

{
var city=new City();
city.setName("sh");
city.setAddress("维度8");
city.setPeopleAccount(10000);
repository.save(city);
}

{
var id=new CityIdentity();
id.setAddress("维度8");
id.setName("bj");
//通过ID 查询
var city = repository.findById(id);
System.out.println(city);
}
}
}

@EmbeddedId 定义联合主键,优点是不需要要写字符串的硬编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Data
@NoArgsConstructor
@AllArgsConstructor
//把联合主键定义成嵌入类型
@Embeddable
public class CityIdentity implements Serializable {
private static final long serialVersionUID = -8229477451589838531L;
private String name;
private String address;
}

@Entity
@Data
public class City {
//嵌入式的主键
@EmbeddedId
private CityIdentity cityIdentity;
private int peopleAccount;
}

@GeneratedValue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

public enum GenerationType {

/*
使用一个特定的数据库表格来保存主键,持久化引擎通过关系数据库的一张特定的表格来生成主键,这种策略的好处就是不依赖于外部环境和数据库的具体实现,在不同数据库间可以很容易的进行移植
*/
TABLE,
/*
在某些数据库中,不支持主键自增长,比如Oracle,其提供了一种叫做"序列(sequence)"的机制生成主键
*/
SEQUENCE,
/*
数据库生成自增长的 id
*/
IDENTITY,
/*
默认值,有 jip 根据数据库引擎自己决定
*/
AUTO
}

@Column

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@Target({METHOD, FIELD}) 
@Retention(RUNTIME)
public @interface Column {

/**
数据库内映射的列名
*/
String name() default "";

/**
此字段是否唯一,也可以在 @Table 里指定唯一约束
*/
boolean unique() default false;

/**
* 是否可为 null
*/
boolean nullable() default true;

/**
* 执行 insert 时是否包含此字段
*/
boolean insertable() default true;

/**
* 执行 update 时是否包含此字段
*/
boolean updatable() default true;

/**
* 此字段在数据库中的指定的数据类型
* 编程语言中字符串一般都用String表示,但是数据库中varcahr数值类型有长度限制,一旦需要大文本,则需要text数值类型
* 但是String类型默认映射的数值类型是varchar,columnDefinition可以进行额外指定 -> columnDefinition="text"
*/
String columnDefinition() default "";

/**
* todo 不确定做什么用
*/
String table() default "";

/**
* 如果是字符串类型 ,可以指定字符串的长度
*/
int length() default 255;

/**
* 表示浮点数的总长度(整数部分和小数部分的长度和)
*/
int precision() default 0;

/**
* 表示浮点数的小数部分的长度
*/
int scale() default 0;
}

@Temporal

1
2
3
4
5
6
7
8
9

public enum TemporalType {
/** Map as <code>java.sql.Date</code> */
DATE,
/** Map as <code>java.sql.Time</code> */
TIME,
/** Map as <code>java.sql.Timestamp</code> */
TIMESTAMP
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Entity
@Data
@IdClass(CityIdentity.class)
public class City {
@Id
@Column(name="name")
private String name;

@Id
@Column(name="address")
private String address;


private int peopleAccount;

//为了配合旧的 Date API
@Temporal(TemporalType.DATE)
private Date overDate;


//如下采用新的 API 根本不需要 @Temporal 做映射
private LocalDate createDate;
private LocalTime updateTime;
private LocalDateTime happenDateTime;
}

@Lob

@Lob 注解支持以下数据库类型的字段:

  1. Clob( Character Large Ojects) 类型是长字符串类型,java.sql.Clob、Character[]、char[] 和 String 将被映射为 Clob 类型。
  2. Blob( Binary Large Objects) 类型是字节类型,java.sql.Blob、Byte[]、byte[] 和实现了 Serializable 接口的类型将被映射为 Blob 类型。

注意:Clob、 Blob 占用内存空间较大,一般配合 @Basic(fetch=FetchType.LAZY) 注解将其设置为延迟加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//可序列的对象也可以保存为数据库的二进制数据
@Data
public class EmbObject implements Serializable {
private String name;
private int account;
}

@Entity
@Data
public class BlobObject {
@Id
@Column(name = "id", nullable = false)
private Long id;

//longblob
@Lob
private String summary1;
//longblob
@Lob
private char[] summary2;
//longblob
@Lob
private Character[] summary3;


//longtext
@Lob
private byte[] face1;
//longtext
@Lob
private Byte[] face2;
//longtext
@Lob
private EmbObject embObject;
}

@Basic

//TODO 对非关联对象的字段使用延迟加载没有效果
某些情况下不需要立即加载某字段的值,可以采用延迟加载

1
2
3
4
5
6
7
8
9
@Target({METHOD, FIELD}) 
@Retention(RUNTIME)
public @interface Basic {
/*
LAZY 延迟加载, 立即加载 EAGER,默认立即加载此字段
*/
FetchType fetch() default EAGER;
boolean optional() default true;
}

@Embeddable , @Embedded 值对象

注意点:

  1. @Embeddable 定义没有主键的值对象
  2. @Embedded 把值对象嵌入实体类里面
  3. @AttributeOverrides 如果一个实体类有多个相同的值对象,用此注解去定义不同值对象在数据库的列名
    麻烦之处是如果值对象的字段比较多,此注解写起来相当麻烦

定义值对象

1
2
3
4
5
6
@Data
@Embeddable
public class MyAddress {
private String road;
private String city;
}

嵌入值对象到实体对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Entity
@Data
public class CargoOrder {
@Id
private Long id;

private String cargoName;

@Embedded
@AttributeOverrides({
@AttributeOverride(name = "road",column = @Column(name = "sendAddress_road")),
@AttributeOverride(name = "city",column = @Column(name = "sendAddress_city"))
})
private MyAddress sendAddress;

@Embedded
@AttributeOverrides({
@AttributeOverride(name = "road",column = @Column(name = "receiveAddress_road")),
@AttributeOverride(name = "city",column = @Column(name = "receiveAddress_city"))
})
private MyAddress receiveAddress;
}

嵌入值对象集合到实体对象

会另外生产一个表用于保存值对象的集合,数据库值对象集合的表和实体表是强关联的表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Entity
@Data
public class CargoOrder {
@Id
@GeneratedValue

private Long id;

private String cargoName;

/*
@ElementCollection(fetch = FetchType.EAGER) 设置成立即加载对集合值对象,用默认的懒加载会报错
*/
@ElementCollection(fetch = FetchType.EAGER)
private List<MyAddress> receiveAddress;
}

针对集合也的写一个强类型的转换器

1
2
3
4
5
6
7
8
9
10
// 此注解会被 spring jpa 全局使用,不需要在值对象字段上加 @Convert(converter = MyInstanceJpaConvertJson.class)
// 如果不加此注解,需要在值对象字段上加 @Convert(converter = MyInstanceJpaConvertJson.class)
// @Converter(autoApply = true)
@Converter
public class MyInstanceJpaConvertJson1 implements JpaConverterJson<List<Contacts>> {
@Override
public List<Contacts> getInstance() {
return new ArrayList<Contacts>() ;
}
}

值对象的其他解决方法

值对象作为可序列化对象

1
2
3
4
5
@Data
public class Contacts implements Serializable {
private String name;
private String phone;
}

序列化为二进制数据,对集合不行

1
2
3
4
5
6
7
8
9
10
11
12
@Entity
@Data
public class CargoOrder {
@Id
private Long id;

private String cargoName;

//默认不加注解,保存到数据库是二进制数据
@Column(name = "sendContacts")
private Contacts sendContacts;
}

自定义保存方式,这个最为灵活

自定义一个数据库和实体字段转换器,此转换器把对象转换为 json 字符串

定义一个泛型的转换器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public interface JpaConverterJson<T> extends AttributeConverter<T, String> {

T getInstance();

final static ObjectMapper objectMapper = new ObjectMapper();

@Override
default public String convertToDatabaseColumn(T meta) {
try {
return objectMapper.writeValueAsString(meta);
} catch (JsonProcessingException ex) {
return null;
// or throw an error
}
}

@Override
default public T convertToEntityAttribute(String dbData) {
try {
var obj = objectMapper.readValue(dbData, (Class<T>) getInstance().getClass());
return obj;
} catch (IOException ex) {
return null;
}
}
}

强类型的转换器,每个值对象都要定义一个

1
2
3
4
5
6
7
8
// @Converter(autoApply = true) 此注解会被 spring jpa 全局使用,不需要在值对象字段上加 @Convert(converter = MyInstanceJpaConvertJson.class)
// @Converter注解,需要在值对象字段上加 @Convert(converter = MyInstanceJpaConvertJson.class)
public class MyInstanceJpaConvertJson implements JpaConverterJson<Contacts> {
@Override
public Contacts getInstance() {
return new Contacts();
}
}

对实体里的值对象字段应用转换器

1
2
3
4
5
6
7
8
9
10
11
12
@Entity
@Data
public class CargoOrder {
@Id
private Long id;

private String cargoName;

@Column(name = "receiveContacts")
@Convert(converter = MyInstanceJpaConvertJson.class)
private Contacts receiveContacts;
}

嵌入值对象集合到实体对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Entity
@Data
public class CargoOrder {
@Id
@GeneratedValue

private Long id;

private String cargoName;

/**
* 这种方式可以写入但是不能读取,等于不能用
*/
//@Column(name = "sendContacts",columnDefinition = "blob")
//private List<Contacts> sendContacts=new ArrayList<>();


/*
自定义序列化方式是比较好用的
*/
@Column(name = "receiveContacts",length = 2000)
@Convert(converter = MyInstanceJpaConvertJson1.class)
private List<Contacts> receiveContacts;
}

针对集合也的写一个强类型的转换器

1
2
3
4
5
6
7
8
9
10
// 此注解会被 spring jpa 全局使用,不需要在值对象字段上加 @Convert(converter = MyInstanceJpaConvertJson.class)
// 如果不加此注解,需要在值对象字段上加 @Convert(converter = MyInstanceJpaConvertJson.class)
// @Converter(autoApply = true)
@Converter
public class MyInstanceJpaConvertJson1 implements JpaConverterJson<List<Contacts>> {
@Override
public List<Contacts> getInstance() {
return new ArrayList<Contacts>() ;
}
}

jpa-hibernate 关联关系

spring data jpa 没有提供关联关系的扩展,关联关系注解包括 @JoinColumn、 @OneToOne、 @OneToMany、 @ManyToOne、 @ManyToMany、@JoinTable、 @OrderBy。
注意点:
多对多关联关系的实体需要加 Serializable 接口

@JoinColumn 注解用来定义主键字段和外键字段的对应关系

如果没有 @JoinColumn 注解的话默认通过两个实体的主键进行关联 name

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@Repeatable(JoinColumns.class)
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface JoinColumn {

/**
定义在数据库里的外键名,此外键名会在使用 @JoinColumn 注解的表里创建.主表通过外键名去查找关联表
默认不指定的化,值是关联表的表名+主键
*/
String name() default "";

/**
此字段用途是 name() 方法定义的外键,指向关联表的字段,默认不写的化是指向关联表的 id
name() 定义的字段里存放的数值是 referencedColumnName() 指定的字段
*/
String referencedColumnName() default "";

/**
外键字段是否唯一
*/
boolean unique() default false;


/**
外键字段是是否允许为空
*/
boolean nullable() default true;

/**
是否允许插入, false :关联表会插入数据,但是外键字段会为 null
*/
boolean insertable() default true;

/**
是否允许更新 没有测试过效果
*/
boolean updatable() default true;

/**
定义建表时创建此列的DDL
*/
String columnDefinition() default "";

/**

*/
String table() default "";

/**

*/
ForeignKey foreignKey() default @ForeignKey(PROVIDER_DEFAULT);
}

@OneToOne 实体和实体是1对1关联

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Target({METHOD, FIELD}) 
@Retention(RUNTIME)

public @interface OneToOne {

/**

*/
Class targetEntity() default void.class;

/**
指定关联操作,查询不需要关联操作
*/
CascadeType[] cascade() default {};

/**
关联表是否立即加载,默认立即加载
*/
FetchType fetch() default EAGER;

/**

*/
boolean optional() default true;

/**

*/
String mappedBy() default "";


/**

*/
boolean orphanRemoval() default false;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Entity
@Data
public class CargoOrder {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String name;

//设置关联是懒加载的方式
@OneToOne(cascade = CascadeType.ALL,fetch = FetchType.LAZY)
//设置外键去关联 customer 表,不是用此注解会生成默认的外键
@JoinColumn(name = "customer_id")
private Customer customer;
}


@Data
@Entity
public class Customer implements Serializable {
private static final long serialVersionUID = 6286816358723854706L;
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
private String name;

//一般我们的关联关系都是单个的
//@OneToOne(cascade = CascadeType.ALL)
//public CargoOrder cargoOrder;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
var  myRepository =  context.getBean(MyRepository.class);
//执行下面语句后,session 已经结束
var result= myRepository.findById(1L);

result.ifPresent(p->{
System.out.println(p.getName());
//查询关联实体会失败, no session 错误
// hibernate 给的解决方法是 JpaProperties hibernate.enable_lazy_load_no_trans 为 true
// 这种方式在关联对象是 @OneToOne 的时候是可以解决问题的 但是在 @OneToMany 的时候会有n +1 的问题

// 开启 JpaProperties hibernate.enable_lazy_load_no_trans,会单独开启会话去查询关联表
System.out.println(p.getCustomer().getName());
});

@OneToMany 实体和实体是1对多关联

@OneToMany 1的一方没有办法维护多的一方的所有外键,但是在多的一方,可以维护1的一方的外键,如果在多的一方维护外键关系用 @ManyToOne 维护,此种情况 hibernate 会生成一张中间表来维护外键之间的关系
数据库中间表的表名是 CargoOrder_Customer 字段是: CargoOrder_Customer,customers_id

1
2
3
4
5
6
7
8
9
10
11
12
13
@Entity
@Data
public class CargoOrder {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String name;

@OneToMany(cascade = CascadeType.ALL,fetch = FetchType.LAZY)
private Set<Customer> customers=new HashSet<Customer>();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Setter
@Getter
@Entity
public class Customer implements Serializable {
private static final long serialVersionUID = 6286816358723854706L;
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
private String name;

private Integer age;

@ManyToOne
private CargoOrder cargoOrder;
}

解决方法1: 1端维护外键,但是外键字段定义在多方表里,在多方表插入数据后1端会更新多方表的外键字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//1的一方
@Entity
@Setter
@Getter
public class CargoOrder {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String name;

//OneToMany 默认是懒加载
@OneToMany(cascade = CascadeType.ALL)
//给 Customer 设置外键指向 CargoOrder
//hibernate 知道这里设置外键肯定不是给 CargoOrder 设置外键 指向 Customer
@JoinColumn(name = "cargoOrderID")
private Set<Customer> customers=new HashSet<Customer>();
}

//多的一方,不需要做其他配置
@Data
@Entity
public class Customer implements Serializable {
private static final long serialVersionUID = 6286816358723854706L;
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
private String name;
}

解决方法2: 1端放弃维护外键,有多方自己维护外键,此外键字段在多方表里,外键的插入是在多方新增数据的时候,同时插入的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//1端

@Entity
@Setter
@Getter
public class CargoOrder implements Serializable {
private static final long serialVersionUID = 458774547746966971L;
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String name;

/*
mappedBy 告知 jpa 外键关系由 Customer 表的 cargoOrder 属性来维护
Customer_.CARGO_ORDER 这个是 jpa metamodel 插件提供的功能
*/
@OneToMany(cascade = CascadeType.ALL,fetch = FetchType.LAZY,mappedBy = Customer_.CARGO_ORDER)
//这里不能在定义外键列了
private Set<Customer> customers=new HashSet<Customer>(0);
}

//多端

@Entity
@Setter
@Getter
public class Customer implements Serializable {

private static final long serialVersionUID = 5227592687670264157L;
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
private String name;

@ManyToOne(cascade = CascadeType.ALL,fetch = FetchType.LAZY)
//此处的外键列可以不定义,用默认值
@JoinColumn(name = "cargoOrderID")
private CargoOrder cargoOrder;
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@ComponentScan({"com.hank"})
@EnableJpaRepositories(
basePackages = {"com.hank.Pesis"},
entityManagerFactoryRef = "myEntityManagerFactory",
transactionManagerRef = "jdbcTransactionManager"
)
@EnableTransactionManagement
public class Programmer {
public static void main(String[] args) throws Exception {
var context = new AnnotationConfigApplicationContext(Programmer.class);
MyRepository myRepository = context.getBean(MyRepository.class);


val cargoOrder = new CargoOrder();
cargoOrder.setName("iphone");
var cus=new HashSet<Customer>();
cus.add(new Customer(){{
setName(LocalDateTime.now().toString());
//有多端来维护外键,如果不这样写多端表里的外键将为空,因为 mappedBy 已经告知 jpa ,外键有多端维护
setCargoOrder(cargoOrder);
}});
cargoOrder.setCustomers(cus);
myRepository.save(cargoOrder);


{
var result= myRepository.<CargoOrder>findById(1L);
result.ifPresent(p->{
val customer = new Customer();
customer.setName(LocalDateTime.now().toString());

/*
如下在多端维护外键的时候,会报在临时对象的属性里赋值持久性或分离对象不被允许
customer 是灵思对象,p 是分离对象,因为此时事务已经结束

解决方法:JpaProperties 属性增加 put("hibernate.event.merge.entity_copy_observer","allow");
*/
customer.setCargoOrder(p);

p.getCustomers().add(customer);
myRepository.save(p);
System.out.println(p);
});
}
}
}

如上俩种方式是互斥的不能混着写,还有就是 lombok 的 @ToString @EqualsAndHashCode 和 hibernate 兼容有问题

@OrderBy 对多端的返回进行排序

一般和@OneToMany一起使用,对多端返回的数据排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Entity
@Setter
@Getter
public class CargoOrder implements Serializable {
private static final long serialVersionUID = 458774547746966971L;
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;


private String name;

@OneToMany(cascade = CascadeType.ALL,fetch = FetchType.LAZY,mappedBy = Customer_.CARGO_ORDER)
//指定返回的多端的排序方式
@OrderBy(Customer_.NAME+" asc,"+Customer_.ID+" asc")
private Set<Customer> customers=new HashSet<Customer>(0);
}

@JoinTable 自定义中间表,基本不用

@JoinTable 注解用于关联映射,它是在关联的拥有方进行配置。使用 @JoinTable 注解将创建一个连接表,也称为“中间表”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Entity
@Setter
@Getter
public class CargoOrder implements Serializable {
private static final long serialVersionUID = 458774547746966971L;
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String name;

//OneToMany 默认会生成中间表,@JoinTable 注解可以自定义中间表的表名,字段等
@OneToMany(cascade = CascadeType.ALL,fetch = FetchType.LAZY)
//自定义中间表
@JoinTable(name = "a_b")
private Set<Customer> customers=new HashSet<Customer>(0);
}

@ManyToMany 多对多-必须要有中间表,基本不用

ManyToMany总是使用中间关系连接表来存储关系。如果两个实体都定义了ManyToMany的话,因为单向关系,会生成有2个中间表。所以需要用 mappedBy 改造,使其只存在一个中间表

jpa-hibernate 缓存

在 JPA2.0 中,缓存分为一级缓存和二级缓存(JPA1.0 只支持一级缓存)

在 JPA 中,持久化上下文(EntityManager)就是 JPA 的一级缓存。在该缓存区中,会将查询到的对象缓存到该区域中。

一级缓存

如果在同一个 EntityManager 中,查询相同 OID 的数据,那么只需要发送一条 sql。在事务提交/关闭 EntityManager 之后,一级缓存会清空。所以在不同的 EntityManager(或者上下文) 中使用不同的一级缓存。JPA 中的一级缓存的缓存能力是非常有限的,因为我们不会经常在一个 EntityManager 中查询相同的数据

JPA 中的一级缓存也可以使用下面的方法手动清除缓存数据:

  1. detach:清除一级缓存中指定的对象
  2. clear:清除一级缓存中的所有的缓存数据

二级缓存

JPA 中的二级缓存通常是用来提高应用程序性能的,它可以避免访问已经从数据库加载的数据,提高访问未被修改数据对象的速度,JPA 二级缓存是跨越持久化上下文的,是真正意义上的全局应用缓存
如果二级缓存激活,JPA 会先从一级缓存中查找实体,未找到再从二级缓存中查找。当二级缓存有效时,就不能依靠事务来保护并发的数据,而是依靠锁策略,如在确认修改后,需要手工处理乐观锁失败等。

JPA 二级缓存通常用来提高性能,同时,使用二级缓存可能会导致提取到“陈旧”数据,也会出现并发写的问题。所以二级缓存最好是用在经常读取的数据,比较少更新数据的情况,而不应该对重要数据使用二级缓存.