欢迎访问我的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类对应的单元测试类,一共就这些内容
- 来看看实际的文件和位置,如下图
- 没错,这个工程就这么简单,官方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) | git@github.com: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这三个和数据库有关的库
quarkus-tutorials com.bolingcavalry 1.0-SNAPSHOT 4.0.0 basic-db io.quarkus quarkus-arc io.quarkus quarkus-agroal io.quarkus quarkus-hibernate-orm io.quarkus quarkus-jdbc-postgresql io.quarkus quarkus-junit5 test io.rest-assured rest-assured test ${quarkus.platform.group-id} quarkus-maven-plugin true build generate-code generate-code-tests maven-compiler-plugin -parameters maven-surefire-plugin org.jboss.logmanager.LogManager ${maven.home}
配置文件
- 本次实战会用到Hibernate自动重新建表的功能,此功能会先删除库中已存在的同名表,因此,只有一个profile配置的时候,不要让此应用连接到生产环境
- 最安全的做法是使用profile功能将生产环境和测试环境的配置文件分开,测试环境的配置文件中,是测试数据库,并且开启了自动重新建表的的功能,而生产环境的配置文件中,自动重新建表的功能是关闭的
- 先来看公共配置文件application.properties,此文件和profile无关,应用一定会加载,里面是各个profile都会用到的公共配置,例如数据库类型
quarkus.datasource.db-kind=postgresqlquarkus.hibernate-orm.log.sql=truequarkus.datasource.jdbc.max-size=8quarkus.datasource.jdbc.min-size=2
- 再看application-test.properties,这是当profile等于test时才会用到的配置文件,有两处要注意的地方稍后会提到
quarkus.datasource.username=quarkusquarkus.datasource.password=123456quarkus.datasource.jdbc.url=jdbc:postgresql://192.168.50.43:15432/quarkus_testquarkus.hibernate-orm.database.generation=drop-and-createquarkus.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"))@Cacheablepublic 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;@ApplicationScopedpublic class FruitService { @Inject EntityManager entityManager; public List 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 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上的测试结果效果如下
- 单元测试代码写完了,是不是可以立即开始测试了?别急,还有个小坑,有一定几率遇到,别看坑小,要是掉进去还有点麻烦…
IDEA的小坑
- 回顾之前的配置,数据库信息都放在application-test.properties文件中,因此只有profile等于test时,才有数据库配置信息,其他profile都没有对应的配置文件
- 一般情况下,如何执行单元测试呢?欣宸的习惯是直接点击下图红框中的按钮,在弹出的菜单上选择第一项Run ‘FruitServiceTest’,这样操作简单,又能通过IDEA界面观察测试结果
实测发现,使用上述方式,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的数据库操作能力和流程有基本的认识,为接下来的逐渐深入打好基础
欢迎关注博客园:程序员欣宸
学习路上,你不孤单,欣宸原创一路相伴…