SpringBoot系列教程JPA之指定id保存

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: 前几天有位小伙伴问了一个很有意思的问题,使用 JPA 保存数据时,即便我指定了主键 id,但是新插入的数据主键却是 mysql 自增的 id;那么是什么原因导致的呢?又可以如何解决呢?本文将介绍一下如何使用 JPA 的 AUTO 保存策略来指定数据库主键 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



相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
6月前
|
Java
Springboot+JPA+Sqlite整合demo
Springboot+JPA+Sqlite整合demo
234 0
|
1月前
|
SQL Java 关系型数据库
Springboot引入jpa来管理数据库
Springboot引入jpa来管理数据库
33 0
Springboot引入jpa来管理数据库
|
5月前
|
druid Java 关系型数据库
Spring Boot2 系列教程(二十五)Spring Boot 整合 Jpa 多数据源
Spring Boot2 系列教程(二十五)Spring Boot 整合 Jpa 多数据源
|
1月前
|
SQL Java 数据库连接
springBoot+Jpa(hibernate)数据库基本操作
springBoot+Jpa(hibernate)数据库基本操作
39 0
|
2月前
|
Java 数据库连接 API
【Java笔记+踩坑】Spring Data JPA
从常用注解、实体类和各层编写方法入手,详细介绍JPA框架在增删改查等方面的基本用法,以及填充用户名日期、分页查询等高级用法。
【Java笔记+踩坑】Spring Data JPA
|
3月前
|
安全 Java 数据安全/隐私保护
基于SpringBoot+Spring Security+Jpa的校园图书管理系统
本文介绍了一个基于SpringBoot、Spring Security和JPA开发的校园图书管理系统,包括系统的核心控制器`LoginController`的代码实现,该控制器处理用户登录、注销、密码更新、角色管理等功能,并提供了系统初始化测试数据的方法。
58 0
基于SpringBoot+Spring Security+Jpa的校园图书管理系统
|
3月前
|
Java 关系型数据库 MySQL
|
3月前
|
Java Spring 数据库
怎样动动手指就能实现数据操作?Spring Data JPA背后的魔法揭秘
【8月更文挑战第31天】在Java开发中,数据库交互至关重要。传统的JDBC操作繁琐且难维护,而Spring Data JPA作为集成JPA的数据访问层解决方案,提供了CRUD等通用操作接口,显著减少代码量。通过继承`JpaRepository`,开发者能轻松实现数据的增删改查,甚至复杂查询和分页也不再困难。本文将通过示例详细介绍如何利用Spring Data JPA简化数据访问层的开发,提升代码质量和可维护性。
41 0
|
3月前
|
存储 Java 数据库
|
3月前
|
存储 Java API