前几天有位小伙伴问了一个很有意思的问题,使用JPA保存数据时,即便我指定了主键id,但是新插入的数据主键却是mysql自增的id;那么是什么原因导致的呢?又可以如何解决呢?
本文将介绍一下如何使用JPA的AUTO保存策略来指定数据库主键id
I. 环境准备 实际开始之前,需要先走一些必要的操作,如安装测试使用mysql,创建SpringBoot项目工程,设置好配置信息等,关于搭建项目的详情可以参考前一篇文章 190612-SpringBoot系列教程JPA之基础环境搭建
下面简单的看一下后续的代码中,需要的配置 (我们使用的是mysql数据库)
1. 表准备 沿用前一篇的表,结构如下
1 2 3 4 5 6 7 8 9 10 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. 项目配置 配置信息,与之前有一点点区别,我们新增了更详细的日志打印;本篇主要目标集中在添加记录的使用姿势,对于配置说明,后面单独进行说明
1 2 3 4 5 6 7 8 9 10 11 ## 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,与数据库中的表绑定起来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @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
1 2 3 4 5 6 public interface MoneyCreateRepositoryV2 extends JpaRepository <MoneyPO , Integer > {}
接下来保存数据就很简单了
1 2 3 4 5 6 7 8 9 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
1 2 3 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的记录时,再次执行,查看日志发现实际执行的是更新数据
1 2 3 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,并指定保存策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @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来生成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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利用数据库自增生成主键的策略)
接下来我们再次测试插入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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日志如下
1 2 3 4 5 6 7 8 9 10 11 12 13 # 第一次插入 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或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
打赏
如果觉得我的文章对您有帮助,请随意打赏。
微信打赏
支付宝打赏