原文连接: 191119-SpringBoot系列教程JPA之指定id保存java
前几天有位小伙伴问了一个颇有意思的问题,使用 JPA 保存数据时,即使我指定了主键 id,可是新插入的数据主键倒是 mysql 自增的 id;那么是什么缘由致使的呢?又能够如何解决呢?mysql
本文将介绍一下如何使用 JPA 的 AUTO 保存策略来指定数据库主键 idgit
实际开始以前,须要先走一些必要的操做,如安装测试使用 mysql,建立 SpringBoot 项目工程,设置好配置信息等,关于搭建项目的详情能够参考前一篇文章 190612-SpringBoot 系列教程 JPA 之基础环境搭建github
下面简单的看一下后续的代码中,须要的配置 (咱们使用的是 mysql 数据库)spring
沿用前一篇的表,结构以下sql
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;
配置信息,与以前有一点点区别,咱们新增了更详细的日志打印;本篇主要目标集中在添加记录的使用姿式,对于配置说明,后面单独进行说明数据库
## 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
首先简单的看一下,咱们通常使用默认的数据库自增生成主键的使用方式,以便后面的自定义主键生成策略的对比dom
对于 jpa 的插入数据的知识点不太清楚的同窗,能够看一下以前的博文: 190614-SpringBoot 系列教程 JPA 之新增记录使用姿式ide
首先咱们须要定义 PO,与数据库中的表绑定起来spring-boot
@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 的执行过程逻辑如
那么问题来了,若是我但愿当个人 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)
注意上面的日志输出
module: https://github.com/liuyueyi/spring-boot-demo/blob/master/spring-boot/102-jpa
尽信书则不如,以上内容,纯属一家之言,因我的能力有限,不免有疏漏和错误之处,如发现 bug 或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的我的博客,记录全部学习和工做中的博文,欢迎你们前去逛逛