《quarkus資料庫篇》系列的開篇,編碼實戰最基礎的資料庫增刪改查,資料庫用的是PostgreSQL,在官方demo基礎上進一步精簡,極速入門quarkus資料庫操作 ...
歡迎訪問我的GitHub
這裡分類和彙總了欣宸的全部原創(含配套源碼):https://github.com/zq2599/blog_demos
關於《資料庫篇》
- 《quarkus資料庫篇》系列是《quarkus實戰》的子系列,目標是與大家一起在quarkus框架下完成常用的資料庫操作,如配置、增刪改查、事物等
本篇概覽
-
本篇敢號稱比官方demo更簡單,是因為官方關於操作資料庫的demo中還有web服務的代碼(如接收http請求和響應,以及web庫的依賴),而本篇不會有這些代碼和依賴,只有存粹的資料庫操作和對應的單元測試類,至於web服務?欣宸應該會出《quarkus之web篇》吧(如果時間允許)
-
作為《資料庫篇》的開篇,為了避免長文勸退大多數人的悲劇發生,本文被死死壓制在Hello World級別,咱們用最簡單的配置和代碼完成資料庫的增刪改查操作,掌握quarkus下基本資料庫操作全掌握,然後在後續文章中逐步深入,整體上就是一次從入門到精通之旅
-
本篇的具體內容是創建一個maven工程,此工程有內容是
- 一個單表的實體類
- 實體類對應的service類,提供單表增刪改查的API
- service類對應的單元測試類,一共就這些內容
- 來看看實際的文件和位置,如下圖
![image-20220514115608338](https://img2023.cnblogs.com/blog/485422/202308/485422-20230812100957589-1510630770.png)
- 沒錯,這個工程就這麼簡單,官方demo好歹還做了web介面,可以用postman做增刪改查的測試,在本篇中這些統統砍掉,只有service層及其單元測試類
環境和版本信息
- 電腦:MacBook Pro M1,macOS Monterey
- jdk:11.0.14.1
- maven:3.8.5
- quarkus:與《quarkus實戰》系列保持一致,依舊是2.7.3.Final
- 資料庫:使用PostgreSQL,版本13.3
源碼下載
- 本篇實戰的完整源碼可在GitHub下載到,地址和鏈接信息如下表所示(https://github.com/zq2599/blog_demos)
名稱 | 鏈接 | 備註 |
---|---|---|
項目主頁 | https://github.com/zq2599/blog_demos | 該項目在GitHub上的主頁 |
git倉庫地址(https) | https://github.com/zq2599/blog_demos.git | 該項目源碼的倉庫地址,https協議 |
git倉庫地址(ssh) | [email protected]:zq2599/blog_demos.git | 該項目源碼的倉庫地址,ssh協議 |
- 這個git項目中有多個文件夾,本次實戰的源碼在quarkus-tutorials文件夾下,如下圖紅框
- quarkus-tutorials是個父工程,裡面有多個module,本篇實戰的module是basic-db,如下圖紅框
確認資料庫已就緒
-
請確認PostgreSQL資料庫已經就緒
-
開發階段推薦用docker部署資料庫,簡單省事兒,參考命令如下,請將/xxx換為您自己的宿主機目錄,用於保存資料庫文件
docker run \
--name quarkus_test \
-e POSTGRES_USER=quarkus \
-e POSTGRES_PASSWORD=123456 \
-e POSTGRES_DB=quarkus_test \
-p 5432:5432 \
-v /xxx:/var/lib/postgresql/data \
postgres:13.3
- 需要在PostgreSQL提前創建名為quarkus_test的資料庫,不用建表
- 在開發過程中可能要連上資料庫查看數據,請自行準備客戶端工具(命令行也行),我這裡用的是IDEA自帶的資料庫工具,如下圖,已連上PostgreSQL的quarkus_test資料庫,裡面空空如也
新建maven子工程basic-db
- 在父工程quarkus-tutorials下麵新建名為basic-db的子項目,其pom.xml內容如下,重點是JDBC、hibernate、postgresql這三個和資料庫有關的庫
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>quarkus-tutorials</artifactId>
<groupId>com.bolingcavalry</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>basic-db</artifactId>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<!-- JDBC庫 -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-agroal</artifactId>
</dependency>
<!-- hibernate庫 -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm</artifactId>
</dependency>
<!-- postgresql庫 -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>
<!-- 單元測試庫 -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>${quarkus.platform.group-id}</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<extensions>true</extensions>
<executions>
<execution>
<goals>
<goal>build</goal>
<goal>generate-code</goal>
<goal>generate-code-tests</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
</project>
配置文件
- 本次實戰會用到Hibernate自動重新建表的功能,此功能會先刪除庫中已存在的同名錶,因此,只有一個profile配置的時候,不要讓此應用連接到生產環境
- 最安全的做法是使用profile功能將生產環境和測試環境的配置文件分開,測試環境的配置文件中,是測試資料庫,並且開啟了自動重新建表的的功能,而生產環境的配置文件中,自動重新建表的功能是關閉的
- 先來看公共配置文件application.properties,此文件和profile無關,應用一定會載入,裡面是各個profile都會用到的公共配置,例如資料庫類型
quarkus.datasource.db-kind=postgresql
quarkus.hibernate-orm.log.sql=true
quarkus.datasource.jdbc.max-size=8
quarkus.datasource.jdbc.min-size=2
- 再看application-test.properties,這是當profile等於test時才會用到的配置文件,有兩處要註意的地方稍後會提到
quarkus.datasource.username=quarkus
quarkus.datasource.password=123456
quarkus.datasource.jdbc.url=jdbc:postgresql://192.168.50.43:15432/quarkus_test
quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.sql-load-script=import.sql
- 上述配置,有以下兩處值得重視的配置項
- quarkus.hibernate-orm.database.generation:有六個取值,如下表
取值 | 含義 |
---|---|
none | 啥也不做 |
create | 第一次啟動會建表,之後啟動不會再改動 |
drop-and-create | 每一次啟動應用的時候都刪表(數據也沒了),然後建表,再執行import.sql導入數據 |
drop | 啟動應用的時候刪表,不刪庫 |
update | 保留數據,升級表結構 |
validate | 檢查表結構與entity是否匹配 |
- 從上表可以看出,drop-and-create這個配置很適合開發和測試階段,因為每次都會整理好數據,讓測試和驗證不受歷史數據的影響
- 由於drop-and-create和update會改動資料庫,因此不適合生產環境使用,這一點要牢記,官方也給出了警告
- quarkus.hibernate-orm.sql-load-script:指定sql文件,在配置項quarkus.hibernate-orm.database.generation等於drop-and-create的時候,就執行此sql文件,可以用來生成初始化數據
- 配置完成了,接下來開始寫代碼,從最核心的實體類開始
SQL文件
- 剛纔的配置文件中配合的import.sql,其放置位置與applicatin.properites文件相同,內容如下,可見是往known_fruits表寫入了三條記錄
INSERT INTO known_fruits(id, name) VALUES (1, 'Cherry');
INSERT INTO known_fruits(id, name) VALUES (2, 'Apple');
INSERT INTO known_fruits(id, name) VALUES (3, 'Banana');
- 從前面的配置可知,profile等於test的時候,應用啟動的時候,會根據實體類的信息執行刪表和建表的操作,然後執行import.sql導入三條記錄
編碼:實體類
- 熟悉hibernate的讀者都知道,實體類並非只有get和set方法的Pojo,它包含了大量的JPA元信息,是應用與資料庫表映射的關鍵
- 實體類Fruit.java如下,有幾處要註意的地方稍後會提到
package com.bolingcavalry.db.entity;
import javax.persistence.Cacheable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.NamedQuery;
import javax.persistence.QueryHint;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
@Entity
@Table(name = "known_fruits")
@NamedQuery(name = "Fruits.findAll", query = "SELECT f FROM Fruit f ORDER BY f.name", hints = @QueryHint(name = "org.hibernate.cacheable", value = "true"))
@Cacheable
public class Fruit {
@Id
@SequenceGenerator(name = "fruitsSequence", sequenceName = "known_fruits_id_seq", allocationSize = 1, initialValue = 10)
@GeneratedValue(generator = "fruitsSequence")
private Integer id;
@Column(length = 40, unique = true)
private String name;
public Fruit() {
}
public Fruit(String name) {
this.name = name;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
- 上述代碼有以下幾處要註意的
- 註解Table確定了表名known_fruits
- 增加了一個自定義SQL,名為Fruits.findAll,後面會用到
- 註解SequenceGenerator定義了known_fruits的自增主鍵的信息,初始值是10,也就是說通過當前應用新增的第一條記錄,ID等於10
- known_fruits表只有兩個欄位:id和name
service層
- 為known_fruits表的操作增加一個服務類,用於上層的調用(所謂上層是指web介面、gRPC介面、消息消費入口等)
- 服務類名為FruitService.java,為了省事兒就直接用class,不寫interface了,代碼如下,增刪改查服務其實就是EntityManager的基本操作,這就不贅述了:
package com.bolingcavalry.db.service;
import com.bolingcavalry.db.entity.Fruit;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.transaction.Transactional;
import java.util.List;
@ApplicationScoped
public class FruitService {
@Inject
EntityManager entityManager;
public List<Fruit> get() {
return entityManager.createNamedQuery("Fruits.findAll", Fruit.class)
.getResultList();
}
public Fruit getSingle(Integer id) {
return entityManager.find(Fruit.class, id);
}
@Transactional
public void create(Fruit fruit) {
entityManager.persist(fruit);
}
@Transactional
public void update(Integer id, Fruit fruit) {
Fruit entity = entityManager.find(Fruit.class, id);
if (null!=entity) {
entity.setName(fruit.getName());
}
}
@Transactional
public void delete(Integer id) {
Fruit entity = entityManager.getReference(Fruit.class, id);
if (null!=entity) {
entityManager.remove(entity);
}
}
}
- 代碼寫到這裡其實已經完成了,當前工程已經有了資料庫增刪改查的能力,至於上層如何使用(是web調用、gRPC調用、消費消息),那並非本篇的重點,您可以根據自己需要隨意添加
- 為了驗證服務類功能正常,接下來會寫一個單元測試類 ,調用FruitService的各API並驗證數據是否符合預期
單元測試類
- 單元測試類只有一個,位置在quarkus-tutorials/basic-db/src/test/java,這是符合maven規範的測試類位置
- FruitServiceTest源碼如下,有幾處要註意的地方稍後會提到
package com.bolingcavalry;
import com.bolingcavalry.db.entity.Fruit;
import com.bolingcavalry.db.service.FruitService;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.*;
import javax.inject.Inject;
import java.util.List;
@QuarkusTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class FruitServiceTest {
/**
* import.sql中導入的記錄數量,這些是應用啟動是導入的
*/
private static final int EXIST_RECORDS_SIZE = 3;
/**
* import.sql中,第一條記錄的id
*/
private static final int EXIST_FIRST_ID = 1;
/**
* 在Fruit.java中,id欄位的SequenceGenerator指定了initialValue等於10,
* 表示自增ID從10開始
*/
private static final int ID_SEQUENCE_INIT_VALUE = 10;
@Inject
FruitService fruitService;
@Test
@DisplayName("list")
@Order(1)
public void testGet() {
List<Fruit> list = fruitService.get();
// 判定非空
Assertions.assertNotNull(list);
// import.sql中新增3條記錄
Assertions.assertEquals(EXIST_RECORDS_SIZE, list.size());
}
@Test
@DisplayName("getSingle")
@Order(2)
public void testGetSingle() {
Fruit fruit = fruitService.getSingle(EXIST_FIRST_ID);
// 判定非空
Assertions.assertNotNull(fruit);
// import.sql中的第一條記錄
Assertions.assertEquals("Cherry", fruit.getName());
}
@Test
@DisplayName("update")
@Order(3)
public void testUpdate() {
String newName = "ShanDongBigCherry";
fruitService.update(EXIST_FIRST_ID, new Fruit(newName));
Fruit fruit = fruitService.getSingle(EXIST_FIRST_ID);
// 從資料庫取出的對象,其名稱應該等於修改的名稱
Assertions.assertEquals(newName, fruit.getName());
}
@Test
@DisplayName("create")
@Order(4)
public void testCreate() {
Fruit fruit = new Fruit("Orange");
fruitService.create(fruit);
// 由於是第一次新增,所以ID應該等於自增ID的起始值
Assertions.assertEquals(ID_SEQUENCE_INIT_VALUE, fruit.getId());
// 記錄總數應該等於已有記錄數+1
Assertions.assertEquals(EXIST_RECORDS_SIZE+1, fruitService.get().size());
}
@Test
@DisplayName("delete")
@Order(5)
public void testDelete() {
// 先記刪除前的總數
int numBeforeDelete = fruitService.get().size();
// 刪除第一條記錄
fruitService.delete(EXIST_FIRST_ID);
// 記錄數應該應該等於刪除前的數量減一
Assertions.assertEquals(numBeforeDelete-1, fruitService.get().size());
}
}
- 上述單元測試類有以下幾處要註意
- 一共五個測試方法,為了給它們排序,要用註解TestMethodOrder修飾類,並制定value為MethodOrderer.OrderAnnotation.class
- 再在每個方法上用Order註解修飾,就可以用value執行測試順序了
- 測試方法有點多,為了便於觀察,用註解DisplayName為每個測試方法起了個名字,有了名字,IDEA上的測試結果效果如下
![image-20220504153230297](https://img2023.cnblogs.com/blog/485422/202308/485422-20230812100957530-506642773.png)
- 單元測試代碼寫完了,是不是可以立即開始測試了?別急,還有個小坑,有一定幾率遇到,別看坑小,要是掉進去還有點麻煩...
IDEA的小坑
- 回顧之前的配置,資料庫信息都放在application-test.properties文件中,因此只有profile等於test時,才有資料庫配置信息,其他profile都沒有對應的配置文件
- 一般情況下,如何執行單元測試呢?欣宸的習慣是直接點擊下圖紅框中的按鈕,在彈出的菜單上選擇第一項Run ‘FruitServiceTest’,這樣操作簡單,又能通過IDEA界面觀察測試結果
![image-20220504155008286](https://img2023.cnblogs.com/blog/485422/202308/485422-20230812100957579-646769632.png)
-
實測發現,使用上述方式,IDEA給我們設置的profile可能不是test,而是default,而default這個profile的配置文件是不存在的,因此單元測試啟動就會失敗
-
上述問題,我這邊偶爾遇到過幾次,目前無法穩定復現,針對此問題的解決方法如下
-
點擊圖標運行單元測試的時候,選擇下圖紅框中的選項
- 在彈出的配置視窗中,新增下圖紅框中的內容,這就指定了profile等於test
- 運行的時候,選擇上圖配置的名字FruitServiceTest(test-profile),就能確保profile是test了
運行單元測試
- 運行單元測試,結果如下圖,不但測試全部通過,輸出的日誌內容也非常豐富,解讀他們,是溫習前面知識點的最佳手段
- 還有一處要註意的,就是上圖顯示getSingle方法耗時僅6ms,例外,getSingle執行的時候也沒有SQL日誌輸出,這是因為getSingleb並沒有真正的查詢資料庫,而是使用了前面list的緩存結果,驗證是否使用了緩存很簡單,將testGet和testGetSingle兩個方法的執行順序調換一下,再執行,就發現testGetSingle執行耗時也變長了,而且SQL日誌也出現了
- 上述這種不查資料庫而走本地緩存的操作,雖然看似提升了性能,然而風險也不小,getSingle得到的結果並非資料庫中最新的,關閉緩存的方法如下圖,修改Fruit.java的配置,如下圖
- 至此,相比官方demo更加精簡的quarkus資料庫操作入門已完成,希望本篇能讓咱們對quarkus的資料庫操作能力和流程有基本的認識,為接下來的逐漸深入打好基礎