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

原文鏈接: 191119-SpringBoot系列教程JPA之指定id保存

前幾天有位小伙伴問了一個很有意思的問題,使用 JPA 保存數(shù)據(jù)時林束,即便我指定了主鍵 id,但是新插入的數(shù)據(jù)主鍵卻是 mysql 自增的 id;那么是什么原因?qū)е碌哪兀坑挚梢匀绾谓鉀Q呢?

本文將介紹一下如何使用 JPA 的 AUTO 保存策略來指定數(shù)據(jù)庫主鍵 id

I. 環(huán)境準(zhǔn)備

實際開始之前姨俩,需要先走一些必要的操作,如安裝測試使用 mysql师郑,創(chuàng)建 SpringBoot 項目工程环葵,設(shè)置好配置信息等,關(guān)于搭建項目的詳情可以參考前一篇文章 190612-SpringBoot 系列教程 JPA 之基礎(chǔ)環(huán)境搭建

下面簡單的看一下后續(xù)的代碼中呕乎,需要的配置 (我們使用的是 mysql 數(shù)據(jù)庫)

1. 表準(zhǔn)備

沿用前一篇的表积担,結(jié)構(gòu)如下

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 '創(chuàng)建時間',
  `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. 項目配置

配置信息陨晶,與之前有一點點區(qū)別猬仁,我們新增了更詳細(xì)的日志打印先誉;本篇主要目標(biāo)集中在添加記錄的使用姿勢湿刽,對于配置說明,后面單獨進(jìn)行說明

## 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相關(guān)配置
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 教程

首先簡單的看一下褐耳,我們一般使用默認(rèn)的數(shù)據(jù)庫自增生成主鍵的使用方式诈闺,以便后面的自定義主鍵生成策略的對比

對于 jpa 的插入數(shù)據(jù)的知識點不太清楚的同學(xué),可以看一下之前的博文: 190614-SpringBoot 系列教程 JPA 之新增記錄使用姿勢

1. 自增主鍵

首先我們需要定義 PO铃芦,與數(shù)據(jù)庫中的表綁定起來

@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 的使用就是利用數(shù)據(jù)庫的自增來生成主鍵 id

/**
 * 新增數(shù)據(jù)
 * Created by @author yihui in 11:00 19/6/12.
 */
public interface MoneyCreateRepositoryV2 extends JpaRepository<MoneyPO, Integer> {
}

接下來保存數(shù)據(jù)就很簡單了

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);
}

強(qiáng)烈建議實際的體驗一下上面的代碼執(zhí)行

首次執(zhí)行確保數(shù)據(jù)庫中不存在 id 為 20 的記錄,雖然我們的 PO 對象中刃滓,指定了 id 為 20仁烹,但是執(zhí)行完畢之后,新增的數(shù)據(jù) 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)

上面是執(zhí)行的 sql 日志咧虎,注意插入的 sql卓缰,是沒有指定 id 的,所以新增的記錄的 id 就會利用 mysql 的自增策略

當(dāng)我們的 db 中存在 id 為 20 的記錄時,再次執(zhí)行征唬,查看日志發(fā)現(xiàn)實際執(zhí)行的是更新數(shù)據(jù)

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 的執(zhí)行過程邏輯如

  • 首先根據(jù) id 到數(shù)據(jù)庫中查詢對應(yīng)的數(shù)據(jù)
  • 如果數(shù)據(jù)不存在,則新增(插入 sql 不指定 id)
  • 如果數(shù)據(jù)存在总寒,則判斷是否有變更扶歪,以確定是否需要更新

2. 指定 id

那么問題來了,如果我希望當(dāng)我的 po 中指定了數(shù)據(jù)庫 id 時摄闸,db 中沒有這條記錄時击罪,就插入 id 為指定值的記錄;如果存在記錄贪薪,則更新

要實現(xiàn)上面這個功能媳禁,自定義主鍵 id,那么我們就需要修改一下主鍵的生成策略了画切,官方提供了四種

取值 說明
GenerationType.TABLE 使用一個特定的數(shù)據(jù)庫表格來保存主鍵
GenerationType.SEQUENCE 根據(jù)底層數(shù)據(jù)庫的序列來生成主鍵竣稽,條件是數(shù)據(jù)庫支持序列
GenerationType.IDENTITY 主鍵由數(shù)據(jù)庫自動生成(主要是自動增長型)
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")這個需要有岛宦,否則執(zhí)行會拋異常

這一行代碼的意思是,主鍵 id 是由ManulInsertGenerator來生成

/**
 *  自定義的主鍵生成策略耍缴,如果填寫了主鍵id砾肺,如果數(shù)據(jù)庫中沒有這條記錄,則新增指定id的記錄防嗡;否則更新記錄
 *
 *  如果不填寫主鍵id变汪,則利用數(shù)據(jù)庫本身的自增策略指定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利用數(shù)據(jù)庫自增生成主鍵的策略)

接下來我們再次測試插入

// 使用自定義的主鍵生成策略
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);

上面的代碼執(zhí)行時番官,確保數(shù)據(jù)庫中沒有主鍵為 20 的數(shù)據(jù),輸出 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 的钢属,也就達(dá)到了我們指定 id 新增數(shù)據(jù)的要求
  • 第二次插入時徘熔,因為 id=20 的記錄存在,所以執(zhí)行的是更新操作
  • 第三次插入時署咽,因為沒有 id近顷,所以插入的 sql 中也沒有指定 id生音,使用 mysql 的自增來生成主鍵 id

II. 其他

0. 項目&博文

1. 一灰灰 Blog

盡信書則不如,以上內(nèi)容窒升,純屬一家之言缀遍,因個人能力有限,難免有疏漏和錯誤之處饱须,如發(fā)現(xiàn) bug 或者有更好的建議域醇,歡迎批評指正,不吝感激

下面一灰灰的個人博客蓉媳,記錄所有學(xué)習(xí)和工作中的博文譬挚,歡迎大家前去逛逛

一灰灰blog
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市酪呻,隨后出現(xiàn)的幾起案子减宣,更是在濱河造成了極大的恐慌,老刑警劉巖玩荠,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件漆腌,死亡現(xiàn)場離奇詭異,居然都是意外死亡阶冈,警方通過查閱死者的電腦和手機(jī)闷尿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來女坑,“玉大人填具,你說我怎么就攤上這事〈移” “怎么了劳景?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長绰筛。 經(jīng)常有香客問我枢泰,道長描融,這世上最難降的妖魔是什么铝噩? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮窿克,結(jié)果婚禮上骏庸,老公的妹妹穿的比我還像新娘。我一直安慰自己年叮,他們只是感情好具被,可當(dāng)我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著只损,像睡著了一般一姿。 火紅的嫁衣襯著肌膚如雪七咧。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天叮叹,我揣著相機(jī)與錄音艾栋,去河邊找鬼。 笑死蛉顽,一個胖子當(dāng)著我的面吹牛蝗砾,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播携冤,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼悼粮,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了曾棕?” 一聲冷哼從身側(cè)響起扣猫,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎翘地,沒想到半個月后苞笨,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡子眶,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年瀑凝,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片臭杰。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡粤咪,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出渴杆,到底是詐尸還是另有隱情寥枝,我是刑警寧澤,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布磁奖,位于F島的核電站囊拜,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏比搭。R本人自食惡果不足惜冠跷,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望身诺。 院中可真熱鬧蜜托,春花似錦、人聲如沸霉赡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽穴亏。三九已至蜂挪,卻和暖如春重挑,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背棠涮。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工攒驰, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人故爵。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓玻粪,卻偏偏與公主長得像,于是被迫代替她去往敵國和親诬垂。 傳聞我的和親對象是個殘疾皇子劲室,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,611評論 2 353

推薦閱讀更多精彩內(nèi)容