SpringBoot系列教程JPA之指定id保存
前几天有位小伙伴问了一个很有意思的问题,使用 JPA 保存数据时,即便我指定了主键 id,但是新插入的数据主键却是 mysql 自增的 id;那么是什么原因导致的呢?又可以如何解决呢?
本文将介绍一下如何使用 JPA 的 AUTO 保存策略来指定数据库主键 id
I. 环境准备
实际开始之前,需要先走一些必要的操作,如安装测试使用 mysql,创建 SpringBoot 项目工程,设置好配置信息等,关于搭建项目的详情可以参考前一篇文章 190612-SpringBoot 系列教程 JPA 之基础环境搭建
下面简单的看一下后续的代码中,需要的配置 (我们使用的是 mysql 数据库)
1. 表准备
沿用前一篇的表,结构如下
CREATE TABLE `money` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
`money` int(26) NOT NULL DEFAULT '0' COMMENT '钱',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0',
`create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
2. 项目配置
配置信息,与之前有一点点区别,我们新增了更详细的日志打印;本篇主要目标集中在添加记录的使用姿势,对于配置说明,后面单独进行说明
## DataSource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=
## jpa相关配置
spring.jpa.database=MYSQL
spring.jpa.hibernate.ddl-auto=none
spring.jpa.show-sql=true
spring.jackson.serialization.indent_output=true
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
II. Insert 教程
首先简单的看一下,我们一般使用默认的数据库自增生成主键的使用方式,以便后面的自定义主键生成策略的对比
对于 jpa 的插入数据的知识点不太清楚的同学,可以看一下之前的博文: 190614-SpringBoot 系列教程 JPA 之新增记录使用姿势
1. 自增主键
首先我们需要定义 PO,与数据库中的表绑定起来
@Data
@DynamicUpdate
@DynamicInsert
@Entity
@Table(name = "money")
public class MoneyPO {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Integer id;
@Column(name = "name")
private String name;
@Column(name = "money")
private Long money;
@Column(name = "is_deleted")
private Byte isDeleted;
@Column(name = "create_at")
@CreatedDate
private Timestamp createAt;
@Column(name = "update_at")
@CreatedDate
private Timestamp updateAt;
}
注意上面的主键生成策略用的是 GenerationType.IDENTITY,配合 mysql 的使用就是利用数据库的自增来生成主键 id
/**
* 新增数据
* Created by @author yihui in 11:00 19/6/12.
*/
public interface MoneyCreateRepositoryV2 extends JpaRepository<MoneyPO, Integer> {
}
接下来保存数据就很简单了
private void addWithId() {
MoneyPO po1 = new MoneyPO();
po1.setId(20);
po1.setName("jpa 一灰灰 1x");
po1.setMoney(2200L + ((long) (Math.random() * 100)));
po1.setIsDeleted((byte) 0x00);
MoneyPO r1 = moneyCreateRepositoryV2.save(po1);
System.out.println("after insert res: " + r1);
}
强烈建议实际的体验一下上面的代码执行
首次执行确保数据库中不存在 id 为 20 的记录,虽然我们的 PO 对象中,指定了 id 为 20,但是执行完毕之后,新增的数据 id 却不是 20
Hibernate: select moneypo0_.id as id1_0_0_, moneypo0_.create_at as create_a2_0_0_, moneypo0_.is_deleted as is_delet3_0_0_, moneypo0_.money as money4_0_0_, moneypo0_.name as name5_0_0_, moneypo0_.update_at as update_a6_0_0_ from money moneypo0_ where moneypo0_.id=?
Hibernate: insert into money (is_deleted, money, name) values (?, ?, ?)
after insert res: MoneyPO(id=104, name=jpa 一灰灰 1x, money=2208, isDeleted=0, createAt=null, updateAt=null)
上面是执行的 sql 日志,注意插入的 sql,是没有指定 id 的,所以新增的记录的 id 就会利用 mysql 的自增策略
当我们的 db 中存在 id 为 20 的记录时,再次执行,查看日志发现实际执行的是更新数据
Hibernate: select moneypo0_.id as id1_0_0_, moneypo0_.create_at as create_a2_0_0_, moneypo0_.is_deleted as is_delet3_0_0_, moneypo0_.money as money4_0_0_, moneypo0_.name as name5_0_0_, moneypo0_.update_at as update_a6_0_0_ from money moneypo0_ where moneypo0_.id=?
Hibernate: update money set create_at=?, money=?, name=?, update_at=? where id=?
after insert res: MoneyPO(id=20, name=jpa 一灰灰 1x, money=2234, isDeleted=0, createAt=null, updateAt=null)
大胆猜测,save 的执行过程逻辑如
- 首先根据 id 到数据库中查询对应的数据
- 如果数据不存在,则新增(插入 sql 不指定 id)
- 如果数据存在,则判断是否有变更,以确定是否需要更新
2. 指定 id
那么问题来了,如果我希望当我的 po 中指定了数据库 id 时,db 中没有这条记录时,就插入 id 为指定值的记录;如果存在记录,则更新
要实现上面这个功能,自定义主键 id,那么我们就需要修改一下主键的生成策略了,官方提供了四种
| 取值 | 说明 |
|---|---|
GenerationType.TABLE |
使用一个特定的数据库表格来保存主键 |
GenerationType.SEQUENCE |
根据底层数据库的序列来生成主键,条件是数据库支持序列 |
GenerationType.IDENTITY |
主键由数据库自动生成(主要是自动增长型) |
GenerationType.AUTO |
主键由程序控制 |
从上面四种生成策略说明中,很明显我们要使用的就是 AUTO 策略了,我们新增一个 PO,并指定保存策略
@Data
@DynamicUpdate
@DynamicInsert
@Entity
@Table(name = "money")
public class AutoMoneyPO {
@Id
@GeneratedValue(strategy = GenerationType.AUTO, generator = "myid")
@GenericGenerator(name = "myid", strategy = "com.git.hui.boot.jpa.generator.ManulInsertGenerator")
@Column(name = "id")
private Integer id;
@Column(name = "name")
private String name;
@Column(name = "money")
private Long money;
@Column(name = "is_deleted")
private Byte isDeleted;
@Column(name = "create_at")
@CreatedDate
private Timestamp createAt;
@Column(name = "update_at")
@CreatedDate
private Timestamp updateAt;
}
采用自定义的生成策略,需要注意,@GenericGenerator(name = "myid", strategy = "com.git.hui.boot.jpa.generator.ManulInsertGenerator")这个需要有,否则执行会抛异常
这一行代码的意思是,主键 id 是由ManulInsertGenerator来生成
/**
* 自定义的主键生成策略,如果填写了主键id,如果数据库中没有这条记录,则新增指定id的记录;否则更新记录
*
* 如果不填写主键id,则利用数据库本身的自增策略指定id
*
* Created by @author yihui in 20:51 19/11/13.
*/
public class ManulInsertGenerator extends IdentityGenerator {
@Override
public Serializable generate(SharedSessionContractImplementor s, Object obj) throws HibernateException {
Serializable id = s.getEntityPersister(null, obj).getClassMetadata().getIdentifier(obj, s);
if (id != null && Integer.valueOf(id.toString()) > 0) {
return id;
} else {
return super.generate(s, obj);
}
}
}
具体的主键生成方式也比较简单了,首先是判断 PO 中有没有主键,如果有则直接使用 PO 中的主键值;如果没有,就利用IdentityGenerator策略来生成主键(而这个主键生成策略,正好是GenerationType.IDENTITY利用数据库自增生成主键的策略)
接下来我们再次测试插入
// 使用自定义的主键生成策略
AutoMoneyPO moneyPO = new AutoMoneyPO();
moneyPO.setId(20);
moneyPO.setName("jpa 一灰灰 ex");
moneyPO.setMoney(2200L + ((long) (Math.random() * 100)));
moneyPO.setIsDeleted((byte) 0x00);
AutoMoneyPO res = moneyCreateRepositoryWithId.save(moneyPO);
System.out.println("after insert res: " + res);
moneyPO.setMoney(3200L + ((long) (Math.random() * 100)));
res = moneyCreateRepositoryWithId.save(moneyPO);
System.out.println("after insert res: " + res);
moneyPO = new AutoMoneyPO();
moneyPO.setName("jpa 一灰灰 2ex");
moneyPO.setMoney(2200L + ((long) (Math.random() * 100)));
moneyPO.setIsDeleted((byte) 0x00);
res = moneyCreateRepositoryWithId.save(moneyPO);
System.out.println("after insert res: " + res);
上面的代码执行时,确保数据库中没有主键为 20 的数据,输出 sql 日志如下
# 第一次插入
Hibernate: select automoneyp0_.id as id1_0_0_, automoneyp0_.create_at as create_a2_0_0_, automoneyp0_.is_deleted as is_delet3_0_0_, automoneyp0_.money as money4_0_0_, automoneyp0_.name as name5_0_0_, automoneyp0_.update_at as update_a6_0_0_ from money automoneyp0_ where automoneyp0_.id=?
Hibernate: insert into money (is_deleted, money, name, id) values (?, ?, ?, ?)
after insert res: AutoMoneyPO(id=20, name=jpa 一灰灰 ex, money=2238, isDeleted=0, createAt=null, updateAt=null)
# 第二次指定id插入
Hibernate: select automoneyp0_.id as id1_0_0_, automoneyp0_.create_at as create_a2_0_0_, automoneyp0_.is_deleted as is_delet3_0_0_, automoneyp0_.money as money4_0_0_, automoneyp0_.name as name5_0_0_, automoneyp0_.update_at as update_a6_0_0_ from money automoneyp0_ where automoneyp0_.id=?
Hibernate: update money set create_at=?, money=?, update_at=? where id=?
after insert res: AutoMoneyPO(id=20, name=jpa 一灰灰 ex, money=3228, isDeleted=0, createAt=null, updateAt=null)
# 第三次无id插入
Hibernate: insert into money (is_deleted, money, name) values (?, ?, ?)
after insert res: AutoMoneyPO(id=107, name=jpa 一灰灰 2ex, money=2228, isDeleted=0, createAt=null, updateAt=null)
注意上面的日志输出
- 第一次插入时拼装的写入 sql 是包含 id 的,也就达到了我们指定 id 新增数据的要求
- 第二次插入时,因为 id=20 的记录存在,所以执行的是更新操作
- 第三次插入时,因为没有 id,所以插入的 sql 中也没有指定 id,使用 mysql 的自增来生成主键 id
II. 其他
0. 项目&博文
1. 一灰灰 Blog
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现 bug 或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
- 一灰灰 Blog 个人博客 https://blog.hhui.top
- 一灰灰 Blog-Spring 专题博客 http://spring.hhui.top

SpringBoot系列教程JPA之指定id保存的更多相关文章
- SpringBoot 系列教程 JPA 错误姿势之环境配置问题
191218-SpringBoot 系列教程 JPA 错误姿势之环境配置问题 又回到 jpa 的教程上了,这一篇源于某个简单的项目需要读写 db,本想着直接使用 jpa 会比较简单,然而悲催的是实际开 ...
- SpringBoot系列教程JPA之新增记录使用姿势
SpringBoot系列教程JPA之新增记录使用姿势 上一篇文章介绍了如何快速的搭建一个JPA的项目环境,并给出了一个简单的演示demo,接下来我们开始业务教程,也就是我们常说的CURD,接下来进入第 ...
- SpringBoot系列教程JPA之query使用姿势详解之基础篇
前面的几篇文章分别介绍了CURD中的增删改,接下来进入最最常见的查询篇,看一下使用jpa进行db的记录查询时,可以怎么玩 本篇将介绍一些基础的查询使用姿势,主要包括根据字段查询,and/or/in/l ...
- SpringBoot系列教程JPA之delete使用姿势详解
原文: 190702-SpringBoot系列教程JPA之delete使用姿势详解 常见db中的四个操作curd,前面的几篇博文分别介绍了insert,update,接下来我们看下delete的使用姿 ...
- SpringBoot系列教程JPA之update使用姿势
原文: 190623-SpringBoot系列教程JPA之update使用姿势 上面两篇博文拉开了jpa使用姿势的面纱一角,接下来我们继续往下扯,数据插入db之后,并不是说就一层不变了,就好比我在银行 ...
- SpringBoot系列教程JPA之基础环境搭建
JPA(Java Persistence API)Java持久化API,是 Java 持久化的标准规范,Hibernate是持久化规范的技术实现,而Spring Data JPA是在 Hibernat ...
- SpringBoot系列教程起步
本篇学习目标 Spring Boot是什么? 构建Spring Boot应用程序 三分钟开发SpringBoot应用程序 本章源码下载 Spring Boot是什么? spring Boot是由Piv ...
- SpringBoot系列教程web篇之过滤器Filter使用指南
web三大组件之一Filter,可以说是很多小伙伴学习java web时最早接触的知识点了,然而学得早不代表就用得多.基本上,如果不是让你从0到1写一个web应用(或者说即便从0到1写一个web应用) ...
- SpringBoot系列教程web篇之全局异常处理
当我们的后端应用出现异常时,通常会将异常状况包装之后再返回给调用方或者前端,在实际的项目中,不可能对每一个地方都做好异常处理,再优雅的代码也可能抛出异常,那么在 Spring 项目中,可以怎样优雅的处 ...
随机推荐
- RSA-演变过程、原理、特点(加解密及签名)及公钥私钥的生成
本篇是iOS逆向开发总结的第一篇文章,是关于iOS密码学的相关技术分析和总结,希望对大家有所帮助,如果有错误地方欢迎指正. 一.前言 密码学的历史追溯到2000年前,相传古罗马凯撒大帝为了防止敌方截获 ...
- [NOIp2014] luogu P2296 寻找道路
不知道是因为我菜还是别的,最近老是看错题. 题目描述 在有向图 GGG 中,每条边的长度均为 1,现给定起点和终点,请你在图中找一条从起点到终点的路径,该路径满足以下条件: 路径上的所有点的出边所指向 ...
- Ubuntu 16.04 集成安装Apache+PHP+Kerberos+LDAP+phpLDAPadmin
一.安装Apache 1.1.安装Apache apt-get update apt-get install apache2 过程如下: root@duke01:~# apt-get update命中 ...
- java集合之linkedList链表基础
LinkedList链表: List接口的链接列表实现.允许存储所有元素(包含null).使用频繁增删元素. linkedList方法: void addFirst(E e) 指定元素插入列表的开头 ...
- 【Labview入门】将输入度数转换为3位精度弧度值
Labview版本2015 程序如下: 可以右键输出控件选择属性来调整输出的小数位数: 运行结果:
- 02--Java Jshell的使用 最适合入门的Java教程
JShell JShell目标 Java Shell 工具(简称:JShell)是一个用于学习Java编程语言和构建Java代码原型的交互式工具.JShell是一个Read-Evaluate-Prin ...
- eclipse 中配置maven环境
选择 菜单栏 window -preferences-maven- installations 点击add 添加自己的maven 库 配置 setting 第一个为全局配置 第二个为用户配置可以覆盖全 ...
- Activity 学习(二) 搭建第一个Activity流程框架
本次示例使用的IDER测试完成 测试背景 : xx饿了去饭店吃饭 需要先和服务员点餐 点完餐后服务员将菜品传递给厨师制作 制作完成后吃饱 一 :创建流程图 创建上一篇测试成功出现的BpmnFil ...
- django-URL应用命名空间(十)
在多个app下有相同函数时,可能会出现混乱,这时要给每个app取名 基本目录: settings.py INSTALLED_APPS = [ 'django.contrib.admin', 'djan ...
- 如何上传项目至GitHub
1.下载 https://gitforwindows.org/ 2.打开Git Bash 把git绑定到GitHub 3.打开GitHub登陆后 点击settings 点击SSH and GPG ke ...