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


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

上一篇文章介紹了如何快速的搭建一個JPA的項目環境,並給出了一個簡單的演示demo,接下來我們開始業務教程,也就是我們常說的CURD,接下來進入第一篇,如何添加數據

通過本篇文章,你可以get到以下技能點

  • POJO對象如何與表關聯
  • 如何向DB中添加單條記錄
  • 如何批量向DB中添加記錄
  • save 與 saveAndFlush的區別

I. 環境准備

實際開始之前,需要先走一些必要的操作,如安裝測試使用mysql,創建SpringBoot項目工程,設置好配置信息等,關於搭建項目的詳情可以參考前一篇文章 190612-SpringBoot系列教程JPA之基礎環境搭建

下面簡單的看一下演示添加記錄的過程中,需要的配置

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,對JPA的原則和hibernate的一些特性了解的也不多,目前處於學習探索階段,主要是介紹下使用姿勢,下面的東西都是經過測試得出,有些地方描述可能與規范不太一樣,或者有些差錯,請發現的大佬指正

接下來我們進入正題,如何通過JPA實現我們常見的Insert功能

1. POJO與表關聯

首先第一步就是將POJO對象與表關聯起來,這樣就可以直接通過java的操作方式來實現數據庫的操作了;

我們直接創建一個MoneyPo對象,包含上面表中的幾個字段

@Data
public class MoneyPO {
    private Integer id;

    private String name;

    private Long money;

    private Byte isDeleted;

    private Timestamp createAt;

    private Timestamp updateAt;
}

自然而然地,我們就有幾個問題了

  • 這個POJO怎么告訴框架它是和表Money綁定的呢?
  • Java中變量命令推薦駝峰結構,那么 isDeleted 又如何與表中的 is_deleted 關聯呢?
  • POJO中成員變量的類型如何與表中的保持一致呢,如果不一致會怎樣呢?

針對上面的問題,一個一個來說明

對hibernate熟悉的同學,可能知道我可以通過xml配置的方式,來關聯POJO與數據庫表(當然mybatis也是這么玩的),友情鏈接一下hibernate的官方說明教程;我們使用SpringBoot,當然是選擇注解的方式了,下面是通過注解的方式改造之后的DO對象

package com.git.hui.boot.jpa.entity;

import lombok.Data;
import org.springframework.data.annotation.CreatedDate;

import javax.persistence.*;
import java.sql.Timestamp;

/**
 * Created by @author yihui in 21:01 19/6/10.
 */
@Data
@Entity(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;
}

有幾個有意思的地方,需要我們注意

a. entity注解

@Entity 這個注解比較重要,用於聲明這個POJO是一個與數據庫中叫做 money 的表關聯的對象;

  • @Entity注解有一個參數name,用於指定表名,如果不主動指定時,默認用類名,即上面如果不指定那么,那么默認與表 moneypo 綁定

另外一個常見的方式是在類上添加注解 @Table,然后指定表名,也是可以的

@Data
@Entity
@Table(name = "money")
public class MoneyPO {
}

b. 主鍵指定

我們可以看到id上面有三個注解,我們先看下前面兩個

  • @Id 顧名思義,用來表明這家伙是主鍵,比較重要,需要特殊關照
  • @GeneratedValue 設置初始值,談到主鍵,我們一般會和”自增“這個一起說,所以你經常會看到的取值為 strategy = GenerationType.IDENTITY (由數據庫自動生成)

這個注解主要提供了四種方式,分別說明如下

取值 說明
GenerationType.TABLE 使用一個特定的數據庫表格來保存主鍵
GenerationType.SEQUENCE 根據底層數據庫的序列來生成主鍵,條件是數據庫支持序列
GenerationType.IDENTITY 主鍵由數據庫自動生成(主要是自動增長型)
GenerationType.AUTO 主鍵由程序控制

關於這幾種使用姿勢,這里不詳細展開了,有興趣的可以可以看一下這博文: @GeneratedValue

c. Column注解

這個注解就是用來解決我們pojo成員名和數據庫列名不一致的問題的,這個注解內部的屬性也不少,相對容易理解,后面會單開一章來記錄這些常用注解的說明查閱

d. CreateDate注解

這個注解和前面不一樣的是它並非來自jpa-api包,而是spring-data-common包中提供的,表示會根據當前時間創建一個時間戳對象

e. 其他

到這里這個POJO已經創建完畢,后續的表中添加記錄也可以直接使用它了,但是還有幾個問題是沒有明確答案的,先提出來,期待后文可以給出回答

  1. POJO屬性的類型與表中類型
  2. mysql表中列可以有默認值,這個在POJO中怎么體現
  3. 一個表包含另一個表的主鍵時(主鍵關聯,外鍵)等特殊的情況,POJO中有體現么?

2. Repository API聲明

jpa非常有意思的一點就是你只需要創建一個接口就可以實現db操作,就這么神奇,可惜本文里面見不到太多神奇的用法,這塊放在查詢篇來見證奇跡

我們定義的API需要繼承自org.springframework.data.repository.CrudRepository,如下

package com.git.hui.boot.jpa.repository;

import com.git.hui.boot.jpa.entity.MoneyPO;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.CrudRepository;

/**
 * 新增數據
 * Created by @author yihui in 11:00 19/6/12.
 */
public interface MoneyCreateRepository extends CrudRepository<MoneyPO, Integer> {
}

好的,到這里就可以直接添加數據了 (感覺什么都沒干,你居然告訴我可以插入數據???)

3. 使用姿勢

a. 基礎使用case

常規的使用姿勢,無非單個插入和批量插入,我們先來看一下常規操作

@Component
public class JpaInsertDemo {
    @Autowired
    private MoneyCreateRepository moneyCreateRepository;
    public void testInsert() {
        addOne();
        addMutl();
    }

    private void addOne() {
        // 單個添加
        MoneyPO moneyPO = new MoneyPO();
        moneyPO.setName("jpa 一灰灰");
        moneyPO.setMoney(1000L);
        moneyPO.setIsDeleted((byte) 0x00);
        Timestamp now = new Timestamp(System.currentTimeMillis());
        moneyPO.setCreateAt(now);
        moneyPO.setUpdateAt(now);

        MoneyPO res = moneyCreateRepository.save(moneyPO);
        System.out.println("after insert res: " + res);
    }

    private void addMutl() {
        // 批量添加
        MoneyPO moneyPO = new MoneyPO();
        moneyPO.setName("batch jpa 一灰灰");
        moneyPO.setMoney(1000L);
        moneyPO.setIsDeleted((byte) 0x00);
        Timestamp now = new Timestamp(System.currentTimeMillis());
        moneyPO.setCreateAt(now);
        moneyPO.setUpdateAt(now);

        MoneyPO moneyPO2 = new MoneyPO();
        moneyPO2.setName("batch jpa 一灰灰");
        moneyPO2.setMoney(1000L);
        moneyPO2.setIsDeleted((byte) 0x00);
        moneyPO2.setCreateAt(now);
        moneyPO2.setUpdateAt(now);

        Iterable<MoneyPO> res = moneyCreateRepository.saveAll(Arrays.asList(moneyPO, moneyPO2));
        System.out.println("after batchAdd res: " + res);
    }
}

看下上面的兩個插入方式,就這么簡單,

  • 通過IoC/DI注入 repository
  • 創建PO對象,然后調用save, saveAll方法就ok了

上面是一般的使用姿勢,那么非一般使用姿勢呢?

b. 插入時默認值支持方式

在創建表的時候,我們知道字段都有默認值,那么如果PO對象中某個成員我不傳,可以插入成功么?會是默認的DB值么?

private void addWithNull() {
    // 單個添加
    try {
        MoneyPO moneyPO = new MoneyPO();
        moneyPO.setName("jpa 一灰灰 ex");
        moneyPO.setMoney(2000L);
        moneyPO.setIsDeleted(null);
        MoneyPO res = moneyCreateRepository.save(moneyPO);
        System.out.println("after insert res: " + res);
    } catch (Exception e) {
        System.out.println("addWithNull field: " + e.getMessage());
    }
}

當看到上面的try/catch可能就有預感,上面的執行多半要跪(😏😏😏),下面是執行截圖,也是明確告訴了我們這個不能為null

0

那么有辦法解決么?難道就這么向現實放棄,向大佬妥協么?

默認值嘛,一個很容易想到的方法,我直接在PO對象中給一個默認值,是不是也可以,然后我們的PO改造為

@Data
@Entity
@Table(name = "money")
public class MoneyPO {
    // ... 省略其他
    
    @Column(name = "is_deleted")
    private Byte isDeleted = (byte) 0x00;
}

測試代碼注釋一行,變成下面這個

private void addWithNull() {
    // 單個添加
    try {
        MoneyPO moneyPO = new MoneyPO();
        moneyPO.setName("jpa 一灰灰 ex");
        moneyPO.setMoney(2000L);
//            moneyPO.setIsDeleted(null);
        MoneyPO res = moneyCreateRepository.save(moneyPO);
        System.out.println("after insert res: " + res);
    } catch (Exception e) {
        System.out.println("addWithNull field: " + e.getMessage());
    }
}

再次執行看下結果如何,順利走下去,沒有報錯,喜大普奔

1

這樣我就滿足了嗎?要是手抖上面測試注釋掉的那一行忘了注釋,豈不是依然會跪?而且我希望是表中的默認值,直接在代碼中硬編碼會不會不太優雅?這個主動設置的默認值,在后面查詢的時候會不會有坑?

  • 作為一個有追求的新青年,當然對上面的答案say no了

我們的解決方法也簡單,在PO類上,加一個注解 @DynamicInsert,表示在最終創建sql的時候,為null的項就不要了哈

然后我們的新的PO,在原始版本上變成如下(注意干掉上一次的默認值)

@Data
@DynamicInsert
@Entity
@Table(name = "money")
public class MoneyPO {
  // ... 省略
}

再來一波實際的測試,完美了,沒有拋異常,插入成功,而且控制台中輸出的sql日志也驗證了我們上面說的@DynamicInsert注解的作用(日志輸出hibernate的sql,可以通過配置application.properties文件,添加參數spring.jpa.show-sql=true

2

c. 類型關聯

針對上面的PO對象,有幾個地方感覺不爽,isDelete我想要boolean,true表示刪除false表示沒刪除,搞一個byte用起來太不方便了,這個要怎么搞?

這個並不怎么復雜,因為直接將byte類型改成boolean就可以了,如果db中時0對應的false;1對應的true,下面是驗證結果,並沒有啥問題

3

在JPA規范中,並不是所有的類型的屬性都可以持久化的,下表列舉了可映射為持久化的屬性類型:

分類 類型
基本類型 byte、int、short、long、boolean、char、float、double
基本類型封裝類 Byte、Integer、Short、Long、Boolean、Character、Float、Double
字節和字符數組 byte[]、Byte[]、char[]、Character[]
大數值類型 BigInteger、BigDecimal
字符串類型 String
時間日期類 java.util.Date、java.util.Calendar、java.sql.Date、java.sql.Time、java.sql.Timestamp
集合類 java.util.Collection、java.util.List、java.util.Set、java.util.Map
枚舉類型
嵌入式

關於類型關聯,在查詢這一篇會更詳細的進行展開說明,比如有個特別有意思的點

如db中is_delete為1,需要映射到PO中的false,0映射到true,和我們上面默認的是個反的,要怎么搞?

d. 插入時指定ID

再插入的時候,我們上面的case都是沒有指定id的,但是如果你指定了id,會發生什么事情?

我們將po恢復到之前的狀態,測試代碼如下

private void addWithId() {
    // 單個添加
    MoneyPO moneyPO = new MoneyPO();
    moneyPO.setId(20);
    moneyPO.setName("jpa 一灰灰 ex");
    moneyPO.setMoney(2200L + ((long) (Math.random() * 100)));
    moneyPO.setIsDeleted((byte) 0x00);
    MoneyPO res = moneyCreateRepository.save(moneyPO);
    System.out.println("after insert res: " + res);
}

看下輸出結果,驚訝的發現,這個指定id並沒有什么卵用,最終db中插入的記錄依然是自增的方式來的

為什么會這樣子呢,我們看下sql是怎樣的

直接把id給丟了,也就是說我們設置的id不生效,我們知道@GeneratedValue 這個注解指定了id的增長方式,如果我們去掉這個注解會怎樣

從輸出結果來看:

  • 如果這個id對應的記錄不存在,則新增
  • 如果這個id對應的記錄存在,則更新

不然這個注解可以主動指定id方式進行插入or修改,那么如果沒有這個注解,插入時也不指定id,會怎樣呢?

很遺憾的是直接拋異常了,沒有這個注解,就必須手動賦值id了

4. 小結

本文主要介紹了下如何使用JPA來實現插入數據,單個or批量插入,也拋出了一些問題,有的給出了回答,有的等待后文繼續跟進,下面簡單小結一下主要的知識點

  • POJO與表關聯方式
    • 注意幾個注解的使用
    • @Entity, @Table 用於指定這個POJO對應哪張表
    • @Column 用於POJO的成員變量與表中的列進行關聯
    • @Id @GeneratedValue來指定主鍵
    • POJO成員變量類型與DB表中列的關系
  • db插入的幾種姿勢
    • save 單個插入
    • saveAll 批量插入
    • 插入時,如要求DO中成員為null時,用mysql默認值,可以使用注解 @DynamicInsert,實現最終拼接部分sql方式插入
    • 指定id查詢時的幾種case

此外本文還留了幾個坑沒有填

  • POJO成員類型與表列類型更靈活的轉換怎么玩?
  • save 與 saveAndFlush 之間的區別(從命名上,前者保存,可能只保存內存,不一定落庫;后者保存並落庫,但是沒有找到驗證他們區別的實例代碼,所以先不予評價)
  • 注解的更詳細使用說明

II. 其他

-1. 相關博文

0. 項目

1. 一灰灰Blog

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

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

一灰灰blog


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM