1. 程式人生 > >SpringBoot系列教程JPA之指定id儲存

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

原文連結: 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. 專案&博文

  • 190612-SpringBoot 系列教程 JPA 之基礎環境搭建
  • 190614-SpringBoot 系列教程 JPA 之新增記錄使用姿勢
  • 190623-SpringBoot 系列教程 JPA 之 update 使用姿勢
  • 190702-SpringBoot 系列教程 JPA 之 delete 使用姿勢詳解
  • 190717-SpringBoot 系列教程 JPA 之 query 使用姿勢詳解之基礎篇

  • 工程:https://github.com/liuyueyi/spring-boot-demo
  • module: https://github.com/liuyueyi/spring-boot-demo/blob/master/spring-boot/102-jpa

1. 一灰灰 Blog

盡信書則不如,以上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現 bug 或者有更好的建議,歡迎批評指正,不吝感激

下面一灰灰的個人部落格,記錄所有學習和工作中的博文,歡迎大家前去逛逛

  • 一灰灰 Blog 個人部落格 https://blog.hhui.top
  • 一灰灰 Blog-Spring 專題部落格 http://spring.hhui.top

相關推薦

SpringBoot系列教程JPA指定id儲存

原文連結: 191119-SpringBoot系列教程JPA之指定id儲存 前幾天有位小夥伴問了一個很有意思的問題,使用 JPA 儲存資料時,即便我指定了主鍵 id,但是新插入的資料主鍵卻是 mysql 自增的 id;那麼是什麼原因導致的呢?又可以如何解決呢? 本文將介紹一下如何使用 JPA 的 AUTO

SpringBoot系列教程JPA新增記錄使用姿勢

SpringBoot系列教程JPA之新增記錄使用姿勢 上一篇文章介紹瞭如何快速的搭建一個JPA的專案環境,並給出了一個簡單的

SpringBoot系列教程JPA基礎環境搭建

JPA(Java Persistence API)Java持久化API,是 Java 持久化的標準規範,Hibernate是持久

SpringBoot系列教程JPAupdate使用姿勢

原文: 190623-SpringBoot系列教程JPA之update使用姿勢 上面兩篇博文拉開了jpa使用姿勢的面紗一角,

SpringBoot系列教程Bean指定初始化順序的若干姿勢

上一篇博文介紹了@Order註解的常見錯誤理解,它並不能指定 bean 的載入順序,那麼問題來了,如果我需要指定 bean 的載入順序,那應該怎麼辦呢? 本文將介紹幾種可行的方式來控制 bean 之間的載入順序 構造方法依賴 @DependOn 註解 BeanPostProcessor 擴充套件 原

Java工程師SpringBoot系列教程前言&目錄

前言 與時俱進是每一個程式設計師都應該有的意識,當一個Java程式設計師在當代步遍佈的時候,你就行該想到我能多學點什麼。可觀的是後端的框架是穩定的,它們能夠維持更久的時間在應用中,而不用擔心技術的更新換代。但是類似SSH,SSM這些框架已經太久了,人們迫不及待地想使用更為優雅而又簡便的框架來代替,所以Spr

SpringBoot系列教程應用篇藉助Redis搭建一個簡單站點統計服務

判斷一個網站值不值錢的一個重要標準就是看pv/uv,那麼你知道pv,uv是怎麼統計的麼?當然現在有第三方做的比較完善的可以直接使用

SpringBoot系列教程web篇Post請求引數解析姿勢彙總

作為一個常年提供各種Http介面的後端而言,如何獲取請求引數可以說是一項基本技能了,本篇為《190824-SpringBoot系列

SpringBoot系列教程Redis叢集環境配置

之前介紹的幾篇redis的博文都是基於單機的redis基礎上進行演示說明的,然而在實際的生產環境中,使用redis叢集的可能性應該

SpringBoot系列教程Bean載入順序錯誤使用姿勢闢謠

在網上查詢 Bean 的載入順序時,看到了大量的文章中使用@Order註解的方式來控制 bean 的載入順序,不知道寫這些的博文的同學自己有沒有實際的驗證過,本文希望通過指出這些錯誤的使用姿勢,讓觀文的小夥伴可以知道@Order的具體的應用場景 原文地址: SpringBoot系列教程之Bean載入順序

SpringBoot系列教程事務傳遞屬性

200202-SpringBoot系列教程之事務傳遞屬性 對於mysql而言,關於事務的主要知識點可能幾種在隔離級別上;在Spring體系中,使用事務的時候,還有一個知識點事務的傳遞屬性同樣重要,本文將主要介紹7中傳遞屬性的使用場景 I. 配置 本文的case,將使用宣告式事務,首先我們建立一個Sp

SpringBoot系列教程起步

本篇學習目標 SpringBoot is What? How use it ? Quick to use 本章原始碼下載 SpringBoot is What? Spring的目標是致力於全方位簡化Java開發 SpringBoot是簡化Java開發 我們來探討一下一個

『React Navigation 3x系列教程React Navigation 3x開發指南

隨著React Navigation逐漸穩定,Navigator也被光榮的退休了。在React Native生態環境中需要一款可擴充套件且易於使用的導航元件,Navigator 自然勝任不了,這時React Native社群便孕育出了一個開源導航元件React Navigation。 React Navig

『React Navigation 3x系列教程createStackNavigator開發指南

createStackNavigator createStackNavigator提供APP螢幕之間切換的能力,它是以棧的形式還管理螢幕之間的切換,新切換到的螢幕會放在棧的頂部。 螢幕轉場風格 預設情況下,createStackNavigator提供了轉場過渡效果,在Android和iOS上過渡效果是不

React Navigation 3x系列教程createStackNavigator開發指南

createStackNavigator createStackNavigator提供APP螢幕之間切換的能力,它是以棧的形式還管理螢幕之間的切換,新切換到的螢幕會放在棧的頂部。 螢幕轉場風格 預設情況下,createStackNavigator提供了轉場過渡效果,在Andr

Linux Shell系列教程(十三)Shell for迴圈

13.1、Shell for迴圈語法 for 變數 in 列表 do command1 command2 ... commandN done **也可以寫成:for var in list; do - 13.1.1、讀取

《VR入門系列教程4---執行平臺

    大多數的VR應用都可以在目前多數的PC和手機上執行,基本上一個不太舊的PC或者配置好點的膝上型電腦都可以正常執行Oculus Rift,如果手機的CPU和顯示卡不錯的話也可以有很好的VR體驗。對於大眾來講,現有的電腦或者手機再加上VR的眼睛就可以很方便的體驗VR,但是對於那種想要體驗極致效果的人來講

前端 | vue.js系列教程4整合Element樣式庫

很多小夥伴在剛開始接觸前端基礎之一的css的時候會苦於它的屬性不正交的特性,阻礙重重。 那麼今天我們試下引入外部ui庫,用別人已經封裝好的樣式,可以讓我們騰出更多時間去”偷懶”。 首先先給大家奉獻技術哥在網上找到的vue開源專案庫,裡面ui庫、基於vue開發

springboot 系列教程一:基礎專案搭建

使用 spring boot 有什麼好處 其實就是簡單、快速、方便!平時如果我們需要搭建一個 spring web 專案

SpringBoot系列JPA的使用

JPA的簡單定義 對於我這樣初次接觸的人來說,jpa就是一種連線資料的api,由於JpaRepository介面提供了很多強大的方法,能很方便的拼接,從而能讓我們對資料庫的操作較為方便的進行操作。 JpaRepository通過各種介面的繼承,從而有了各種CR