上一篇文章介紹了如何快速的搭建一個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已經創建完畢,后續的表中添加記錄也可以直接使用它了,但是還有幾個問題是沒有明確答案的,先提出來,期待后文可以給出回答
- POJO屬性的類型與表中類型
- mysql表中列可以有默認值,這個在POJO中怎么體現
- 一個表包含另一個表的主鍵時(主鍵關聯,外鍵)等特殊的情況,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
那么有辦法解決么?難道就這么向現實放棄,向大佬妥協么?
默認值嘛,一個很容易想到的方法,我直接在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());
}
}
再次執行看下結果如何,順利走下去,沒有報錯,喜大普奔
這樣我就滿足了嗎?要是手抖上面測試注釋掉的那一行忘了注釋,豈不是依然會跪?而且我希望是表中的默認值,直接在代碼中硬編碼會不會不太優雅?這個主動設置的默認值,在后面查詢的時候會不會有坑?
- 作為一個有追求的新青年,當然對上面的答案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
)
c. 類型關聯
針對上面的PO對象,有幾個地方感覺不爽,isDelete我想要boolean,true表示刪除false表示沒刪除,搞一個byte用起來太不方便了,這個要怎么搞?
這個並不怎么復雜,因為直接將byte類型改成boolean就可以了,如果db中時0對應的false;1對應的true,下面是驗證結果,並沒有啥問題
在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. 項目
- 工程: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