前几天有位小伙伴问了一个很有意思的问题,使用 JPA 保存数据时,即便我指定了主键 id,但是新插入的数据主键却是 mysql 自增的 id;那么是什么原因导致的呢?又可以如何解决呢?

本文将介绍一下如何使用 JPA 的 AUTO 保存策略来指定数据库主键 id

I. 环境准备

下面简单的看一下后续的代码中,需要的配置 (我们使用的是 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 教程

首先简单的看一下,我们一般使用默认的数据库自增生成主键的使用方式,以便后面的自定义主键生成策略的对比

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")@CreatedDateprivate Timestamp createAt;@Column(name = "update_at")@CreatedDateprivate 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")@CreatedDateprivate Timestamp createAt;@Column(name = "update_at")@CreatedDateprivate 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 {@Overridepublic 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. 项目&博文

  • 工程:https://github.com/liuyueyi/spring-boot-demo[1]

  • module: https://github.com/liuyueyi/spring-boot-demo/blob/master/spring-boot/102-jpa[2]

1. 一灰灰 Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现 bug 或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

  • 一灰灰 Blog 个人博客 https://blog.hhui.top [3]
  • 一灰灰 Blog-Spring 专题博客 http://spring.hhui.top [4]

一灰灰blog

参考资料

[1]

https://github.com/liuyueyi/spring-boot-demo: https://github.com/liuyueyi/spring-boot-demo

[2]

https://github.com/liuyueyi/spring-boot-demo/blob/master/spring-boot/102-jpa: https://github.com/liuyueyi/spring-boot-demo/blob/master/spring-boot/102-jpa

[3]

https://blog.hhui.top: https://blog.hhui.top

[4]

http://spring.hhui.top: http://spring.hhui.top

SpringBoot系列教程JPA之指定id保存相关推荐

  1. SpringBoot 系列教程(八十五):Spring Boot使用MD5加盐验签Api接口之前后端分离架构设计

    加密算法参考: 浅谈常见的七种加密算法及实现 加密算法参考: 加密算法(DES,AES,RSA,MD5,SHA1,Base64)比较和项目应用 目的: 通过对API接口请求报文签名,后端进行验签处理, ...

  2. Java工程师之SpringBoot系列教程前言目录

    前言 与时俱进是每一个程序员都应该有的意识,当一个Java程序员在当代步遍布的时候,你就行该想到我能多学点什么.可观的是后端的框架是稳定的,它们能够维持更久的时间在应用中,而不用担心技术的更新换代.但 ...

  3. springboot 系列教程十:springboot单元测试

    2019独角兽企业重金招聘Python工程师标准>>> 单元测试 springboot 支持多种方式的单元测试,方便开发者测试代码,首先需要在 pom 文件中添加 starter & ...

  4. springboot 系列教程四:springboot thymeleaf配置

    2019独角兽企业重金招聘Python工程师标准>>> thymeleaf介绍 thymeleaf 是新一代的模板引擎,在spring4.0中推荐使用thymeleaf来做前端模版引 ...

  5. SpringBoot 系列教程(二十三) :使用@Order注解调整配置类加载顺序

    1 .@Order    1.Spring 4.2 利用@Order控制配置类的加载顺序, 2.Spring在加载Bean的时候,有用到order注解.   3.通过@Order指定执行顺序,值越小, ...

  6. SpringBoot 系列教程(十三):SpringBoot集成EasyPoi实现Excel导入导出

    "无意中发现了一个巨牛的人工智能教程,忍不住分享一下给大家.教程不仅是零基础,通俗易懂,而且非常风趣幽默,像看小说一样!觉得太牛了,所以分享给大家.点人工智能教程可以跳转到教程. easyp ...

  7. SpringBoot 系列教程(六十):SpringBoot整合Swagger-Bootstrap-Ui

    SpringBoot2.x整合swagger-bootstrap-ui 一.前言 swagger-bootstrap-ui 是基于swagger接口api实现的一套UI,因swagger原生ui是上下 ...

  8. SpringBoot 系列教程(六十五):Spring Boot整合WxJava开发微信公众号

    一.前言 做微信公众号开发项目以及近两年整了,积累了一点微薄的行业经验,既然开了微信开发专栏博客,那么今天就来回忆回忆,从零开始搭建一个微信公众号开发的框架,可以用于企业级项目开发的脚手架,同时搭配博 ...

  9. SpringBoot 系列教程(九十九):SpringBoot整合阿里云OSS实现文件上传,下载,删除功能

    一.前言 之所以写这篇文章呢? 是因为最近在做文件上传时遇到一个问题,就是我们在以前使用传统Spring+SpringMVC+Mybatis框架开发Web项目的时候,都是将项目打包生成一个War包,然 ...

最新文章

  1. mac下mysql5.7.10密码问题
  2. tracepro应用实例详解_建筑安装工程造价,高清PPT图文详解,小白也能学会的简单步骤...
  3. GCN代码超详解析Two-stream adaptive graph convolutional network for Skeleton-Based Action Recognition(一)
  4. Ubuntu 18.04安装CUDA(版本10.2)和cuDNN
  5. Day 11: AeroGear 推送服务器:使应用的通知推送变得简单
  6. webpack之DefinePlugin使用
  7. var conf=confirm(确定要删除吗?);_微信查看谁删除了4种方法
  8. 基于jsoneditor二次封装一个可实时预览的json编辑器组件(react版)
  9. 命令集matlab,Matlab常用命令集2
  10. Centos下docker/docker-compose离线安装
  11. Spring RMI反序列化漏洞分析
  12. Eureka注册中心配置登录验证
  13. 如何将mysql导出数据泵_Oracle数据库之ORACLE 数据泵导入导出数据
  14. 规范TS项目Any类型的使用
  15. 公司生活备忘录——兼乱弹中国古代思想
  16. 【一、建站综述及步骤简介】2021最详细wordpress博客建站教程(2021.03.01更新)
  17. 有Python基础学习PyTorch,可以选择的书籍有哪些?
  18. 好书推送-《代码不朽:编写可维护软件的10大要则》
  19. JavaScript-合同到期续约案例
  20. 机器学习29:Sklearn库常用分类器及效果比较

热门文章

  1. 百度腾讯双核驱动,小程序的“黄金时代”已来?
  2. windows server 2008R2/2012R2安装
  3. 连接上无线网信号没有网络连接到服务器,无线网络连接上但上不了网怎么办? | 192路由网...
  4. docker镜像文件上传至Docker Hub
  5. 百度由来之众里寻他千百度
  6. 现在不是情绪最低落的时候
  7. c++ 中operater delete和operater new重载
  8. android listview单击,如何在Android中处理ListView单击
  9. VitualBox的那些坑
  10. Day1小白做毕设(跟着青哥学java哔哩哔哩搜程序员青戈)