原文链接: 191119-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 或者有更好的建议,欢迎批评指正,不吝感激

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

SpringBoot系列教程JPA之指定id保存的更多相关文章

  1. SpringBoot 系列教程 JPA 错误姿势之环境配置问题

    191218-SpringBoot 系列教程 JPA 错误姿势之环境配置问题 又回到 jpa 的教程上了,这一篇源于某个简单的项目需要读写 db,本想着直接使用 jpa 会比较简单,然而悲催的是实际开 ...

  2. SpringBoot系列教程JPA之新增记录使用姿势

    SpringBoot系列教程JPA之新增记录使用姿势 上一篇文章介绍了如何快速的搭建一个JPA的项目环境,并给出了一个简单的演示demo,接下来我们开始业务教程,也就是我们常说的CURD,接下来进入第 ...

  3. SpringBoot系列教程JPA之query使用姿势详解之基础篇

    前面的几篇文章分别介绍了CURD中的增删改,接下来进入最最常见的查询篇,看一下使用jpa进行db的记录查询时,可以怎么玩 本篇将介绍一些基础的查询使用姿势,主要包括根据字段查询,and/or/in/l ...

  4. SpringBoot系列教程JPA之delete使用姿势详解

    原文: 190702-SpringBoot系列教程JPA之delete使用姿势详解 常见db中的四个操作curd,前面的几篇博文分别介绍了insert,update,接下来我们看下delete的使用姿 ...

  5. SpringBoot系列教程JPA之update使用姿势

    原文: 190623-SpringBoot系列教程JPA之update使用姿势 上面两篇博文拉开了jpa使用姿势的面纱一角,接下来我们继续往下扯,数据插入db之后,并不是说就一层不变了,就好比我在银行 ...

  6. SpringBoot系列教程JPA之基础环境搭建

    JPA(Java Persistence API)Java持久化API,是 Java 持久化的标准规范,Hibernate是持久化规范的技术实现,而Spring Data JPA是在 Hibernat ...

  7. SpringBoot系列教程起步

    本篇学习目标 Spring Boot是什么? 构建Spring Boot应用程序 三分钟开发SpringBoot应用程序 本章源码下载 Spring Boot是什么? spring Boot是由Piv ...

  8. SpringBoot系列教程web篇之过滤器Filter使用指南

    web三大组件之一Filter,可以说是很多小伙伴学习java web时最早接触的知识点了,然而学得早不代表就用得多.基本上,如果不是让你从0到1写一个web应用(或者说即便从0到1写一个web应用) ...

  9. SpringBoot系列教程web篇之全局异常处理

    当我们的后端应用出现异常时,通常会将异常状况包装之后再返回给调用方或者前端,在实际的项目中,不可能对每一个地方都做好异常处理,再优雅的代码也可能抛出异常,那么在 Spring 项目中,可以怎样优雅的处 ...

随机推荐

  1. 域渗透-Kerberos协议中spn的应用

    0x01 关于SPN 服务主体名称(SPN)是Kerberos客户端用于唯一标识给特定Kerberos目标计算机的服务实例名称. 服务主体名称是服务实例(可以理解为一个服务,比如 HTTP.MSSQL ...

  2. 提权案例(一)渗透某asp.net网站通过sql server数据库public 提权 思路分享

    先罗列出sql server 角色用户的权限 按照从最低级别角色(bulkadmin)到最高级别角色(sysadmin)的顺序进行描述:1.bulkadmin:这个角色可以运行BULK INSERT语 ...

  3. python中函数定义与调用顺序问题

    def main(): try: mtd(3) except Exception as e: print("程序出现异常:", e) mtd(3) def mtd(a): if a ...

  4. spring cloud alibaba 简介

    ### Spring Cloud Alibaba [官方github地址](https://github.com/alibaba/spring-cloud-alibaba) Spring Cloud ...

  5. oracle计算两个时间的差值(XX天XX时XX分XX秒)

    在工作中需要计算两个时间的差值,结束时间 - 开始时间,又不想在js里写function,也不想在java里去计算,干脆就在数据库做了一个函数来计算两个时间的差值.格式为XX天XX时XX分XX秒: 上 ...

  6. ESP8266开发之旅 进阶篇⑤ 代码规范 —— 像写文章一样优美

    1.前言     之前,一直在跟大伙分享怎么去玩蓝牙模块,怎么去玩wifi模块,怎么去玩json,然后有很多小伙伴就留言各种问题或者说直接怼他的代码过来让我看,然后我就一脸懵逼(代码中到处各种abcd ...

  7. 再不努力提高效率,小姐姐都被人追走了:K8S一键部署了解一下?

    随着互联网时代的不断发展,开发者可能会面临这样的困境:为了解决问题.提升开发效率而竭力研发出来的"创新",似乎削弱了他们在公司的重要程度,甚至取代了他们原先的地位.比如,在云原生时 ...

  8. 非确定的自动机NFA确定化为DFA

    摘要: 在编译系统中,词法分析阶段是整个编译系统的基础.对于单词的识别,有限自动机FA是一种十分有效的工具.有限自动机由其映射f是否为单值而分为确定的有限自动机DFA和非确定的有限自动机NFA.在非确 ...

  9. 如何在vue里引入Bootstrap

    一.引入jquery 步骤: 1. 安装jquery $ npm install jquery --save-dev 2.在webpack.config.js 添加内容 + const webpack ...

  10. 元素“context:component-scan”的前缀“context”未绑定

    首先报这个错误,你得明白,是什么原因导致的? 答:未引入命名空间,和约束文件 解决方法(加上标红色标记): <?xml version="1.0" encoding=&quo ...