Spring Data JPA 多条件连表查询 (2022 更新)

痛点 项目中使用 Spring Data JPA 作为 ORM 框架的时候,实体映射非常方便。Spring Data Repository 的顶层抽象完全解决单实体的查询,面对单实体的复杂查询,也能使用 JpaSpecificationExecutor<T> 构造 Specification<T> 轻松应对。 而对于后台管理报表查询需求来说,需要进行连表多条件动态查询的时候,就显得无从下手。因为它并不像 MyBatis 一样能够在 XML 文件中写出动态 SQL 语句。 尽管可以使用 EntityManager 动态拼接原生 SQL 语句,但是该方法返回值为 ResultSet ,也就是说查出来的实体映射关系需要手动映射(😢这样不太优雅,已经定义出实体,还需要自己去映射)。 所以,本文的目的是,在现有实体关系的基础上,结合 Specification<T> 记录下 Spring Data JPA 多条件动态连表查询操作,以及其中的踩坑和优化。 想要直接看结论的,请看这篇 Spring Data JPA 动态多条件连表查询最佳实践。 基础操作 那么,让我们开始进入代码操作。【本文所有代码在此】 前置说明 相关依赖 Java 11 SpringBoot 2.4.2 build.gradle plugins { id 'org.springframework.boot' version '2.4.2' id 'io.spring.dependency-management' version '1.0.11.RELEASE' id 'java' } dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' compileOnly 'org.projectlombok:lombok' runtimeOnly 'mysql:mysql-connector-java' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'com.h2database:h2' } maven.xml ...

九月 24, 2022 · 8 分钟 · Lex Cao

通过构建全栈待办应用学习 Rust

什么 我想写一篇关于我如何学习Rust的博客。 请原谅我在 2022 年才开始学习这个伟大的编程语言。 为了在实践中学习Rust,我构建了一个全栈待办应用。 你可以在 这里 尝试。相关源码可以在 GitHub 上找到。 现在,我准备写一下它是如何构建的。 如何 首先,和大家一样,我也是从 The Book 中学习 Rust。这是一本不应该跳过入门学习 Rust 的好书。 在学习了一些基本的语法之后,我尝试从零开始使用 Rust 构建一个全栈应用,这是一个使用 Rust tokio 的后端服务和一个使用 Rust WASM(Web Assembly)的前端页面。 我将分别对这两部分做一个简单的介绍。 后端 后台服务是一个的简单的 REST API。使用 actix-web 作为网络框架。 我将写另一篇博客,介绍我如何使用 TDD 开发 Rust 后台服务。 被 Rust 编译器和 Borrow Checker 教育是一次特别的体验。 对于部署,我使用 Supabase 作为 Postgres 服务,使用 Railway 来运行后台服务 docker 镜像。 前端 前端页面有在线和离线数据源,在线是从后台服务器获取数据,而离线是在本地存储。并且有一个按钮来切换它们。 前台由 Rust WASM 和 yew 框架驱动,这是一个类似 React 基于组件构建 Web 应用框架。 如果你熟悉 JSX,你可以在使用 Yew 时感到很自在。 在 Rust 中编写类似 React 的代码体验良好,而且真的很有趣。 但有一些不同之处我想与大家分享,晚点会写一篇博客来谈这个问题,所以敬请关注。 部署的话,是放在 Vercel 上进行托管,用 GitHub Action 来实现自动部署。 ...

五月 2, 2022 · 1 分钟 · Lex Cao

Kotlin/Java TDD 开发流程记录

通过使用 Kotlin / Java 中 Junit5 和 Mockito 测试框架,在预约功能中演示 TDD 开发流程。 TDD 介绍 TDD(Test-Driven Development) 是一种开发流程,中文是「测试驱动开发」。用一句白话形容,就是「先写测试再开发」。先写测试除了能确保测试程式的撰写,还有一个好处:有助于在开发初期厘清程式介面如何设计。详细理论知识可以前往 Wiki 了解,这里不再过多介绍。 测试驱动开发 Test Driven Development TDD 开发流程(5步) 术语说明: 红灯 - Failure - 测试用例失败 绿灯 - Success - 测试用例成功 重构 - Refactor - 重构功能代码 具体步骤: 选定一个功能,编写测试用例 执行测试,得到【红灯】 编写满足测试用例的功能代码 再次执行,得到【绿灯】 【重构】代码 小结: 对于每一个功能,在【红灯】-【绿灯】-【重构】间来回循环往复,不断得到完善。 前置工作 代码说明 使用 Kotlin 语言(会有相对应的 Java 代码) 使用到的测试框架 Running: JUnit5 Mock: MockK / Mockito Assertion: Kotest / AssertJ 只涉及 TDD 的具体流程,不涉及单元测试如何编写(可以看 SpringBoot 单元测试各层) 功能介绍 假设一个用户预约的场景。 ...

五月 3, 2021 · 2 分钟 · Lex Cao

Kotlin 奇怪的相等现象探究

最近遇到一个平时没怎么关注的 Kotlin 相等问题,决定记录一下探究过程。 事由 以下代码片段 Kotlin 版本 1.3.72。 还原问题代码,已去除业务逻辑部分,仅保留关键代码,片段如下: // 有一个状态枚举 enum class MyState { OK, CANCELED } // 某个处理函数会返回 nullable MyState fun processing(): MyState? { // 假设当前某种情况下返回 取消 这个状态 return MyState.CANCELED } // 在处理状态时 fun handleState() { // 此时编译器推断出类型为 State? val state = processing() if (state == CANCELED) { // 当处理 CANCELED 以下代码没有执行 println("Handle <CANCELED> state") } } 当处理 CANCELED 代码没有执行,原因在于***「import」***。 import javax.print.attribute.standard.JobState.CANCELED // 此处使用静态导入引入了一个其他包中同名的一个静态变量,该变量声明如下 // public static final JobState CANCELED = new JobState (7); 解决方法: ...

四月 21, 2020 · 5 分钟 · Lex Cao

使用 KAPT 生成 Kotlin Data Class 转换器

背景 Web 后台开发中,对于一个实体的操作会衍生出多个类似的对象进行操作(避免直接使用实体),由此出现相关名词 持久化对象,即实体 PO(Persistent Object) 传输对象 DTO(Data Transfer Object) 业务对象 BO(Business Object) 展示对象 VO(View Object) 等等…… 这些对象大多数直接从实体里面裁剪几个字段,比如,在一次创建订单请求中以订单实体(OrderEntity)为例,经历如下流程: 1. 接收请求体 CreateOrderRequest 2. 根据 OrderQuery 构造查询对象查询订单 3. 构造 OrderEntity 进行持久化操作 4. 构造 OrderBO 进行下游消费 5. 返回响应体 CreateOrderResponse 可见,从 OrderEntity 衍生出 4 个对象,仅仅是对订单的实体的部分裁剪,但是要编写很多重复的代码(复制也行)。当然,如果是新增字段的话可以使用继承解决。 在 Kotlin Web 后台开发中,data class 的语法特性带来很多优势,但还是避免不了创建类似的重复对象。 所以 Konverter 诞生于此,解决实体对象裁剪问题。还有另一个功能那就是自动生成两个实体间的转换方法。 注意:目前只支持 Kotlin,并且生成的转换方法是通过扩展函数实现 是什么 通过 KAPT(Kotlin Annotation Processing Tool 注解处理以及 Kotlin Poet 代码生成,实现自动生成对实体的相关裁剪的对象。 主要有两个注解: @Konvertable 生成裁剪的实体以及对应的转换方法 @Konvert 单独针对某个类生成转换方法 废话不多说来看怎么使用。 怎么用 1. 引入依赖 // for build.gradle.kts repositories { maven("https://jitpack.io") } dependencies { kapt("com.github.lexcao:konverter:master-SNAPSHOT") implementation("com.github.lexcao:konverter-annotation:master-SNAPSHOT") } // for build.gradle repositories { maven { url 'https://jitpack.io' } } dependencies { kapt 'com.github.lexcao:konverter:master-SNAPSHOT' implementation 'com.github.lexcao:konverter-annotation:master-SNAPSHOT' } 2. 在需要转换的类上加上注解 @Konvertable( To(name = "LoginDTO", pick = ["username", "password"]), To(name = "UserListDTO", omit = ["password"]) ) @Konvert(to = UserVO::class) data class UserEntity( val id: Long, @Konvert.Field("name") val username: String, val password: String, @Konvert.By(GenderEnumConverter::class) val gender: Int ) 3. 生成的代码如下: // @Konvertable /** * Auto generated code by @Konvertable */ data class LoginDTO( val username: String, val password: String ) /** * Auto generated code by @Konvertable */ data class UserListDTO( val id: Long, val username: String, val gender: Int ) /** * Auto generated code by @Konvert */ fun UserEntity.toLoginDTO(username: String = [email protected], password: String = [email protected]): LoginDTO = LoginDTO(username=username,password=password) /** * Auto generated code by @Konvert */ fun LoginDTO.toUserEntity( id: Long = 0L, username: String = [email protected], password: String = [email protected], gender: Int = 0 ): UserEntity = UserEntity(id=id,username=username,password=password,gender=gender) /** * Auto generated code by @Konvert */ fun UserEntity.toUserListDTO( id: Long = [email protected], username: String = [email protected], gender: Int = [email protected] ): UserListDTO = UserListDTO(id=id,username=username,gender=gender) /** * Auto generated code by @Konvert */ fun UserListDTO.toUserEntity( id: Long = [email protected], username: String = [email protected], password: String = "", gender: Int = [email protected] ): UserEntity = UserEntity(id=id,username=username,password=password,gender=gender) /** * Auto generated code by @Konvert */ fun UserEntity.toRegisterDTO( username: String = [email protected], password: String = [email protected], gender: Int = [email protected] ): RegisterDTO = RegisterDTO(username=username,password=password,gender=gender) /** * Auto generated code by @Konvert */ fun RegisterDTO.toUserEntity( id: Long = 0L, username: String = [email protected], password: String = [email protected], gender: Int = [email protected] ): UserEntity = UserEntity(id=id,username=username,password=password,gender=gender) // @Konvert // 转换为如下对象 data class UserVO( val id: String, val name: String, val gender: GenderEnum ) enum class GenderEnum { MALE, FEMALE; } object GenderEnumConverter : Konvert.KonvertBy<Int, GenderEnum> { override fun Int.forward(): GenderEnum { return GenderEnum.values()[this] } override fun GenderEnum.backward(): Int { return this.ordinal } } // 生成的代码 ** * Auto generated code by @Konvert */ fun UserEntity.toUserVO( id: String = [email protected](), name: String = [email protected], gender: GenderEnum = with(GenderEnumConverter) { [email protected]() } ): UserVO = UserVO(id=id,name=name,gender=gender) /** * Auto generated code by @Konvert */ fun UserVO.toUserEntity( id: Long = [email protected](), username: String = [email protected], password: String = "", gender: Int = with(GenderEnumConverter) { [email protected]() } ): UserEntity = UserEntity(id=id,username=username,password=password,gender=gender) 相关 API 说明 转换规则 ...

四月 13, 2020 · 3 分钟 · Lex Cao