Compare commits
16 Commits
f81217f14a
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 6bf9a3295a | |||
| 0a7e38f931 | |||
| fc9cb14daf | |||
| 4abb65129b | |||
| 8ef18a8e85 | |||
| 22811e4adb | |||
| 28bcc1a118 | |||
| e9b0e79d48 | |||
| 18cd1dbed8 | |||
| 2db84152b5 | |||
| 28baf5600b | |||
| 015016a2da | |||
| 7b555492ee | |||
| e7fa23a365 | |||
| 2adf4951f7 | |||
| 3692657b64 |
11
.gitignore
vendored
11
.gitignore
vendored
@@ -54,12 +54,7 @@ buildNumber.properties
|
|||||||
*.rar
|
*.rar
|
||||||
hs_err_pid*
|
hs_err_pid*
|
||||||
replay_pid*
|
replay_pid*
|
||||||
.vscode/*
|
.vscode/
|
||||||
!.vscode/settings.json
|
|
||||||
!.vscode/tasks.json
|
|
||||||
!.vscode/launch.json
|
|
||||||
!.vscode/extensions.json
|
|
||||||
!.vscode/*.code-snippets
|
|
||||||
.history/
|
.history/
|
||||||
*.vsix
|
*.vsix
|
||||||
.metadata
|
.metadata
|
||||||
@@ -132,3 +127,7 @@ Icon
|
|||||||
Network Trash Folder
|
Network Trash Folder
|
||||||
Temporary Items
|
Temporary Items
|
||||||
.apdisk
|
.apdisk
|
||||||
|
.opencode
|
||||||
|
.claude
|
||||||
|
.kilocode
|
||||||
|
openspec/changes/archive
|
||||||
506
README.md
506
README.md
@@ -1,469 +1,109 @@
|
|||||||
# Spring Boot Service Template
|
# Spring Boot Service Template
|
||||||
|
|
||||||
这是一个基于 Spring Boot 的服务模板项目,旨在为开发者提供一个标准化的微服务基础结构,简化新项目的搭建过程,提高开发效率。
|
微服务快速开发能力模板,v1.1.0-SNAPSHOT。Java 17, Spring Boot 4.0.0, Spring Cloud 2025.1.0。
|
||||||
|
|
||||||
## 目录
|
GroupId: `com.lanyuanxiaoyao`,根包: `com.lanyuanxiaoyao.service.template`。
|
||||||
|
|
||||||
- [1. 项目概述](#1-项目概述)
|
## 模块
|
||||||
- [2. 技术架构](#2-技术架构)
|
|
||||||
- [3. 项目结构](#3-项目结构)
|
|
||||||
- [4. 核心功能](#4-核心功能)
|
|
||||||
- [5. 使用指南](#5-使用指南)
|
|
||||||
- [6. 开发规范](#6-开发规范)
|
|
||||||
- [7. 项目集成指南](#7-项目集成指南)
|
|
||||||
|
|
||||||
## 1. 项目概述
|
|
||||||
|
|
||||||
### 1.1 项目简介
|
|
||||||
|
|
||||||
Spring Boot Service Template 是一个标准化的微服务基础结构模板,专为 Java 开发者和微服务架构设计者打造。该项目提供了一套完整的 CRUD 操作框架,通过泛型支持不同类型的数据转换,大大减少了重复代码的编写。
|
|
||||||
|
|
||||||
该模板内置了完善的实体审计机制,自动维护实体的创建时间和修改时间。同时,提供了强大的查询功能,支持多种条件查询、分页和排序功能。
|
|
||||||
|
|
||||||
### 1.2 项目目标
|
|
||||||
|
|
||||||
提供统一的服务模板,简化新项目的搭建过程,提高开发效率。通过封装通用的业务逻辑,让开发者能够专注于核心业务功能的实现。
|
|
||||||
|
|
||||||
### 1.3 核心特性
|
|
||||||
|
|
||||||
- 标准化的项目结构:遵循业界最佳实践的目录结构和代码组织方式
|
|
||||||
- 简化依赖管理和构建流程:基于 Maven 的依赖管理,清晰的构建配置
|
|
||||||
- 支持快速构建和部署微服务:提供完整的微服务基础组件
|
|
||||||
- 泛型支持:通过泛型实现不同类型间的数据转换
|
|
||||||
- 完善的审计机制:自动维护实体的创建时间和修改时间
|
|
||||||
- 强大的查询功能:支持多种条件查询、分页和排序
|
|
||||||
- 灵活的扩展机制:易于定制和扩展的架构设计
|
|
||||||
|
|
||||||
### 1.4 适用场景
|
|
||||||
|
|
||||||
适用于需要快速搭建 Spring Boot 微服务的项目,特别是那些需要大量 CRUD 操作的业务系统。
|
|
||||||
|
|
||||||
## 2. 技术架构
|
|
||||||
|
|
||||||
### 2.1 技术选型
|
|
||||||
|
|
||||||
#### 2.1.1 后端技术栈
|
|
||||||
- Java 17
|
|
||||||
- Spring Boot 3.4.3
|
|
||||||
- Spring Data JPA
|
|
||||||
- QueryDSL 7.0
|
|
||||||
- Lombok
|
|
||||||
- Fenix Spring Boot Starter 3.1.0
|
|
||||||
|
|
||||||
#### 2.1.2 核心框架
|
|
||||||
- Spring Boot 作为核心框架,提供自动配置和快速开发能力
|
|
||||||
- Spring Data JPA 用于数据访问,简化数据库操作
|
|
||||||
- QueryDSL 用于类型安全的查询构建,避免运行时错误
|
|
||||||
- Fenix 用于复杂动态查询,提供更灵活的查询能力
|
|
||||||
- Lombok 减少样板代码,提高开发效率
|
|
||||||
|
|
||||||
#### 2.1.3 数据库技术
|
|
||||||
- H2 Database (测试环境)
|
|
||||||
|
|
||||||
### 2.2 架构设计
|
|
||||||
|
|
||||||
#### 2.2.1 整体架构图
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────┐
|
spring-boot-service-template/ (根 POM, packaging=pom)
|
||||||
│ Controller Layer │
|
├── spring-boot-service-template-common/ (jar — 通用工具)
|
||||||
│ (处理HTTP请求,数据转换与响应) │
|
└── spring-boot-service-template-database/ (jar — JPA 数据库能力)
|
||||||
├─────────────────────────────────────┤
|
|
||||||
│ Service Layer │
|
|
||||||
│ (业务逻辑处理,事务管理) │
|
|
||||||
├─────────────────────────────────────┤
|
|
||||||
│ Repository Layer │
|
|
||||||
│ (数据访问,数据库交互) │
|
|
||||||
├─────────────────────────────────────┤
|
|
||||||
│ Entity Layer │
|
|
||||||
│ (数据模型定义,实体映射) │
|
|
||||||
└─────────────────────────────────────┘
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 2.2.2 模块划分
|
模块命名规则: `spring-boot-service-template-{capability}[-{impl}]`
|
||||||
- controller: 控制层,处理 HTTP 请求,包括接口定义和支持类
|
|
||||||
- entity: 实体层,定义数据模型和数据库映射
|
|
||||||
- service: 服务层,处理业务逻辑和事务管理
|
|
||||||
- repository: 仓储层,处理数据访问和数据库交互
|
|
||||||
- helper: 辅助类模块,提供工具类和通用方法
|
|
||||||
|
|
||||||
#### 2.2.3 设计模式
|
## 包结构
|
||||||
- 模板方法模式:通过抽象类定义通用操作流程
|
|
||||||
- 策略模式:通过函数式接口实现数据转换策略
|
|
||||||
- 仓储模式:封装数据访问逻辑,提供统一的数据操作接口
|
|
||||||
|
|
||||||
## 3. 项目结构
|
|
||||||
|
|
||||||
### 3.1 目录结构说明
|
|
||||||
```
|
```
|
||||||
src/
|
com.lanyuanxiaoyao.service.template.{module}/
|
||||||
├── main/
|
configuration/ # @Configuration(用 configuration 不用 config)
|
||||||
│ └── java/
|
controller/ # REST 控制器 + 接口
|
||||||
│ └── com/lanyuanxiaoyao/service/template/
|
entity/ # JPA 实体、Query、GlobalResponse 等数据对象
|
||||||
│ ├── controller/
|
exception/ # 自定义异常(用 exception 不用 exceptions)
|
||||||
│ ├── entity/
|
helper/ # 工具类(用 helper 不用 util/utils)
|
||||||
│ ├── helper/
|
repository/ # Spring Data 仓库
|
||||||
│ ├── repository/
|
service/ # 业务服务
|
||||||
│ └── service/
|
|
||||||
└── test/
|
|
||||||
└── java/
|
|
||||||
└── com/lanyuanxiaoyao/service/template/
|
|
||||||
├── controller/
|
|
||||||
├── entity/
|
|
||||||
├── repository/
|
|
||||||
└── service/
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3.2 核心模块介绍
|
无独立 DTO/VO 包,DTO 作为 Controller 内部 record。
|
||||||
|
|
||||||
#### 3.2.1 controller模块
|
## database 模块
|
||||||
提供基础的 CRUD 操作控制器接口和支持类。主要包含:
|
|
||||||
- [SimpleController](src/main/java/com/lanyuanxiaoyao/service/template/controller/SimpleController.java):定义基础 CRUD 接口
|
|
||||||
- [SimpleControllerSupport](src/main/java/com/lanyuanxiaoyao/service/template/controller/SimpleControllerSupport.java):实现基础 CRUD 功能
|
|
||||||
- [Query](src/main/java/com/lanyuanxiaoyao/service/template/controller/Query.java):查询条件封装类
|
|
||||||
- 其他辅助接口如 SaveController、ListController、DetailController、RemoveController
|
|
||||||
|
|
||||||
#### 3.2.2 entity模块
|
单表 CRUD → REST 接口快速实现框架。基于 JPA + Fenix + QueryDSL + MapStruct。
|
||||||
定义基础实体类,包含审计字段。主要包含:
|
|
||||||
- [IdOnlyEntity](src/main/java/com/lanyuanxiaoyao/service/template/entity/IdOnlyEntity.java):仅包含 ID 的基础实体
|
|
||||||
- [SimpleEntity](src/main/java/com/lanyuanxiaoyao/service/template/entity/SimpleEntity.java):包含基础字段的实体类,继承自 IdOnlyEntity
|
|
||||||
|
|
||||||
#### 3.2.3 service模块
|
**文档**:
|
||||||
提供基础服务接口和支持类。主要包含:
|
- [开发指南](docs/database-development.md) - 模块架构、核心设计、技术实现、扩展指南
|
||||||
- [SimpleService](src/main/java/com/lanyuanxiaoyao/service/template/service/SimpleService.java):定义基础服务接口
|
- [使用指南](docs/database-usage.md) - 快速开始、API 接口、查询条件、高级用法
|
||||||
- [SimpleServiceSupport](src/main/java/com/lanyuanxiaoyao/service/template/service/SimpleServiceSupport.java):实现基础服务功能
|
|
||||||
|
|
||||||
#### 3.2.4 repository模块
|
## 开发规范
|
||||||
定义数据访问仓储接口。主要包含:
|
|
||||||
- [SimpleRepository](src/main/java/com/lanyuanxiaoyao/service/template/repository/SimpleRepository.java):基础仓储接口
|
|
||||||
|
|
||||||
#### 3.2.5 helper模块
|
### 命名
|
||||||
提供辅助工具类。主要包含:
|
|
||||||
- [ObjectHelper](src/main/java/com/lanyuanxiaoyao/service/template/helper/ObjectHelper.java):对象帮助类
|
|
||||||
|
|
||||||
### 3.3 测试模块结构
|
| 类别 | 模式 | 示例 |
|
||||||
测试模块包含完整的测试用例,覆盖 controller、entity、repository 和 service 各层。通过实际的业务实体(如 Employee、Company、Report)演示如何使用模板。
|
| --------------- | ---------------------------------------- | ---------------------------------- |
|
||||||
|
| 实体基类 | `*Entity` | `IdOnlyEntity`, `SimpleEntity` |
|
||||||
|
| 业务实体 | 纯名词 | `Employee` |
|
||||||
|
| 控制器接口 | `*Controller` | `SaveController` |
|
||||||
|
| 控制器抽象基类 | `*ControllerSupport` | `SimpleControllerSupport` |
|
||||||
|
| 服务接口 | `*Service` | `SaveService` |
|
||||||
|
| 服务抽象基类 | `*ServiceSupport` | `SimpleServiceSupport` |
|
||||||
|
| 仓库 | `*Repository` | `EmployeeRepository` |
|
||||||
|
| 工具类 | `*Helper` | `ObjectHelper` |
|
||||||
|
| 配置类 | `*Configuration` | `QueryDSLConfiguration` |
|
||||||
|
| 异常 | `*Exception` | `IdNotFoundException` |
|
||||||
|
| 内部 DTO record | `SaveItem`/`ListItem`/`DetailItem` | Controller 内部 |
|
||||||
|
|
||||||
## 4. 核心功能
|
- 接口不加 `I` 前缀
|
||||||
|
- 方法: 动词开头 `save()`/`list()`/`detail()`/`remove()`;布尔用 `is`/`has` 前缀
|
||||||
|
- 常量 UPPER_SNAKE_CASE;字段常量用 `@FieldNameConstants` 生成 `{Class}.Fields.{field}`
|
||||||
|
- 泛型: 简单用 `T`/`E`,领域用 `ENTITY`/`SAVE_ITEM`/`LIST_ITEM`/`DETAIL_ITEM`
|
||||||
|
|
||||||
### 4.1 基础CRUD操作
|
### 代码风格
|
||||||
|
|
||||||
#### 4.1.1 创建(Create)
|
- 2 空格缩进,K&R 大括号,不用 Tab
|
||||||
支持通过 POST 请求创建实体对象。通过 save 接口实现,接收 SAVE_ITEM 类型的参数,通过 Mapper 转换为实体对象后保存。
|
- Import 排序: `java.*` → `jakarta.*` → 第三方 → `com.lanyuanxiaoyao.*`,不用通配符
|
||||||
|
- 积极使用 Java 17 特性: `record`(DTO) / `var` / `instanceof` 模式匹配 / Text Blocks / `String.formatted()`
|
||||||
|
- JavaDoc 用中文,含 `@param`/`@return`/`@see`,可用 `<p>`/`<h3>`/`<ul>`/`<pre>`
|
||||||
|
- 不写 `//` 行内注释,除非必要
|
||||||
|
|
||||||
#### 4.1.2 查询(Retrieve)
|
### 注解
|
||||||
支持多种查询方式:
|
|
||||||
- 列表查询:获取所有实体对象
|
|
||||||
- 条件查询:根据指定条件查询实体对象
|
|
||||||
- 详情查询:根据 ID 获取单个实体对象
|
|
||||||
|
|
||||||
#### 4.1.3 更新(Update)
|
用:
|
||||||
支持通过 POST 请求更新实体对象。通过 save 接口实现,当传入包含 ID 的对象时执行更新操作。
|
|
||||||
|
|
||||||
#### 4.1.4 删除(Delete)
|
- 实体: `@Getter` `@Setter` `@ToString` `@FieldNameConstants`,子类加 `@ToString(callSuper = true)`
|
||||||
支持通过 GET 请求删除实体对象。通过 remove 接口实现,根据 ID 删除指定实体。
|
- 日志: `@Slf4j`
|
||||||
|
- 注入: `@RequiredArgsConstructor(access = AccessLevel.PROTECTED)`(构造器注入)
|
||||||
|
- 事务: 写 `@Transactional(rollbackFor = Throwable.class)`,读 `@Transactional(readOnly = true)`
|
||||||
|
- 实体基类: `@MappedSuperclass` `@EntityListeners(AuditingEntityListener.class)`
|
||||||
|
|
||||||
### 4.2 查询功能详解
|
不用:
|
||||||
|
|
||||||
#### 4.2.1 简单查询
|
- 不用 `@Valid`/`@NotNull` 等校验注解
|
||||||
支持基于 ID 的简单查询,通过 detail 接口实现。
|
- 不用 Swagger/OpenAPI 注解
|
||||||
|
- 不用 `@Autowired` 字段注入
|
||||||
|
- 不写代码注释(`//`),除非必要
|
||||||
|
|
||||||
#### 4.2.2 条件查询
|
### 异常
|
||||||
支持基于多种条件的复杂查询,通过 list 接口实现,支持以下查询条件:
|
|
||||||
- nullEqual: 字段值为null的条件
|
|
||||||
- notNullEqual: 字段值不为null的条件
|
|
||||||
- empty: 字段值为空的条件
|
|
||||||
- notEmpty: 字段值不为空的条件
|
|
||||||
- equal: 字段值相等的条件
|
|
||||||
- notEqual: 字段值不相等的条件
|
|
||||||
- like: 字段值模糊匹配的条件
|
|
||||||
- notLike: 字段值不模糊匹配的条件
|
|
||||||
- great: 字段值大于的条件
|
|
||||||
- less: 字段值小于的条件
|
|
||||||
- greatEqual: 字段值大于等于的条件
|
|
||||||
- lessEqual: 字段值小于等于的条件
|
|
||||||
- in: 字段值在指定范围内的条件
|
|
||||||
- notIn: 字段值不在指定范围内的条件
|
|
||||||
- between: 字段值在指定区间内的条件
|
|
||||||
- notBetween: 字段值不在指定区间内的条件
|
|
||||||
|
|
||||||
#### 4.2.3 分页查询
|
继承 `RuntimeException`,构造器用 `String.formatted()`。查无数据用 `Optional.orElseThrow(() -> new IdNotFoundException(id))`。无全局 `@ControllerAdvice`。
|
||||||
支持分页查询功能,通过 page 配置实现分页参数设置。
|
|
||||||
|
|
||||||
#### 4.2.4 排序查询
|
### 依赖管理
|
||||||
支持排序查询功能,通过 sort 配置实现排序参数设置。
|
|
||||||
|
|
||||||
### 4.3 数据实体设计
|
版本集中在根 POM `<properties>` + `<dependencyManagement>`,子模块不声明版本。
|
||||||
|
全局继承: `lombok` / `jspecify:1.0.0`
|
||||||
|
核心: `spring-boot-starter-data-jpa` / `fenix-spring-boot-starter:4.0.0` / `querydsl-jpa:7.1` / `mapstruct:1.6.3`
|
||||||
|
database 模块注解处理器: lombok → hibernate-jpamodelgen → querydsl-apt(jpa) → jakarta.persistence-api → mapstruct-processor
|
||||||
|
|
||||||
#### 4.3.1 基础实体类
|
### 通用原则
|
||||||
定义通用的实体基类,包括 IdOnlyEntity 和 SimpleEntity。
|
|
||||||
|
|
||||||
#### 4.3.2 审计字段
|
- 文档/注释/commit 用中文,代码标识符用英文
|
||||||
包含创建时间(createdTime)和修改时间(modifiedTime)等审计字段,通过 Spring Data JPA 的审计功能自动维护。
|
- 不引入新依赖前先复用已有组件,优先 JDK 和 Spring 内置能力
|
||||||
|
- 构造器注入,不使用 `@Autowired` 字段注入
|
||||||
|
|
||||||
#### 4.3.3 实体关系
|
### 构建
|
||||||
支持常见的实体关系映射,如一对一、一对多、多对多等。
|
|
||||||
|
|
||||||
## 5. 使用指南
|
```bash
|
||||||
|
mvn clean package
|
||||||
### 5.1 环境准备
|
|
||||||
|
|
||||||
#### 5.1.1 JDK安装
|
|
||||||
需要安装 JDK 17 或更高版本。
|
|
||||||
|
|
||||||
#### 5.1.2 Maven配置
|
|
||||||
需要配置 Maven 3.x 环境。
|
|
||||||
|
|
||||||
### 5.2 项目构建
|
|
||||||
|
|
||||||
#### 5.2.1 依赖管理
|
|
||||||
通过 Maven 管理项目依赖。
|
|
||||||
|
|
||||||
#### 5.2.2 编译打包
|
|
||||||
使用 `mvn clean package` 命令编译打包项目。
|
|
||||||
|
|
||||||
### 5.3 运行部署
|
|
||||||
|
|
||||||
#### 5.3.1 本地运行
|
|
||||||
可以使用 `mvn spring-boot:run` 命令运行项目。
|
|
||||||
|
|
||||||
#### 5.3.2 生产部署
|
|
||||||
可通过生成的 JAR 文件直接运行或部署到服务器,使用命令:
|
|
||||||
`java -jar target/spring-boot-service-template-1.0-SNAPSHOT.jar`
|
|
||||||
|
|
||||||
## 6. 开发规范
|
|
||||||
|
|
||||||
### 6.1 代码规范
|
|
||||||
遵循 Java 标准编码规范和 Spring Boot 最佳实践。
|
|
||||||
|
|
||||||
### 6.2 接口规范
|
|
||||||
RESTful API 设计规范,统一的响应格式。
|
|
||||||
|
|
||||||
### 6.3 注释规范
|
|
||||||
遵循标准的 JavaDoc 格式编写注释,详细说明类、方法、参数和返回值的含义。
|
|
||||||
|
|
||||||
### 6.4 异常处理
|
|
||||||
统一异常处理机制,提供友好的错误信息。
|
|
||||||
|
|
||||||
## 7. 项目集成指南
|
|
||||||
|
|
||||||
本章节详细介绍如何在现有项目中集成 Spring Boot Service Template 的能力。
|
|
||||||
|
|
||||||
### 7.1 集成方式
|
|
||||||
|
|
||||||
通过 Maven 依赖引入(推荐)
|
|
||||||
|
|
||||||
### 7.2 Maven 依赖引入方式
|
|
||||||
|
|
||||||
#### 7.2.1 添加依赖
|
|
||||||
|
|
||||||
在您的项目 pom.xml 文件中添加以下依赖:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.lanyuanxiaoyao</groupId>
|
|
||||||
<artifactId>spring-boot-service-template</artifactId>
|
|
||||||
<version>1.0.0-SNAPSHOT</version>
|
|
||||||
</dependency>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 7.2.2 配置依赖管理
|
|
||||||
|
|
||||||
确保您的项目中包含以下依赖管理配置:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<dependencyManagement>
|
|
||||||
<dependencies>
|
|
||||||
<!-- spring boot 相关依赖 -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-dependencies</artifactId>
|
|
||||||
<version>${spring-boot.version}</version>
|
|
||||||
<type>pom</type>
|
|
||||||
<scope>import</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.cloud</groupId>
|
|
||||||
<artifactId>spring-cloud-dependencies</artifactId>
|
|
||||||
<version>${spring-cloud.version}</version>
|
|
||||||
<type>pom</type>
|
|
||||||
<scope>import</scope>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
</dependencyManagement>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 7.2.3 配置编译插件
|
|
||||||
|
|
||||||
确保您的项目中包含以下编译插件配置:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<build>
|
|
||||||
<plugins>
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
|
||||||
<version>3.14.0</version>
|
|
||||||
<configuration>
|
|
||||||
<annotationProcessorPaths>
|
|
||||||
<path>
|
|
||||||
<groupId>org.projectlombok</groupId>
|
|
||||||
<artifactId>lombok</artifactId>
|
|
||||||
<version>1.18.36</version>
|
|
||||||
</path>
|
|
||||||
<path>
|
|
||||||
<groupId>org.hibernate</groupId>
|
|
||||||
<artifactId>hibernate-jpamodelgen</artifactId>
|
|
||||||
<version>6.6.3.Final</version>
|
|
||||||
</path>
|
|
||||||
<path>
|
|
||||||
<groupId>io.github.openfeign.querydsl</groupId>
|
|
||||||
<artifactId>querydsl-jpa</artifactId>
|
|
||||||
<version>7.0</version>
|
|
||||||
</path>
|
|
||||||
</annotationProcessorPaths>
|
|
||||||
</configuration>
|
|
||||||
</plugin>
|
|
||||||
</plugins>
|
|
||||||
</build>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.4 创建业务模块
|
|
||||||
|
|
||||||
创建业务模块的步骤如下:
|
|
||||||
|
|
||||||
#### 7.4.1 创建实体类
|
|
||||||
|
|
||||||
创建实体类并继承 [SimpleEntity](src/main/java/com/lanyuanxiaoyao/service/template/entity/SimpleEntity.java):
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Entity
|
|
||||||
@Table(name = "employee")
|
|
||||||
public class Employee extends SimpleEntity {
|
|
||||||
private String name;
|
|
||||||
private Integer age;
|
|
||||||
// 其他字段和方法
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 7.4.2 创建仓储接口
|
|
||||||
|
|
||||||
创建仓储接口并继承 [SimpleRepository](src/main/java/com/lanyuanxiaoyao/service/template/repository/SimpleRepository.java):
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Repository
|
|
||||||
public interface EmployeeRepository extends SimpleRepository<Employee> {
|
|
||||||
// 自定义查询方法
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 7.4.3 创建服务类
|
|
||||||
|
|
||||||
创建服务类并继承 [SimpleServiceSupport](src/main/java/com/lanyuanxiaoyao/service/template/service/SimpleServiceSupport.java):
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Service
|
|
||||||
public class EmployeeService extends SimpleServiceSupport<Employee> {
|
|
||||||
public EmployeeService(EmployeeRepository repository) {
|
|
||||||
super(repository);
|
|
||||||
}
|
|
||||||
// 自定义业务方法
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 7.4.4 创建控制器类
|
|
||||||
|
|
||||||
创建控制器类并继承 [SimpleControllerSupport](src/main/java/com/lanyuanxiaoyao/service/template/controller/SimpleControllerSupport.java):
|
|
||||||
|
|
||||||
```java
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("employee")
|
|
||||||
public class EmployeeController extends SimpleControllerSupport<Employee, EmployeeSaveItem, EmployeeListItem, EmployeeDetailItem> {
|
|
||||||
public EmployeeController(EmployeeService service) {
|
|
||||||
super(service);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Function<EmployeeSaveItem, Employee> saveItemMapper() {
|
|
||||||
// 实现保存项转换逻辑
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Function<Employee, EmployeeListItem> listItemMapper() {
|
|
||||||
// 实现列表项转换逻辑
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Function<Employee, EmployeeDetailItem> detailItemMapper() {
|
|
||||||
// 实现详情项转换逻辑
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 7.4.5 创建数据传输对象
|
|
||||||
|
|
||||||
创建用于数据传输的对象:
|
|
||||||
|
|
||||||
```java
|
|
||||||
// 保存项
|
|
||||||
public class EmployeeSaveItem {
|
|
||||||
private Long id;
|
|
||||||
private String name;
|
|
||||||
private Integer age;
|
|
||||||
// getter和setter方法
|
|
||||||
}
|
|
||||||
|
|
||||||
// 列表项
|
|
||||||
public class EmployeeListItem {
|
|
||||||
private Long id;
|
|
||||||
private String name;
|
|
||||||
private Integer age;
|
|
||||||
// getter和setter方法
|
|
||||||
}
|
|
||||||
|
|
||||||
// 详情项
|
|
||||||
public class EmployeeDetailItem {
|
|
||||||
private Long id;
|
|
||||||
private String name;
|
|
||||||
private Integer age;
|
|
||||||
private LocalDateTime createdTime;
|
|
||||||
private LocalDateTime modifiedTime;
|
|
||||||
// getter和setter方法
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.5 配置启用 JPA 审计
|
|
||||||
|
|
||||||
在您的主应用类上添加 [@EnableJpaAuditing](https://docs.spring.io/spring-data/jpa/docs/current/api/org/springframework/data/jpa/repository/config/EnableJpaAuditing.html) 注解以启用 JPA 审计功能:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@SpringBootApplication
|
|
||||||
@EnableJpaAuditing
|
|
||||||
public class YourApplication {
|
|
||||||
public static void main(String[] args) {
|
|
||||||
SpringApplication.run(YourApplication.class, args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.6 配置 Fenix
|
|
||||||
|
|
||||||
在您的 application.yml 或 application.properties 文件中添加 Fenix 配置:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
fenix:
|
|
||||||
# 是否开启 Fenix
|
|
||||||
enabled: true
|
|
||||||
# SQL 执行后的输出格式,console: 控制台彩色输出; html: 彩色 HTML 输出; text: 纯文本输出
|
|
||||||
output-format: console
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.7 测试集成效果
|
|
||||||
|
|
||||||
完成以上步骤后,您可以运行您的应用程序并测试以下功能:
|
|
||||||
|
|
||||||
1. 创建实体:POST /employee/save
|
|
||||||
2. 查询列表:GET /employee/list 或 POST /employee/list(带条件)
|
|
||||||
3. 查询详情:GET /employee/detail/{id}
|
|
||||||
4. 删除实体:GET /employee/remove/{id}
|
|
||||||
|
|
||||||
通过以上步骤,您就可以成功在现有项目中集成 Spring Boot Service Template 的能力,快速实现 CRUD 功能。
|
|
||||||
205
docs/database-development.md
Normal file
205
docs/database-development.md
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
# Database 模块开发指南
|
||||||
|
|
||||||
|
单表 CRUD → REST 接口快速实现框架。基于 JPA + Fenix + QueryDSL + MapStruct。
|
||||||
|
|
||||||
|
## 架构
|
||||||
|
|
||||||
|
```
|
||||||
|
Controller (REST) → Service (业务) → Repository (数据访问) → Entity (模型)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 组件结构
|
||||||
|
|
||||||
|
| 包 | 组件 | 职责 |
|
||||||
|
|---|---|---|
|
||||||
|
| entity | IdOnlyEntity, SimpleEntity | 实体基类 |
|
||||||
|
| entity | SnowflakeId, SnowflakeIdGenerator | ID 生成 |
|
||||||
|
| entity | Query, GlobalResponse, Page | 查询/响应封装 |
|
||||||
|
| repository | SimpleRepository | 统一数据访问接口 |
|
||||||
|
| service | SaveService, QueryService, RemoveService | 功能接口 |
|
||||||
|
| service | SimpleService, SimpleServiceSupport | 组合接口与实现 |
|
||||||
|
| service | QueryParser | 查询条件解析 |
|
||||||
|
| controller | SaveController, QueryController, RemoveController | REST 接口 |
|
||||||
|
| controller | SimpleController, SimpleControllerSupport | 组合接口与实现 |
|
||||||
|
| helper | DatabaseHelper, SnowflakeHelper | 工具类 |
|
||||||
|
| exception | *Exception | 异常定义 |
|
||||||
|
|
||||||
|
## 核心设计
|
||||||
|
|
||||||
|
### 实体继承
|
||||||
|
|
||||||
|
```
|
||||||
|
IdOnlyEntity (id: Long, @SnowflakeId)
|
||||||
|
↑
|
||||||
|
SimpleEntity (+ createdTime, modifiedTime)
|
||||||
|
↑
|
||||||
|
业务实体 (@Entity)
|
||||||
|
```
|
||||||
|
|
||||||
|
**实现要点**:
|
||||||
|
- `@MappedSuperclass` 标记基类
|
||||||
|
- `@SnowflakeId` 触发 `SnowflakeIdGenerator` 生成 ID
|
||||||
|
- `@CreatedDate/@LastModifiedDate` + `AuditingEntityListener` 自动填充时间
|
||||||
|
|
||||||
|
### Repository
|
||||||
|
|
||||||
|
```java
|
||||||
|
@NoRepositoryBean
|
||||||
|
public interface SimpleRepository<E> extends
|
||||||
|
FenixJpaRepository<E, Long>, // CRUD + Fenix
|
||||||
|
FenixJpaSpecificationExecutor<E>, // Specification
|
||||||
|
ListQueryByExampleExecutor<E>, // Example
|
||||||
|
ListQuerydslPredicateExecutor<E> {} // QueryDSL
|
||||||
|
```
|
||||||
|
|
||||||
|
**核心能力**:
|
||||||
|
- `saveOrUpdateByNotNullProperties()` - 部分字段更新
|
||||||
|
- Specification - 动态条件查询
|
||||||
|
- QueryDSL - 类型安全查询
|
||||||
|
|
||||||
|
### Service 接口组合
|
||||||
|
|
||||||
|
```java
|
||||||
|
SaveService<ENTITY> // save(entity), save(entities)
|
||||||
|
QueryService<ENTITY> // detail(id), list(), list(query), count()
|
||||||
|
RemoveService<ENTITY> // remove(id), remove(ids)
|
||||||
|
|
||||||
|
SimpleService<ENTITY> extends SaveService, QueryService, RemoveService
|
||||||
|
```
|
||||||
|
|
||||||
|
**SimpleServiceSupport 实现**:
|
||||||
|
- 保存:Fenix `saveOrUpdateByNotNullProperties()`
|
||||||
|
- 查询:JPA Criteria + Specification
|
||||||
|
- 删除:`deleteBatchByIds()`
|
||||||
|
- 扩展点:重写 `commonPredicates()` 添加全局过滤条件
|
||||||
|
|
||||||
|
### Controller 接口组合
|
||||||
|
|
||||||
|
```java
|
||||||
|
SaveController<SAVE_ITEM> // POST /save
|
||||||
|
QueryController<LIST_ITEM, DETAIL_ITEM> // GET/POST /list, GET /detail/{id}
|
||||||
|
RemoveController // GET /remove/{id}
|
||||||
|
|
||||||
|
SimpleController<SAVE_ITEM, LIST_ITEM, DETAIL_ITEM>
|
||||||
|
```
|
||||||
|
|
||||||
|
**SimpleControllerSupport 实现**:
|
||||||
|
- 调用 Service 方法
|
||||||
|
- 通过 Mapper 转换 DTO ↔ Entity
|
||||||
|
- 封装 GlobalResponse
|
||||||
|
- 扩展点:实现 `saveItemMapper()`, `listItemMapper()`, `detailItemMapper()`
|
||||||
|
|
||||||
|
### 查询条件
|
||||||
|
|
||||||
|
**Query 结构**:`Query(query: Queryable, sort: List<Sortable>, page: Pageable)`
|
||||||
|
|
||||||
|
**QueryParser**:抽象类定义解析接口,`JpaQueryParser` 转换为 JPA Predicate
|
||||||
|
|
||||||
|
**支持操作**:
|
||||||
|
|
||||||
|
| 类别 | 操作 |
|
||||||
|
|---|---|
|
||||||
|
| 空值 | nullEqual, notNullEqual, empty, notEmpty |
|
||||||
|
| 相等 | equal, notEqual |
|
||||||
|
| 模糊 | like, notLike, contain, notContain |
|
||||||
|
| 前后缀 | startWith, endWith, notStartWith, notEndWith |
|
||||||
|
| 比较 | great, less, greatEqual, lessEqual |
|
||||||
|
| 区间 | between, notBetween |
|
||||||
|
| 集合 | inside, notInside |
|
||||||
|
|
||||||
|
**实现**:JPA CriteriaBuilder 构建,支持多级字段路径(如 `user.name`),自动类型转换(枚举、LocalDateTime)
|
||||||
|
|
||||||
|
### 响应格式
|
||||||
|
|
||||||
|
```java
|
||||||
|
GlobalResponse<T>(status, message, data)
|
||||||
|
// status: 0 成功, 500 失败
|
||||||
|
// 列表: data = ListItem(items, total)
|
||||||
|
// 详情: data = DetailItem(item)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 技术细节
|
||||||
|
|
||||||
|
### 雪花算法
|
||||||
|
|
||||||
|
`SnowflakeHelper`:64 位 Long,1 位符号 + 41 位时间戳 + 10 位机器 ID + 12 位序列号
|
||||||
|
|
||||||
|
`SnowflakeIdGenerator`:实现 `IdentifierGenerator`,持久化时调用 `SnowflakeHelper.next()`
|
||||||
|
|
||||||
|
### 部分更新
|
||||||
|
|
||||||
|
Fenix `saveOrUpdateByNotNullProperties()`:
|
||||||
|
- 自动判断 INSERT/UPDATE
|
||||||
|
- 仅更新非 null 字段
|
||||||
|
|
||||||
|
### 命名策略
|
||||||
|
|
||||||
|
`PhysicalNamingStrategySnakeCaseImpl`:camelCase → snake_case
|
||||||
|
|
||||||
|
### 注解处理器
|
||||||
|
|
||||||
|
执行顺序:lombok → hibernate-jpamodelgen → querydsl-apt → mapstruct-processor
|
||||||
|
|
||||||
|
生成:getter/setter、JPA 元模型(_Entity)、QueryDSL Q 类(QEntity)、MapStruct Mapper
|
||||||
|
|
||||||
|
## 工具类
|
||||||
|
|
||||||
|
### DatabaseHelper
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 生成 DDL
|
||||||
|
generateDDL(entityPackages, ddlFilePath, dialect, jdbc, username, password, driver)
|
||||||
|
|
||||||
|
// 生成脚手架
|
||||||
|
generateBasicFiles(entityPackages, projectRootPackage, projectRootPath, override)
|
||||||
|
```
|
||||||
|
|
||||||
|
### SnowflakeHelper
|
||||||
|
|
||||||
|
```java
|
||||||
|
Long id = SnowflakeHelper.next();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 扩展指南
|
||||||
|
|
||||||
|
### 软删除
|
||||||
|
|
||||||
|
1. 创建 `SoftDeleteEntity extends SimpleEntity`,添加 `deleted` 字段
|
||||||
|
2. 重写 `commonPredicates()` 返回 `deleted = false` 条件
|
||||||
|
3. 覆盖 `remove()` 改为更新 `deleted` 字段
|
||||||
|
|
||||||
|
### 多租户
|
||||||
|
|
||||||
|
1. 创建 `TenantEntity extends SimpleEntity`,添加 `tenantId` 字段
|
||||||
|
2. 重写 `commonPredicates()` 添加租户过滤
|
||||||
|
3. ThreadLocal 或 Spring Security 存储当前租户
|
||||||
|
|
||||||
|
### 审计字段
|
||||||
|
|
||||||
|
在 `SimpleEntity` 添加 `createdBy`, `modifiedBy`,配合 Spring Security 获取当前用户
|
||||||
|
|
||||||
|
### 自定义查询操作符
|
||||||
|
|
||||||
|
1. `Query.Queryable` 添加字段
|
||||||
|
2. `QueryParser` 添加抽象方法
|
||||||
|
3. `JpaQueryParser` 实现转换为 Predicate
|
||||||
|
|
||||||
|
## 测试
|
||||||
|
|
||||||
|
- H2 内存数据库
|
||||||
|
- `@DataJpaTest` 测试 Repository
|
||||||
|
- `@WebMvcTest` 测试 Controller
|
||||||
|
- 测试用例:`src/test/java/.../integration/`
|
||||||
|
|
||||||
|
## 依赖
|
||||||
|
|
||||||
|
核心:spring-boot-starter-data-jpa, fenix-spring-boot-starter:4.0.0, querydsl-jpa:7.1, mapstruct:1.6.3
|
||||||
|
|
||||||
|
传递:spring-boot-service-template-common
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 事务:写操作 `@Transactional(rollbackFor = Throwable.class)`,读操作 `@Transactional(readOnly = true)`
|
||||||
|
- 异常:不使用全局处理器,直接抛出
|
||||||
|
- 性能:批量操作使用批量方法
|
||||||
|
- 线程安全:GlobalResponse 用 record 保证不可变,SnowflakeHelper 用原子变量
|
||||||
409
docs/database-usage.md
Normal file
409
docs/database-usage.md
Normal file
@@ -0,0 +1,409 @@
|
|||||||
|
# Database 模块使用指南
|
||||||
|
|
||||||
|
单表 CRUD → REST 接口快速实现框架。
|
||||||
|
|
||||||
|
## 使用模式
|
||||||
|
|
||||||
|
### Web 应用
|
||||||
|
|
||||||
|
引入 database 模块,创建 Entity → Repository → Service → Controller,实现 REST 接口。
|
||||||
|
|
||||||
|
### 非 Web 应用
|
||||||
|
|
||||||
|
**无需引入 web 依赖**(database 模块的 `spring-boot-starter-web` scope 为 `provided`)。
|
||||||
|
|
||||||
|
仅使用 Entity → Repository → Service,直接注入 Service 使用:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@SpringBootApplication
|
||||||
|
public class Application implements CommandLineRunner {
|
||||||
|
@Autowired
|
||||||
|
private EmployeeService service;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run(String... args) {
|
||||||
|
Employee emp = new Employee();
|
||||||
|
emp.setName("张三");
|
||||||
|
Long id = service.save(emp);
|
||||||
|
Employee found = service.detail(id);
|
||||||
|
List<Employee> list = service.list();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
适用:批处理、定时任务、数据迁移、命令行应用、后台服务。
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 1. 添加依赖
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.lanyuanxiaoyao</groupId>
|
||||||
|
<artifactId>spring-boot-service-template-database</artifactId>
|
||||||
|
</dependency>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 创建实体
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Getter @Setter
|
||||||
|
@ToString(callSuper = true)
|
||||||
|
@FieldNameConstants
|
||||||
|
@Entity
|
||||||
|
@Table(name = "employee")
|
||||||
|
public class Employee extends SimpleEntity {
|
||||||
|
@Column(comment = "员工姓名", nullable = false)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Column(comment = "部门ID")
|
||||||
|
private Long departmentId;
|
||||||
|
|
||||||
|
@Column(comment = "邮箱")
|
||||||
|
private String email;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
继承 `SimpleEntity` 自动获得 `id`(雪花算法)、`createdTime`、`modifiedTime`(自动填充)。
|
||||||
|
|
||||||
|
### 3. 创建 Repository
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Repository
|
||||||
|
public interface EmployeeRepository extends SimpleRepository<Employee> {}
|
||||||
|
```
|
||||||
|
|
||||||
|
继承能力:CRUD、分页、Specification、QueryDSL、Example。
|
||||||
|
|
||||||
|
### 4. 创建 Service
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class EmployeeService extends SimpleServiceSupport<Employee> {
|
||||||
|
public EmployeeService(EmployeeRepository repository) {
|
||||||
|
super(repository);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
自动获得完整 CRUD 能力。
|
||||||
|
|
||||||
|
### 5. 创建 Controller(Web 应用)
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("employee")
|
||||||
|
public class EmployeeController
|
||||||
|
extends SimpleControllerSupport<Employee, EmployeeController.SaveItem, EmployeeController.ListItem, EmployeeController.DetailItem> {
|
||||||
|
|
||||||
|
private final EmployeeService service;
|
||||||
|
|
||||||
|
public EmployeeController(EmployeeService service) {
|
||||||
|
super(service);
|
||||||
|
this.service = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Function<SaveItem, Employee> saveItemMapper() {
|
||||||
|
return item -> {
|
||||||
|
Employee entity = new Employee();
|
||||||
|
entity.setId(item.id());
|
||||||
|
entity.setName(item.name());
|
||||||
|
entity.setDepartmentId(item.departmentId());
|
||||||
|
entity.setEmail(item.email());
|
||||||
|
return entity;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Function<Employee, ListItem> listItemMapper() {
|
||||||
|
return entity -> new ListItem(
|
||||||
|
entity.getId(), entity.getName(), entity.getDepartmentId(),
|
||||||
|
entity.getEmail(), entity.getCreatedTime()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Function<Employee, DetailItem> detailItemMapper() {
|
||||||
|
return entity -> new DetailItem(
|
||||||
|
entity.getId(), entity.getName(), entity.getDepartmentId(),
|
||||||
|
entity.getEmail(), entity.getCreatedTime(), entity.getModifiedTime()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record SaveItem(Long id, String name, Long departmentId, String email) {}
|
||||||
|
public record ListItem(Long id, String name, Long departmentId, String email, LocalDateTime createdTime) {}
|
||||||
|
public record DetailItem(Long id, String name, Long departmentId, String email, LocalDateTime createdTime, LocalDateTime modifiedTime) {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
实现三个 Mapper:`saveItemMapper()`, `listItemMapper()`, `detailItemMapper()`
|
||||||
|
|
||||||
|
## 代码生成
|
||||||
|
|
||||||
|
```java
|
||||||
|
DatabaseHelper.generateBasicFiles(
|
||||||
|
"com.example.entity", // 实体包
|
||||||
|
"com.example", // 项目根包
|
||||||
|
"./src/main/java/com/example", // 源码路径
|
||||||
|
false // 是否覆盖
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
生成 Repository、Service、Controller。
|
||||||
|
|
||||||
|
## API 接口(Web 应用)
|
||||||
|
|
||||||
|
### POST /{entity}/save
|
||||||
|
|
||||||
|
保存/更新实体。
|
||||||
|
|
||||||
|
请求(新增):
|
||||||
|
```json
|
||||||
|
{"name": "张三", "departmentId": 1, "email": "zhangsan@example.com"}
|
||||||
|
```
|
||||||
|
|
||||||
|
请求(更新):
|
||||||
|
```json
|
||||||
|
{"id": 123456789, "name": "李四"}
|
||||||
|
```
|
||||||
|
|
||||||
|
响应:
|
||||||
|
```json
|
||||||
|
{"status": 0, "message": "OK", "data": 123456789}
|
||||||
|
```
|
||||||
|
|
||||||
|
特性:不传 id 为新增,传 id 为更新(仅更新非 null 字段)。
|
||||||
|
|
||||||
|
### GET/POST /{entity}/list
|
||||||
|
|
||||||
|
GET:获取全部列表
|
||||||
|
|
||||||
|
POST:条件查询
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"query": {
|
||||||
|
"equal": {"departmentId": 1},
|
||||||
|
"like": {"name": "%张%"},
|
||||||
|
"greatEqual": {"createdTime": "2026-01-01 00:00:00"}
|
||||||
|
},
|
||||||
|
"sort": [{"column": "createdTime", "direction": "DESC"}],
|
||||||
|
"page": {"index": 1, "size": 20}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
响应:
|
||||||
|
```json
|
||||||
|
{"status": 0, "message": "OK", "data": {"items": [...], "total": 100}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GET /{entity}/detail/{id}
|
||||||
|
|
||||||
|
响应:
|
||||||
|
```json
|
||||||
|
{"status": 0, "message": "OK", "data": {"id": 123, "name": "张三", ...}}
|
||||||
|
```
|
||||||
|
|
||||||
|
ID 不存在返回 500。
|
||||||
|
|
||||||
|
### GET /{entity}/remove/{id}
|
||||||
|
|
||||||
|
响应:
|
||||||
|
```json
|
||||||
|
{"status": 0, "message": "OK", "data": null}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 查询条件
|
||||||
|
|
||||||
|
### Query 结构
|
||||||
|
|
||||||
|
```java
|
||||||
|
Query(
|
||||||
|
query: Queryable, // 查询条件
|
||||||
|
sort: List<Sortable>, // 排序
|
||||||
|
page: Pageable // 分页
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 查询操作
|
||||||
|
|
||||||
|
| 操作 | 类型 | 示例 |
|
||||||
|
|---|---|---|
|
||||||
|
| equal | Map | `{"name": "张三"}` |
|
||||||
|
| notEqual | Map | `{"status": "DELETED"}` |
|
||||||
|
| like | Map | `{"name": "%张%"}` |
|
||||||
|
| contain | Map | `{"name": "张"}` → `%张%` |
|
||||||
|
| startWith | Map | `{"name": "张"}` → `张%` |
|
||||||
|
| endWith | Map | `{"name": "三"}` → `%三` |
|
||||||
|
| great/greatEqual | Map | `{"age": 18}` |
|
||||||
|
| less/lessEqual | Map | `{"age": 60}` |
|
||||||
|
| between | Map | `{"age": {"start": 18, "end": 60}}` |
|
||||||
|
| inside | Map | `{"id": [1, 2, 3]}` |
|
||||||
|
| notInside | Map | `{"status": ["DELETED"]}` |
|
||||||
|
| nullEqual | List | `["deletedAt"]` |
|
||||||
|
| notNullEqual | List | `["email"]` |
|
||||||
|
|
||||||
|
### 排序
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"sort": [{"column": "createdTime", "direction": "DESC"}]}
|
||||||
|
```
|
||||||
|
|
||||||
|
direction: `ASC` 升序,`DESC` 降序
|
||||||
|
|
||||||
|
### 分页
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"page": {"index": 1, "size": 20}}
|
||||||
|
```
|
||||||
|
|
||||||
|
index 从 1 开始,默认 `(1, 10)`,无排序默认 `createdTime DESC`
|
||||||
|
|
||||||
|
## 高级用法
|
||||||
|
|
||||||
|
### 扩展 Service
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Service
|
||||||
|
public class EmployeeService extends SimpleServiceSupport<Employee> {
|
||||||
|
private final EmployeeRepository repository;
|
||||||
|
|
||||||
|
public EmployeeService(EmployeeRepository repository) {
|
||||||
|
super(repository);
|
||||||
|
this.repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义方法
|
||||||
|
public List<Employee> findByDepartmentId(Long departmentId) {
|
||||||
|
return repository.findAll(
|
||||||
|
(root, query, builder) -> builder.equal(root.get("departmentId"), departmentId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全局过滤条件
|
||||||
|
@Override
|
||||||
|
protected Predicate commonPredicates(Root<Employee> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
|
||||||
|
return builder.equal(root.get("deleted"), false); // 软删除过滤
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### QueryDSL 查询
|
||||||
|
|
||||||
|
```java
|
||||||
|
public List<Employee> findActiveEmployees() {
|
||||||
|
QEmployee q = QEmployee.employee;
|
||||||
|
return repository.findAll(q.status.eq("ACTIVE").and(q.deleted.isFalse()));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### MapStruct Mapper
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Mapper
|
||||||
|
public interface EmployeeMapper {
|
||||||
|
Employee toEntity(SaveItem item);
|
||||||
|
ListItem toListItem(Employee entity);
|
||||||
|
DetailItem toDetailItem(Employee entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Controller 中使用
|
||||||
|
@Override
|
||||||
|
protected Function<SaveItem, Employee> saveItemMapper() {
|
||||||
|
return mapper::toEntity;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 实体设计
|
||||||
|
|
||||||
|
### 字段类型
|
||||||
|
|
||||||
|
- ID: `Long`(雪花算法)
|
||||||
|
- 时间: `LocalDateTime`
|
||||||
|
- 金额: `BigDecimal`
|
||||||
|
- 枚举: Java enum(存储为字符串)
|
||||||
|
- 布尔: `Boolean`
|
||||||
|
|
||||||
|
### 关联关系
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Entity
|
||||||
|
public class Order extends SimpleEntity {
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "customer_id")
|
||||||
|
private Customer customer;
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
|
||||||
|
private List<OrderItem> items;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
注意:谨慎使用 `@OneToMany`,可能导致 N+1 问题。
|
||||||
|
|
||||||
|
### 索引
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Entity
|
||||||
|
@Table(name = "employee", indexes = {
|
||||||
|
@Index(name = "idx_department", columnList = "department_id")
|
||||||
|
})
|
||||||
|
public class Employee extends SimpleEntity { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
## 工具类
|
||||||
|
|
||||||
|
### DatabaseHelper
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 生成 DDL
|
||||||
|
DatabaseHelper.generateDDL(
|
||||||
|
"com.example.entity", "./sql", MySQL8Dialect.class,
|
||||||
|
"jdbc:mysql://localhost:3306/test", "root", "password",
|
||||||
|
com.mysql.cj.jdbc.Driver.class
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### SnowflakeHelper
|
||||||
|
|
||||||
|
```java
|
||||||
|
Long id = SnowflakeHelper.next();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
**Q: 非 Web 应用如何使用?**
|
||||||
|
|
||||||
|
A: 不引入 web 依赖,创建 Entity → Repository → Service,直接注入 Service 使用。
|
||||||
|
|
||||||
|
**Q: 如何实现软删除?**
|
||||||
|
|
||||||
|
A: 添加 `deleted` 字段,重写 `commonPredicates()` 过滤,覆盖 `remove()` 改为更新。
|
||||||
|
|
||||||
|
**Q: 如何处理复杂查询?**
|
||||||
|
|
||||||
|
A: 使用 QueryDSL 或 Repository `@Query` 方法:
|
||||||
|
```java
|
||||||
|
@Query("SELECT e FROM Employee e WHERE e.departmentId = :deptId")
|
||||||
|
List<Employee> findByDepartment(@Param("deptId") Long deptId);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Q: 如何批量插入?**
|
||||||
|
|
||||||
|
A: `service.save(entities)` 或 `repository.saveAll(entities)`
|
||||||
|
|
||||||
|
**Q: 查询条件支持关联对象吗?**
|
||||||
|
|
||||||
|
A: 支持,使用多级路径如 `"department.name"`
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
1. DTO 设计:SaveItem 可修改字段,ListItem 列表字段,DetailItem 完整字段
|
||||||
|
2. 事务:Service 方法已加事务,无需重复
|
||||||
|
3. 性能:列表查询避免关联对象,使用投影或 DTO
|
||||||
|
4. 代码生成:初期脚手架生成,后期手动调整
|
||||||
|
|
||||||
|
## 测试用例
|
||||||
|
|
||||||
|
`src/test/java/.../integration/`
|
||||||
14
openspec/config.yaml
Normal file
14
openspec/config.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
|
||||||
|
context: |
|
||||||
|
- **优先阅读README.md**获取项目结构与开发规范,所有代码风格、命名、注解、依赖、API等规范以README为准
|
||||||
|
- 新增代码优先复用已有组件、工具、依赖库,不引入新依赖
|
||||||
|
- 涉及模块结构、API、实体等变更时同步更新README.md
|
||||||
|
- Git提交: 仅中文; 格式"类型: 简短描述", 类型: feat/fix/refactor/docs/style/test/chore; 多行描述空行后写详细说明
|
||||||
|
- 禁止创建git操作task
|
||||||
|
- 积极使用subagents精心设计并行任务,节省上下文空间,加速任务执行
|
||||||
|
- 优先使用提问工具对用户进行提问
|
||||||
|
|
||||||
|
rules:
|
||||||
|
proposal:
|
||||||
|
- 仔细审查每一个过往spec判断是否存在Modified Capabilities
|
||||||
31
pom.xml
31
pom.xml
@@ -11,11 +11,7 @@
|
|||||||
|
|
||||||
<modules>
|
<modules>
|
||||||
<module>spring-boot-service-template-common</module>
|
<module>spring-boot-service-template-common</module>
|
||||||
<module>spring-boot-service-template-database/spring-boot-service-template-database-common</module>
|
<module>spring-boot-service-template-database</module>
|
||||||
<module>spring-boot-service-template-database/spring-boot-service-template-database-common-test</module>
|
|
||||||
<module>spring-boot-service-template-database/spring-boot-service-template-database-eq</module>
|
|
||||||
<module>spring-boot-service-template-database/spring-boot-service-template-database-jpa</module>
|
|
||||||
<module>spring-boot-service-template-database/spring-boot-service-template-database-xbatis</module>
|
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
@@ -31,8 +27,6 @@
|
|||||||
<mapstruct.version>1.6.3</mapstruct.version>
|
<mapstruct.version>1.6.3</mapstruct.version>
|
||||||
<mapstruct-plus.version>1.5.0</mapstruct-plus.version>
|
<mapstruct-plus.version>1.5.0</mapstruct-plus.version>
|
||||||
<datasource-decorator.version>2.0.0</datasource-decorator.version>
|
<datasource-decorator.version>2.0.0</datasource-decorator.version>
|
||||||
<easy-query.version>3.1.68</easy-query.version>
|
|
||||||
<xbatis.version>1.9.7-spring-boot4</xbatis.version>
|
|
||||||
|
|
||||||
<hutool.version>5.8.43</hutool.version>
|
<hutool.version>5.8.43</hutool.version>
|
||||||
</properties>
|
</properties>
|
||||||
@@ -57,12 +51,7 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.lanyuanxiaoyao</groupId>
|
<groupId>com.lanyuanxiaoyao</groupId>
|
||||||
<artifactId>spring-boot-service-template-database-common</artifactId>
|
<artifactId>spring-boot-service-template-database</artifactId>
|
||||||
<version>${project.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.lanyuanxiaoyao</groupId>
|
|
||||||
<artifactId>spring-boot-service-template-database-common-test</artifactId>
|
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
@@ -117,22 +106,6 @@
|
|||||||
<version>${hibernate.version}</version>
|
<version>${hibernate.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- xbatis -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>cn.xbatis</groupId>
|
|
||||||
<artifactId>xbatis-spring-boot-parent</artifactId>
|
|
||||||
<version>${xbatis.version}</version>
|
|
||||||
<type>pom</type>
|
|
||||||
<scope>import</scope>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- easy-query -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.easy-query</groupId>
|
|
||||||
<artifactId>sql-springboot4-starter</artifactId>
|
|
||||||
<version>3.1.68</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jspecify</groupId>
|
<groupId>org.jspecify</groupId>
|
||||||
<artifactId>jspecify</artifactId>
|
<artifactId>jspecify</artifactId>
|
||||||
|
|||||||
@@ -11,6 +11,14 @@
|
|||||||
|
|
||||||
<artifactId>spring-boot-service-template-common</artifactId>
|
<artifactId>spring-boot-service-template-common</artifactId>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
<plugin>
|
<plugin>
|
||||||
|
|||||||
@@ -0,0 +1,263 @@
|
|||||||
|
package com.lanyuanxiaoyao.service.template.common.helper;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
@DisplayName("ObjectHelper Tests")
|
||||||
|
class ObjectHelperTest {
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("isNull/isNotNull")
|
||||||
|
class NullTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("isNull with null object returns true")
|
||||||
|
void isNull_withNull_returnsTrue() {
|
||||||
|
assertThat(ObjectHelper.isNull(null)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("isNull with non-null object returns false")
|
||||||
|
void isNull_withNonNull_returnsFalse() {
|
||||||
|
assertThat(ObjectHelper.isNull(new Object())).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("isNotNull with null object returns false")
|
||||||
|
void isNotNull_withNull_returnsFalse() {
|
||||||
|
assertThat(ObjectHelper.isNotNull(null)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("isNotNull with non-null object returns true")
|
||||||
|
void isNotNull_withNonNull_returnsTrue() {
|
||||||
|
assertThat(ObjectHelper.isNotNull(new Object())).isTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("isEmpty/isNotEmpty")
|
||||||
|
class EmptyTests {
|
||||||
|
|
||||||
|
static Stream<Arguments> emptyObjects() {
|
||||||
|
return Stream.of(
|
||||||
|
Arguments.of("null", null, true),
|
||||||
|
Arguments.of("empty collection", Collections.emptyList(), true),
|
||||||
|
Arguments.of("non-empty collection", List.of("a"), false),
|
||||||
|
Arguments.of("empty map", Collections.emptyMap(), true),
|
||||||
|
Arguments.of("non-empty map", Map.of("key", "value"), false),
|
||||||
|
Arguments.of("empty string", "", true),
|
||||||
|
Arguments.of("non-empty string", "text", false),
|
||||||
|
Arguments.of("empty char sequence", new StringBuilder(), true),
|
||||||
|
Arguments.of("non-empty char sequence", new StringBuilder("text"), false),
|
||||||
|
Arguments.of("empty object array", new Object[]{}, true),
|
||||||
|
Arguments.of("non-empty object array", new Object[]{"a"}, false),
|
||||||
|
Arguments.of("empty byte array", new byte[]{}, true),
|
||||||
|
Arguments.of("non-empty byte array", new byte[]{1}, false),
|
||||||
|
Arguments.of("empty short array", new short[]{}, true),
|
||||||
|
Arguments.of("non-empty short array", new short[]{1}, false),
|
||||||
|
Arguments.of("empty int array", new int[]{}, true),
|
||||||
|
Arguments.of("non-empty int array", new int[]{1}, false),
|
||||||
|
Arguments.of("empty long array", new long[]{}, true),
|
||||||
|
Arguments.of("non-empty long array", new long[]{1L}, false),
|
||||||
|
Arguments.of("empty float array", new float[]{}, true),
|
||||||
|
Arguments.of("non-empty float array", new float[]{1.0f}, false),
|
||||||
|
Arguments.of("empty double array", new double[]{}, true),
|
||||||
|
Arguments.of("non-empty double array", new double[]{1.0}, false),
|
||||||
|
Arguments.of("empty char array", new char[]{}, true),
|
||||||
|
Arguments.of("non-empty char array", new char[]{'a'}, false),
|
||||||
|
Arguments.of("empty boolean array", new boolean[]{}, true),
|
||||||
|
Arguments.of("non-empty boolean array", new boolean[]{true}, false),
|
||||||
|
Arguments.of("empty optional", Optional.empty(), true),
|
||||||
|
Arguments.of("non-empty optional", Optional.of("value"), false),
|
||||||
|
Arguments.of("non-empty object", new Object(), false)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest(name = "{0}")
|
||||||
|
@MethodSource("emptyObjects")
|
||||||
|
@DisplayName("isEmpty returns correct result")
|
||||||
|
void isEmpty_withVariousObjects_returnsCorrectResult(String description, Object obj, boolean expected) {
|
||||||
|
assertThat(ObjectHelper.isEmpty(obj)).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest(name = "{0}")
|
||||||
|
@MethodSource("emptyObjects")
|
||||||
|
@DisplayName("isNotEmpty returns opposite of isEmpty")
|
||||||
|
void isNotEmpty_withVariousObjects_returnsCorrectResult(String description, Object obj, boolean expected) {
|
||||||
|
assertThat(ObjectHelper.isNotEmpty(obj)).isEqualTo(!expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("defaultIfNull")
|
||||||
|
class DefaultIfNullTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("defaultIfNull with null returns default value")
|
||||||
|
void defaultIfNull_withNull_returnsDefault() {
|
||||||
|
assertThat(ObjectHelper.defaultIfNull(null, "default")).isEqualTo("default");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("defaultIfNull with non-null returns original value")
|
||||||
|
void defaultIfNull_withNonNull_returnsOriginal() {
|
||||||
|
assertThat(ObjectHelper.defaultIfNull("value", "default")).isEqualTo("value");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("defaultIfNull with null and null default returns null")
|
||||||
|
void defaultIfNull_withBothNull_returnsNull() {
|
||||||
|
String result = ObjectHelper.defaultIfNull(null, null);
|
||||||
|
assertThat(result).isNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("isComparable")
|
||||||
|
class ComparableTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("isComparable with enum class returns true")
|
||||||
|
void isComparable_withEnum_returnsTrue() {
|
||||||
|
assertThat(ObjectHelper.isComparable(TestEnum.class)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("isComparable with String class returns true")
|
||||||
|
void isComparable_withString_returnsTrue() {
|
||||||
|
assertThat(ObjectHelper.isComparable(String.class)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("isComparable with Integer class returns true")
|
||||||
|
void isComparable_withInteger_returnsTrue() {
|
||||||
|
assertThat(ObjectHelper.isComparable(Integer.class)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("isComparable with primitive class returns true")
|
||||||
|
void isComparable_withPrimitive_returnsTrue() {
|
||||||
|
assertThat(ObjectHelper.isComparable(int.class)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("isComparable with non-comparable class returns false")
|
||||||
|
void isComparable_withNonComparable_returnsFalse() {
|
||||||
|
assertThat(ObjectHelper.isComparable(Object.class)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("isComparable with null class returns false")
|
||||||
|
void isComparable_withNull_returnsFalse() {
|
||||||
|
Class<?> nullClass = null;
|
||||||
|
assertThat(ObjectHelper.isComparable(nullClass)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("isComparable with enum object returns true")
|
||||||
|
void isComparable_withEnumObject_returnsTrue() {
|
||||||
|
assertThat(ObjectHelper.isComparable(TestEnum.VALUE)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("isComparable with null object returns false")
|
||||||
|
void isComparable_withNullObject_returnsFalse() {
|
||||||
|
Object nullObj = null;
|
||||||
|
assertThat(ObjectHelper.isComparable(nullObj)).isFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("isCollection")
|
||||||
|
class CollectionTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("isCollection with List class returns true")
|
||||||
|
void isCollection_withList_returnsTrue() {
|
||||||
|
assertThat(ObjectHelper.isCollection(List.class)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("isCollection with Set class returns true")
|
||||||
|
void isCollection_withSet_returnsTrue() {
|
||||||
|
assertThat(ObjectHelper.isCollection(Set.class)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("isCollection with non-collection class returns false")
|
||||||
|
void isCollection_withNonCollection_returnsFalse() {
|
||||||
|
assertThat(ObjectHelper.isCollection(String.class)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("isCollection with null class returns false")
|
||||||
|
void isCollection_withNull_returnsFalse() {
|
||||||
|
Class<?> nullClass = null;
|
||||||
|
assertThat(ObjectHelper.isCollection(nullClass)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("isCollection with ArrayList object returns true")
|
||||||
|
void isCollection_withArrayListObject_returnsTrue() {
|
||||||
|
assertThat(ObjectHelper.isCollection(new ArrayList<>())).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("isCollection with null object returns false")
|
||||||
|
void isCollection_withNullObject_returnsFalse() {
|
||||||
|
Object nullObj = null;
|
||||||
|
assertThat(ObjectHelper.isCollection(nullObj)).isFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("isString")
|
||||||
|
class StringTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("isString with String class returns true")
|
||||||
|
void isString_withStringClass_returnsTrue() {
|
||||||
|
assertThat(ObjectHelper.isString(String.class)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("isString with non-string class returns false")
|
||||||
|
void isString_withNonStringClass_returnsFalse() {
|
||||||
|
assertThat(ObjectHelper.isString(Integer.class)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("isString with null class returns false")
|
||||||
|
void isString_withNullClass_returnsFalse() {
|
||||||
|
Class<?> nullClass = null;
|
||||||
|
assertThat(ObjectHelper.isString(nullClass)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("isString with string object returns true")
|
||||||
|
void isString_withStringObject_returnsTrue() {
|
||||||
|
assertThat(ObjectHelper.isString("text")).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("isString with null object returns false")
|
||||||
|
void isString_withNullObject_returnsFalse() {
|
||||||
|
Object nullObj = null;
|
||||||
|
assertThat(ObjectHelper.isString(nullObj)).isFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum TestEnum {
|
||||||
|
VALUE
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,12 +9,12 @@
|
|||||||
<version>1.1.0-SNAPSHOT</version>
|
<version>1.1.0-SNAPSHOT</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>spring-boot-service-template-database-jpa</artifactId>
|
<artifactId>spring-boot-service-template-database</artifactId>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.lanyuanxiaoyao</groupId>
|
<groupId>com.lanyuanxiaoyao</groupId>
|
||||||
<artifactId>spring-boot-service-template-database-common</artifactId>
|
<artifactId>spring-boot-service-template-common</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -35,15 +35,37 @@
|
|||||||
<groupId>io.github.openfeign.querydsl</groupId>
|
<groupId>io.github.openfeign.querydsl</groupId>
|
||||||
<artifactId>querydsl-jpa</artifactId>
|
<artifactId>querydsl-jpa</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.hibernate.orm</groupId>
|
<groupId>org.hibernate.orm</groupId>
|
||||||
<artifactId>hibernate-ant</artifactId>
|
<artifactId>hibernate-ant</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.lanyuanxiaoyao</groupId>
|
<groupId>org.mapstruct</groupId>
|
||||||
<artifactId>spring-boot-service-template-database-common-test</artifactId>
|
<artifactId>mapstruct</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-data-jpa-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-webmvc-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.h2database</groupId>
|
||||||
|
<artifactId>h2</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.gavlyukovskiy</groupId>
|
||||||
|
<artifactId>p6spy-spring-boot-starter</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
@@ -87,6 +109,11 @@
|
|||||||
<artifactId>jakarta.persistence-api</artifactId>
|
<artifactId>jakarta.persistence-api</artifactId>
|
||||||
<version>3.2.0</version>
|
<version>3.2.0</version>
|
||||||
</path>
|
</path>
|
||||||
|
<path>
|
||||||
|
<groupId>org.mapstruct</groupId>
|
||||||
|
<artifactId>mapstruct-processor</artifactId>
|
||||||
|
<version>${mapstruct.version}</version>
|
||||||
|
</path>
|
||||||
</annotationProcessorPaths>
|
</annotationProcessorPaths>
|
||||||
<compilerArgs>
|
<compilerArgs>
|
||||||
<arg>-Aquerydsl.entityAccessors=true</arg>
|
<arg>-Aquerydsl.entityAccessors=true</arg>
|
||||||
@@ -94,6 +121,10 @@
|
|||||||
</compilerArgs>
|
</compilerArgs>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
</project>
|
</project>
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
||||||
<modelVersion>4.0.0</modelVersion>
|
|
||||||
<parent>
|
|
||||||
<groupId>com.lanyuanxiaoyao</groupId>
|
|
||||||
<artifactId>spring-boot-service-template</artifactId>
|
|
||||||
<version>1.1.0-SNAPSHOT</version>
|
|
||||||
</parent>
|
|
||||||
|
|
||||||
<artifactId>spring-boot-service-template-database-common-test</artifactId>
|
|
||||||
|
|
||||||
<dependencies>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-jdbc</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-test</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.mapstruct</groupId>
|
|
||||||
<artifactId>mapstruct</artifactId>
|
|
||||||
<scope>compile</scope>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.github.gavlyukovskiy</groupId>
|
|
||||||
<artifactId>p6spy-spring-boot-starter</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.h2database</groupId>
|
|
||||||
<artifactId>h2</artifactId>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
</project>
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.common.test;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Random;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.http.HttpEntity;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.util.Assert;
|
|
||||||
import org.springframework.web.client.RestTemplate;
|
|
||||||
import tools.jackson.databind.JsonNode;
|
|
||||||
import tools.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
public class AbstractTestApplication {
|
|
||||||
private static final String BASE_URL = "http://localhost:2490";
|
|
||||||
private static final RestTemplate REST_CLIENT = new RestTemplate();
|
|
||||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
|
||||||
private static final Random random = new Random();
|
|
||||||
private static final String randomChars = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM";
|
|
||||||
|
|
||||||
protected void testCrud() {
|
|
||||||
formatLog("Save");
|
|
||||||
var cid1 = saveItem("company", randomCompany("Apple")).get("data").asLong();
|
|
||||||
var cid2 = saveItem("company", randomCompany()).get("data").asLong();
|
|
||||||
var cid3 = saveItem("company", randomCompany()).get("data").asLong();
|
|
||||||
|
|
||||||
formatLog("List");
|
|
||||||
assertListItems(listItems("company"), 3, 3);
|
|
||||||
|
|
||||||
formatLog("Detail");
|
|
||||||
var company1 = detailItem("company", cid1);
|
|
||||||
Assert.isTrue(cid1 == company1.at("/data/id").asLong(), "id错误");
|
|
||||||
Assert.isTrue(company1.at("/data/name").asString("").startsWith("Apple"), "name错误");
|
|
||||||
|
|
||||||
var company2 = detailItem("company", cid2);
|
|
||||||
Assert.isTrue(cid2 == company2.at("/data/id").asLong(), "id错误");
|
|
||||||
|
|
||||||
var company3 = detailItem("company", cid3);
|
|
||||||
Assert.isTrue(cid3 == company3.at("/data/id").asLong(), "id错误");
|
|
||||||
|
|
||||||
formatLog("List Page");
|
|
||||||
// language=JSON
|
|
||||||
var pageRequest = """
|
|
||||||
{
|
|
||||||
"page": {
|
|
||||||
"index": 1,
|
|
||||||
"size": 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
assertListItems(listItems("company", pageRequest), 2, 3);
|
|
||||||
|
|
||||||
formatLog("List Queryable");
|
|
||||||
// language=JSON
|
|
||||||
var queryRequest = """
|
|
||||||
{
|
|
||||||
"query": {
|
|
||||||
"notNullEqual": [
|
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"equal": {
|
|
||||||
"name": "Apple"
|
|
||||||
},
|
|
||||||
"like": {
|
|
||||||
"name": "Appl%"
|
|
||||||
},
|
|
||||||
"contain": {
|
|
||||||
"name": "ple"
|
|
||||||
},
|
|
||||||
"startWith": {
|
|
||||||
"name": "Appl"
|
|
||||||
},
|
|
||||||
"endWith": {
|
|
||||||
"name": "le"
|
|
||||||
},
|
|
||||||
"less": {
|
|
||||||
"members": 100
|
|
||||||
},
|
|
||||||
"greatEqual": {
|
|
||||||
"members": 0,
|
|
||||||
"createdTime": "2025-01-01 00:00:00"
|
|
||||||
},
|
|
||||||
"inside": {
|
|
||||||
"name": [
|
|
||||||
"Apple",
|
|
||||||
"Banana"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"between": {
|
|
||||||
"members": {
|
|
||||||
"start": 0,
|
|
||||||
"end": 100
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"page": {
|
|
||||||
"index": 1,
|
|
||||||
"size": 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
assertListItems(listItems("company", queryRequest), 1, 1);
|
|
||||||
|
|
||||||
formatLog("Clean");
|
|
||||||
removeItem("company", cid1);
|
|
||||||
assertListItems(listItems("company"), 2, 2);
|
|
||||||
removeItem("company", cid2);
|
|
||||||
removeItem("company", cid3);
|
|
||||||
assertListItems(listItems("company"), 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void assertListItems(JsonNode node, int itemSizeTarget, int itemTotalTarget) {
|
|
||||||
var itemSize = node.at("/data/items").size();
|
|
||||||
var itemTotal = node.at("/data/total").asLong();
|
|
||||||
Assert.isTrue(itemSize == itemSizeTarget, "数量错误 (%d)".formatted(itemSize));
|
|
||||||
Assert.isTrue(itemTotal == itemTotalTarget, "分页总数错误 (%d)".formatted(itemTotal));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void formatLog(String text) {
|
|
||||||
log.info("===== {} =====", text);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Map<String, Object> randomCompany() {
|
|
||||||
return randomCompany(randomString(10));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Map<String, Object> randomCompany(String name) {
|
|
||||||
return Map.of(
|
|
||||||
"name", name,
|
|
||||||
"members", randomInt(100)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected String randomString(String prefix, int length) {
|
|
||||||
return prefix + randomString(length);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected String randomString(int length) {
|
|
||||||
var builder = new StringBuilder();
|
|
||||||
for (int i = 0; i < length; i++) {
|
|
||||||
builder.append(randomChars.charAt(random.nextInt(randomChars.length())));
|
|
||||||
}
|
|
||||||
return builder.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected String randomChar(String base) {
|
|
||||||
return base.charAt(randomInt(base.length())) + "";
|
|
||||||
}
|
|
||||||
|
|
||||||
protected int randomInt(int bound) {
|
|
||||||
return random.nextInt(1, bound);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected double randomDouble(int bound) {
|
|
||||||
return random.nextDouble(1, bound);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected HttpHeaders headers() {
|
|
||||||
var headers = new HttpHeaders();
|
|
||||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
|
||||||
return headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected JsonNode saveItem(String path, Object body) {
|
|
||||||
var response = REST_CLIENT.postForEntity(
|
|
||||||
"%s/%s/save".formatted(BASE_URL, path),
|
|
||||||
new HttpEntity<>(body, headers()),
|
|
||||||
String.class
|
|
||||||
);
|
|
||||||
Assert.isTrue(response.getStatusCode().is2xxSuccessful(), "请求失败");
|
|
||||||
Assert.notNull(response.getBody(), "请求失败");
|
|
||||||
return MAPPER.readTree(response.getBody());
|
|
||||||
}
|
|
||||||
|
|
||||||
protected JsonNode listItems(String path) {
|
|
||||||
var response = REST_CLIENT.getForEntity(
|
|
||||||
"%s/%s/list".formatted(BASE_URL, path),
|
|
||||||
String.class
|
|
||||||
);
|
|
||||||
Assert.isTrue(response.getStatusCode().is2xxSuccessful(), "请求失败");
|
|
||||||
Assert.notNull(response.getBody(), "请求失败");
|
|
||||||
return MAPPER.readTree(response.getBody());
|
|
||||||
}
|
|
||||||
|
|
||||||
protected JsonNode listItems(String path, String query) {
|
|
||||||
var response = REST_CLIENT.postForEntity(
|
|
||||||
"%s/%s/list".formatted(BASE_URL, path),
|
|
||||||
new HttpEntity<>(query, headers()),
|
|
||||||
String.class
|
|
||||||
);
|
|
||||||
Assert.isTrue(response.getStatusCode().is2xxSuccessful(), "请求失败");
|
|
||||||
Assert.notNull(response.getBody(), "请求失败");
|
|
||||||
return MAPPER.readTree(response.getBody());
|
|
||||||
}
|
|
||||||
|
|
||||||
protected JsonNode detailItem(String path, Long id) {
|
|
||||||
var response = REST_CLIENT.getForEntity(
|
|
||||||
"%s/%s/detail/%d".formatted(BASE_URL, path, id),
|
|
||||||
String.class
|
|
||||||
);
|
|
||||||
Assert.isTrue(response.getStatusCode().is2xxSuccessful(), "请求失败");
|
|
||||||
Assert.notNull(response.getBody(), "请求失败");
|
|
||||||
return MAPPER.readTree(response.getBody());
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void removeItem(String path, Long id) {
|
|
||||||
var response = REST_CLIENT.getForEntity(
|
|
||||||
"%s/%s/remove/%d".formatted(BASE_URL, path, id),
|
|
||||||
Void.class
|
|
||||||
);
|
|
||||||
Assert.isTrue(response.getStatusCode().is2xxSuccessful(), "请求失败");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
server:
|
|
||||||
port: 2490
|
|
||||||
decorator:
|
|
||||||
datasource:
|
|
||||||
p6spy:
|
|
||||||
multiline: false
|
|
||||||
exclude-categories:
|
|
||||||
- commit
|
|
||||||
- result
|
|
||||||
- resultset
|
|
||||||
- rollback
|
|
||||||
log-format: "%(category)|%(executionTime)|%(sqlSingleLine)"
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
||||||
<modelVersion>4.0.0</modelVersion>
|
|
||||||
<parent>
|
|
||||||
<groupId>com.lanyuanxiaoyao</groupId>
|
|
||||||
<artifactId>spring-boot-service-template</artifactId>
|
|
||||||
<version>1.1.0-SNAPSHOT</version>
|
|
||||||
</parent>
|
|
||||||
|
|
||||||
<artifactId>spring-boot-service-template-database-common</artifactId>
|
|
||||||
|
|
||||||
<dependencies>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.lanyuanxiaoyao</groupId>
|
|
||||||
<artifactId>spring-boot-service-template-common</artifactId>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
</project>
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
||||||
<modelVersion>4.0.0</modelVersion>
|
|
||||||
<parent>
|
|
||||||
<groupId>com.lanyuanxiaoyao</groupId>
|
|
||||||
<artifactId>spring-boot-service-template</artifactId>
|
|
||||||
<version>1.1.0-SNAPSHOT</version>
|
|
||||||
</parent>
|
|
||||||
|
|
||||||
<artifactId>spring-boot-service-template-database-eq</artifactId>
|
|
||||||
|
|
||||||
<dependencies>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.lanyuanxiaoyao</groupId>
|
|
||||||
<artifactId>spring-boot-service-template-database-common</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
|
||||||
<scope>provided</scope>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.easy-query</groupId>
|
|
||||||
<artifactId>sql-springboot4-starter</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.lanyuanxiaoyao</groupId>
|
|
||||||
<artifactId>spring-boot-service-template-database-common-test</artifactId>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
|
|
||||||
<build>
|
|
||||||
<plugins>
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-source-plugin</artifactId>
|
|
||||||
<executions>
|
|
||||||
<execution>
|
|
||||||
<phase>package</phase>
|
|
||||||
<goals>
|
|
||||||
<goal>jar-no-fork</goal>
|
|
||||||
</goals>
|
|
||||||
</execution>
|
|
||||||
</executions>
|
|
||||||
</plugin>
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
|
||||||
<configuration>
|
|
||||||
<annotationProcessorPaths>
|
|
||||||
<path>
|
|
||||||
<groupId>org.projectlombok</groupId>
|
|
||||||
<artifactId>lombok</artifactId>
|
|
||||||
</path>
|
|
||||||
<path>
|
|
||||||
<groupId>com.easy-query</groupId>
|
|
||||||
<artifactId>sql-processor</artifactId>
|
|
||||||
<version>${easy-query.version}</version>
|
|
||||||
</path>
|
|
||||||
</annotationProcessorPaths>
|
|
||||||
</configuration>
|
|
||||||
</plugin>
|
|
||||||
</plugins>
|
|
||||||
</build>
|
|
||||||
</project>
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.eq.controller;
|
|
||||||
|
|
||||||
import com.easy.query.core.proxy.AbstractProxyEntity;
|
|
||||||
import com.easy.query.core.proxy.ProxyEntityAvailable;
|
|
||||||
import com.lanyuanxiaoyao.service.template.common.helper.ObjectHelper;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.controller.SimpleController;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.entity.GlobalResponse;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.entity.Query;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.eq.entity.SimpleEntity;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.eq.service.SimpleServiceSupport;
|
|
||||||
import java.util.function.Function;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 简单控制器支持类,提供基础的CRUD操作实现
|
|
||||||
* <p>
|
|
||||||
* 该类实现了基本的增删改查功能,通过泛型支持不同类型的数据转换。
|
|
||||||
* 子类需要实现对应的Mapper函数来完成实体类与传输对象之间的转换。
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* <h3>设计特点</h3>
|
|
||||||
* <ul>
|
|
||||||
* <li>泛型设计,支持任意实体类型和数据转换</li>
|
|
||||||
* <li>统一的异常处理和事务管理</li>
|
|
||||||
* <li>支持条件查询、分页查询和详情查询</li>
|
|
||||||
* <li>提供抽象的Mapper方法,便于子类实现数据转换逻辑</li>
|
|
||||||
* </ul>
|
|
||||||
*
|
|
||||||
* <h3>使用说明</h3>
|
|
||||||
* <p>子类需要实现以下抽象方法:</p>
|
|
||||||
* <ul>
|
|
||||||
* <li>saveItemMapper(): 保存项到实体的转换函数</li>
|
|
||||||
* <li>listItemMapper(): 实体到列表项的转换函数</li>
|
|
||||||
* <li>detailItemMapper(): 实体到详情项的转换函数</li>
|
|
||||||
* </ul>
|
|
||||||
*
|
|
||||||
* @param <ENTITY> 实体类型,必须继承SimpleEntity
|
|
||||||
* @param <SAVE_ITEM> 保存项类型
|
|
||||||
* @param <LIST_ITEM> 列表项类型
|
|
||||||
* @param <DETAIL_ITEM> 详情项类型
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public abstract class SimpleControllerSupport<ENTITY extends SimpleEntity & ProxyEntityAvailable<ENTITY, PROXY>, PROXY extends AbstractProxyEntity<PROXY, ENTITY>, SAVE_ITEM, LIST_ITEM, DETAIL_ITEM> implements SimpleController<SAVE_ITEM, LIST_ITEM, DETAIL_ITEM> {
|
|
||||||
protected final SimpleServiceSupport<ENTITY, PROXY> service;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 保存实体对象
|
|
||||||
* <p>
|
|
||||||
* 将保存项转换为实体对象后保存,返回保存后的实体ID。
|
|
||||||
* 支持新增和更新操作,通过事务保证数据一致性。
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param item 需要保存的项
|
|
||||||
* @return 返回保存后的实体ID响应对象,格式:{status: 0, message: "OK", data: 实体ID}
|
|
||||||
* @throws Exception 保存过程中可能抛出的异常
|
|
||||||
*/
|
|
||||||
@Transactional(rollbackFor = Throwable.class)
|
|
||||||
@PostMapping(SAVE)
|
|
||||||
@Override
|
|
||||||
public GlobalResponse<Long> save(@RequestBody SAVE_ITEM item) throws Exception {
|
|
||||||
var mapper = saveItemMapper();
|
|
||||||
return GlobalResponse.responseSuccess(service.save(mapper.apply(item)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有实体列表
|
|
||||||
* <p>
|
|
||||||
* 查询所有记录,不带任何过滤条件,返回分页格式的数据。
|
|
||||||
* 将实体对象转换为列表项对象后返回。
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @return 返回实体列表响应对象,格式:{status: 0, message: "OK", data: {items: [...], total: total}}
|
|
||||||
* @throws Exception 查询过程中可能抛出的异常
|
|
||||||
*/
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
@GetMapping(LIST)
|
|
||||||
@Override
|
|
||||||
public GlobalResponse<GlobalResponse.ListItem<LIST_ITEM>> list() throws Exception {
|
|
||||||
var mapper = listItemMapper();
|
|
||||||
var result = service.list();
|
|
||||||
return GlobalResponse.responseListData(
|
|
||||||
result
|
|
||||||
.stream()
|
|
||||||
.map(entity -> {
|
|
||||||
try {
|
|
||||||
return mapper.apply(entity);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.toList(),
|
|
||||||
result.size()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据查询条件获取实体列表
|
|
||||||
* <p>
|
|
||||||
* 支持复杂的查询条件、排序和分页,返回符合条件的数据。
|
|
||||||
* 将实体对象转换为列表项对象后返回。
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param query 查询条件对象,包含过滤条件、排序规则和分页信息
|
|
||||||
* @return 返回符合条件的实体列表响应对象,格式:{status: 0, message: "OK", data: {items: [...], total: total}}
|
|
||||||
* @throws Exception 查询过程中可能抛出的异常
|
|
||||||
*/
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
@PostMapping(LIST)
|
|
||||||
@Override
|
|
||||||
public GlobalResponse<GlobalResponse.ListItem<LIST_ITEM>> list(@RequestBody Query query) throws Exception {
|
|
||||||
if (ObjectHelper.isNull(query)) {
|
|
||||||
return GlobalResponse.responseListData();
|
|
||||||
}
|
|
||||||
var mapper = listItemMapper();
|
|
||||||
var result = service.list(query);
|
|
||||||
return GlobalResponse.responseListData(
|
|
||||||
result.items()
|
|
||||||
.stream()
|
|
||||||
.map(entity -> {
|
|
||||||
try {
|
|
||||||
return mapper.apply(entity);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.toList(),
|
|
||||||
result.total()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据ID获取实体详情
|
|
||||||
* <p>
|
|
||||||
* 根据主键ID查询单条记录的详细信息,转换为详情项对象后返回。
|
|
||||||
* 如果记录不存在则抛出异常。
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param id 实体主键ID
|
|
||||||
* @return 返回实体详情响应对象,格式:{status: 0, message: "OK", data: 详情数据}
|
|
||||||
* @throws Exception 查询过程中可能抛出的异常
|
|
||||||
*/
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
@GetMapping(DETAIL)
|
|
||||||
@Override
|
|
||||||
public GlobalResponse<DETAIL_ITEM> detail(@PathVariable("id") Long id) throws Exception {
|
|
||||||
var mapper = detailItemMapper();
|
|
||||||
return GlobalResponse.responseSuccess(mapper.apply(service.detailOrThrow(id)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据ID删除实体对象
|
|
||||||
* <p>
|
|
||||||
* 根据主键ID删除指定的记录,执行成功后返回成功响应。
|
|
||||||
* 通过事务保证删除操作的一致性。
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param id 需要删除的实体主键ID
|
|
||||||
* @return 返回删除结果响应对象,格式:{status: 0, message: "OK", data: null}
|
|
||||||
* @throws Exception 删除过程中可能抛出的异常
|
|
||||||
*/
|
|
||||||
@Transactional(rollbackFor = Throwable.class)
|
|
||||||
@GetMapping(REMOVE)
|
|
||||||
@Override
|
|
||||||
public GlobalResponse<Object> remove(@PathVariable("id") Long id) throws Exception {
|
|
||||||
service.remove(id);
|
|
||||||
return GlobalResponse.responseSuccess();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 保存项映射器,将保存项转换为实体对象
|
|
||||||
* <p>
|
|
||||||
* 子类需要实现此方法,定义保存项到实体的转换逻辑。
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @return Function<SAVE_ITEM, ENTITY> 保存项到实体的转换函数
|
|
||||||
*/
|
|
||||||
protected abstract Function<SAVE_ITEM, ENTITY> saveItemMapper();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 列表项映射器,将实体对象转换为列表项
|
|
||||||
* <p>
|
|
||||||
* 子类需要实现此方法,定义实体到列表项的转换逻辑。
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @return Function<ENTITY, LIST_ITEM> 实体到列表项的转换函数
|
|
||||||
*/
|
|
||||||
protected abstract Function<ENTITY, LIST_ITEM> listItemMapper();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 详情项映射器,将实体对象转换为详情项
|
|
||||||
* <p>
|
|
||||||
* 子类需要实现此方法,定义实体到详情项的转换逻辑。
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @return Function<ENTITY, DETAIL_ITEM> 实体到详情项的转换函数
|
|
||||||
*/
|
|
||||||
protected abstract Function<ENTITY, DETAIL_ITEM> detailItemMapper();
|
|
||||||
|
|
||||||
public interface Mapper<S, T> {
|
|
||||||
T map(S source) throws Exception;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.eq.entity;
|
|
||||||
|
|
||||||
import com.easy.query.core.annotation.Column;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.ToString;
|
|
||||||
import lombok.experimental.FieldNameConstants;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@ToString
|
|
||||||
@FieldNameConstants
|
|
||||||
public class IdOnlyEntity {
|
|
||||||
@Column(primaryKey = true, primaryKeyGenerator = SnowflakeIdGenerator.class)
|
|
||||||
private Long id;
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.eq.entity;
|
|
||||||
|
|
||||||
import com.easy.query.core.annotation.LogicDelete;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.ToString;
|
|
||||||
import lombok.experimental.FieldNameConstants;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@ToString
|
|
||||||
@FieldNameConstants
|
|
||||||
public class LogicDeleteEntity extends IdOnlyEntity {
|
|
||||||
@LogicDelete
|
|
||||||
private Boolean deleted = false;
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.eq.entity;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.ToString;
|
|
||||||
import lombok.experimental.FieldNameConstants;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@ToString(callSuper = true)
|
|
||||||
@FieldNameConstants
|
|
||||||
public class SimpleEntity extends LogicDeleteEntity {
|
|
||||||
private LocalDateTime createdTime;
|
|
||||||
private LocalDateTime modifiedTime;
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.eq.entity;
|
|
||||||
|
|
||||||
import com.easy.query.core.basic.extension.generated.PrimaryKeyGenerator;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.helper.SnowflakeHelper;
|
|
||||||
import java.io.Serializable;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class SnowflakeIdGenerator implements PrimaryKeyGenerator {
|
|
||||||
@Override
|
|
||||||
public Serializable getPrimaryKey() {
|
|
||||||
return SnowflakeHelper.next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,280 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.eq.service;
|
|
||||||
|
|
||||||
import com.easy.query.api.proxy.client.EasyEntityQuery;
|
|
||||||
import com.easy.query.core.enums.SQLExecuteStrategyEnum;
|
|
||||||
import com.easy.query.core.proxy.AbstractProxyEntity;
|
|
||||||
import com.easy.query.core.proxy.ProxyEntityAvailable;
|
|
||||||
import com.lanyuanxiaoyao.service.template.common.helper.ObjectHelper;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.entity.Page;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.entity.Query;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.exception.IdNotFoundException;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.service.QueryParser;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.service.SimpleService;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.eq.entity.SimpleEntity;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.mapstruct.Named;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public abstract class SimpleServiceSupport<ENTITY extends SimpleEntity & ProxyEntityAvailable<ENTITY, PROXY>, PROXY extends AbstractProxyEntity<PROXY, ENTITY>> implements SimpleService<ENTITY> {
|
|
||||||
private static final int DEFAULT_PAGE_INDEX = 1;
|
|
||||||
private static final int DEFAULT_PAGE_SIZE = 10;
|
|
||||||
|
|
||||||
protected final EasyEntityQuery entityQuery;
|
|
||||||
private final Class<ENTITY> target;
|
|
||||||
|
|
||||||
@Transactional(rollbackFor = Throwable.class)
|
|
||||||
@Override
|
|
||||||
public Long save(ENTITY entity) {
|
|
||||||
if (ObjectHelper.isNull(entity.getId())) {
|
|
||||||
entityQuery.insertable(entity).executeRows();
|
|
||||||
} else {
|
|
||||||
entityQuery.updatable(entity)
|
|
||||||
.setSQLStrategy(SQLExecuteStrategyEnum.ONLY_NOT_NULL_COLUMNS)
|
|
||||||
.executeRows();
|
|
||||||
}
|
|
||||||
return entity.getId();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional(rollbackFor = Throwable.class)
|
|
||||||
@Override
|
|
||||||
public void save(Iterable<ENTITY> entities) {
|
|
||||||
var insertList = new ArrayList<ENTITY>();
|
|
||||||
var updateList = new ArrayList<ENTITY>();
|
|
||||||
for (var entity : entities) {
|
|
||||||
if (ObjectHelper.isNull(entity.getId())) {
|
|
||||||
insertList.add(entity);
|
|
||||||
} else {
|
|
||||||
updateList.add(entity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (ObjectHelper.isNotEmpty(insertList)) {
|
|
||||||
entityQuery.insertable(insertList).executeRows();
|
|
||||||
}
|
|
||||||
if (ObjectHelper.isNotEmpty(updateList)) {
|
|
||||||
entityQuery.updatable(updateList)
|
|
||||||
.setSQLStrategy(SQLExecuteStrategyEnum.ONLY_NOT_NULL_COLUMNS)
|
|
||||||
.executeRows();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Long count() {
|
|
||||||
return entityQuery.queryable(target).count();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<ENTITY> list() {
|
|
||||||
return entityQuery.queryable(target).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<ENTITY> list(Set<Long> ids) {
|
|
||||||
if (ObjectHelper.isEmpty(ids)) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
return entityQuery.queryable(target)
|
|
||||||
.whereByIds(ids)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void commonPredicates(PROXY proxy) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Page<ENTITY> list(Query query) {
|
|
||||||
var index = DEFAULT_PAGE_INDEX;
|
|
||||||
var size = DEFAULT_PAGE_SIZE;
|
|
||||||
if (ObjectHelper.isNotNull(query.page())) {
|
|
||||||
index = Math.max(ObjectHelper.defaultIfNull(query.page().index(), DEFAULT_PAGE_INDEX), 1);
|
|
||||||
size = Math.max(ObjectHelper.defaultIfNull(query.page().size(), DEFAULT_PAGE_SIZE), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = entityQuery.queryable(target)
|
|
||||||
.where(this::commonPredicates)
|
|
||||||
.where(proxy -> new EqQueryParser<ENTITY, PROXY>(query.query(), proxy).build())
|
|
||||||
.orderBy(ObjectHelper.isNotEmpty(query.sort()), proxy -> query.sort().forEach(sort -> proxy.anyColumn(sort.column()).orderBy(Query.Sortable.Direction.ASC.equals(sort.direction()))))
|
|
||||||
.toPageResult(index, size);
|
|
||||||
|
|
||||||
return new Page<>(result.getData(), result.getTotal());
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<ENTITY> detailOptional(Long id) {
|
|
||||||
if (ObjectHelper.isNull(id)) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
return entityQuery.queryable(target)
|
|
||||||
.whereById(id)
|
|
||||||
.singleOptional();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Named("detail")
|
|
||||||
@Override
|
|
||||||
public ENTITY detail(Long id) {
|
|
||||||
return detailOptional(id).orElse(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Named("detailOrThrow")
|
|
||||||
@Override
|
|
||||||
public ENTITY detailOrThrow(Long id) {
|
|
||||||
return detailOptional(id).orElseThrow(() -> new IdNotFoundException(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional(rollbackFor = Throwable.class)
|
|
||||||
@Override
|
|
||||||
public void remove(Long id) {
|
|
||||||
if (ObjectHelper.isNotNull(id)) {
|
|
||||||
entityQuery.deletable(target)
|
|
||||||
.whereById(id)
|
|
||||||
.allowDeleteStatement(true)
|
|
||||||
.executeRows();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional(rollbackFor = Throwable.class)
|
|
||||||
@Override
|
|
||||||
public void remove(Set<Long> ids) {
|
|
||||||
if (ObjectHelper.isNotEmpty(ids)) {
|
|
||||||
entityQuery.deletable(target)
|
|
||||||
.whereByIds(ids)
|
|
||||||
.allowDeleteStatement(true)
|
|
||||||
.executeRows();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class EqQueryParser<ENTITY extends SimpleEntity & ProxyEntityAvailable<ENTITY, PROXY>, PROXY extends AbstractProxyEntity<PROXY, ENTITY>> extends QueryParser<PROXY> {
|
|
||||||
public EqQueryParser(Query.Queryable queryable, PROXY container) {
|
|
||||||
super(queryable, container);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void nullEqual(Query.Queryable queryable, PROXY proxy) {
|
|
||||||
queryable.nullEqual().forEach(column -> proxy.anyColumn(column).isNull());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void notNullEqual(Query.Queryable queryable, PROXY proxy) {
|
|
||||||
queryable.notNullEqual().forEach(column -> proxy.anyColumn(column).isNotNull());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void empty(Query.Queryable queryable, PROXY proxy) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void notEmpty(Query.Queryable queryable, PROXY proxy) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void equal(Query.Queryable queryable, PROXY proxy) {
|
|
||||||
queryable.equal().forEach((column, value) -> proxy.anyColumn(column).eq(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void notEqual(Query.Queryable queryable, PROXY proxy) {
|
|
||||||
queryable.notEqual().forEach((column, value) -> proxy.anyColumn(column).ne(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void like(Query.Queryable queryable, PROXY proxy) {
|
|
||||||
queryable.like().forEach((column, value) -> proxy.anyColumn(column).likeRaw(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void notLike(Query.Queryable queryable, PROXY proxy) {
|
|
||||||
queryable.notLike().forEach((column, value) -> proxy.anyColumn(column).notLikeRaw(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void contain(Query.Queryable queryable, PROXY proxy) {
|
|
||||||
queryable.contain().forEach((column, value) -> proxy.anyColumn(column).like(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void notContain(Query.Queryable queryable, PROXY proxy) {
|
|
||||||
queryable.notContain().forEach((column, value) -> proxy.anyColumn(column).notLike(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void startWith(Query.Queryable queryable, PROXY proxy) {
|
|
||||||
queryable.startWith().forEach((column, value) -> proxy.anyColumn(column).likeMatchLeft(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void notStartWith(Query.Queryable queryable, PROXY proxy) {
|
|
||||||
queryable.notStartWith().forEach((column, value) -> proxy.anyColumn(column).notLikeMatchLeft(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void endWith(Query.Queryable queryable, PROXY proxy) {
|
|
||||||
queryable.endWith().forEach((column, value) -> proxy.anyColumn(column).likeMatchRight(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void notEndWith(Query.Queryable queryable, PROXY proxy) {
|
|
||||||
queryable.notEndWith().forEach((column, value) -> proxy.anyColumn(column).notLikeMatchRight(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void great(Query.Queryable queryable, PROXY proxy) {
|
|
||||||
queryable.great().forEach((column, value) -> proxy.anyColumn(column).gt(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void less(Query.Queryable queryable, PROXY proxy) {
|
|
||||||
queryable.less().forEach((column, value) -> proxy.anyColumn(column).lt(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void greatEqual(Query.Queryable queryable, PROXY proxy) {
|
|
||||||
queryable.greatEqual().forEach((column, value) -> proxy.anyColumn(column).ge(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void lessEqual(Query.Queryable queryable, PROXY proxy) {
|
|
||||||
queryable.lessEqual().forEach((column, value) -> proxy.anyColumn(column).le(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void inside(Query.Queryable queryable, PROXY proxy) {
|
|
||||||
queryable.inside()
|
|
||||||
.entrySet()
|
|
||||||
.stream()
|
|
||||||
.filter(entry -> ObjectHelper.isNotEmpty(entry.getValue()))
|
|
||||||
.forEach(entry -> proxy.anyColumn(entry.getKey()).in(entry.getValue()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void notInside(Query.Queryable queryable, PROXY proxy) {
|
|
||||||
queryable.notInside()
|
|
||||||
.entrySet()
|
|
||||||
.stream()
|
|
||||||
.filter(entry -> ObjectHelper.isNotEmpty(entry.getValue()))
|
|
||||||
.forEach(entry -> proxy.anyColumn(entry.getKey()).notIn(entry.getValue()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void between(Query.Queryable queryable, PROXY proxy) {
|
|
||||||
queryable.between().forEach((column, value) -> {
|
|
||||||
proxy.anyColumn(column).gt(value.start());
|
|
||||||
proxy.anyColumn(column).le(value.end());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void notBetween(Query.Queryable queryable, PROXY proxy) {
|
|
||||||
queryable.between().forEach((column, value) -> {
|
|
||||||
proxy.anyColumn(column).le(value.start());
|
|
||||||
proxy.anyColumn(column).gt(value.end());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
create table if not exists Company
|
|
||||||
(
|
|
||||||
id bigint primary key,
|
|
||||||
name varchar(255) not null,
|
|
||||||
members int not null,
|
|
||||||
created_time timestamp not null default current_timestamp(),
|
|
||||||
modified_time timestamp not null default current_timestamp() on update current_timestamp(),
|
|
||||||
deleted tinyint not null default false
|
|
||||||
);
|
|
||||||
|
|
||||||
create table if not exists Employee
|
|
||||||
(
|
|
||||||
id bigint primary key,
|
|
||||||
name varchar(255) not null,
|
|
||||||
age int not null,
|
|
||||||
company_id bigint not null,
|
|
||||||
created_time timestamp not null default current_timestamp(),
|
|
||||||
modified_time timestamp not null default current_timestamp() on update current_timestamp(),
|
|
||||||
deleted tinyint not null default false
|
|
||||||
);
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.eq;
|
|
||||||
|
|
||||||
import com.easy.query.api.proxy.client.EasyEntityQuery;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.test.AbstractTestApplication;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.eq.entity.Company;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.eq.entity.Employee;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.eq.entity.proxy.EmployeeProxy;
|
|
||||||
import java.util.List;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.boot.SpringApplication;
|
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
|
||||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
|
||||||
import org.springframework.context.event.EventListener;
|
|
||||||
import org.springframework.util.Assert;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@SpringBootApplication
|
|
||||||
public class TestApplication extends AbstractTestApplication {
|
|
||||||
private final EasyEntityQuery entityQuery;
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
|
||||||
SpringApplication.run(TestApplication.class, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventListener(ApplicationReadyEvent.class)
|
|
||||||
public void runTests() {
|
|
||||||
testCrud();
|
|
||||||
testDelete();
|
|
||||||
testQuery();
|
|
||||||
|
|
||||||
System.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void testDelete() {
|
|
||||||
formatLog("Delete");
|
|
||||||
saveItem("company", randomCompany());
|
|
||||||
saveItem("company", randomCompany());
|
|
||||||
entityQuery.deletable(Company.class)
|
|
||||||
.where(proxy -> proxy.id().isNotNull())
|
|
||||||
.allowDeleteStatement(true)
|
|
||||||
.executeRows();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void testQuery() {
|
|
||||||
formatLog("Added");
|
|
||||||
var company1 = Company.builder().name(randomString(5)).members(randomInt(100)).build();
|
|
||||||
entityQuery.insertable(company1).executeRows();
|
|
||||||
var company2 = Company.builder().name(randomString(5)).members(randomInt(100)).build();
|
|
||||||
entityQuery.insertable(company2).executeRows();
|
|
||||||
var employee1 = Employee.builder().name("Tom").age(randomInt(100)).companyId(company1.getId()).build();
|
|
||||||
entityQuery.insertable(employee1).executeRows();
|
|
||||||
var employee2 = Employee.builder().name(randomString(10)).age(randomInt(100)).companyId(company2.getId()).build();
|
|
||||||
entityQuery.insertable(employee2).executeRows();
|
|
||||||
|
|
||||||
formatLog("Query");
|
|
||||||
var employees1 = entityQuery.queryable(Employee.class)
|
|
||||||
.include(EmployeeProxy::company)
|
|
||||||
.where(proxy -> {
|
|
||||||
proxy.name().isNotNull();
|
|
||||||
proxy.name().eq("Tom");
|
|
||||||
proxy.name().startsWith("To");
|
|
||||||
proxy.name().endsWith("om");
|
|
||||||
proxy.age().lt(200);
|
|
||||||
proxy.age().gt(0);
|
|
||||||
proxy.name().in(List.of("Tom", "Mike"));
|
|
||||||
})
|
|
||||||
.toList();
|
|
||||||
Assert.isTrue(employees1.size() == 1, "查询数量错误");
|
|
||||||
|
|
||||||
formatLog("Clean");
|
|
||||||
entityQuery.deletable(Company.class).where(proxy -> proxy.id().isNotNull()).executeRows();
|
|
||||||
entityQuery.deletable(Employee.class).where(proxy -> proxy.id().isNotNull()).executeRows();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.eq.controller;
|
|
||||||
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.eq.entity.Company;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.eq.entity.proxy.CompanyProxy;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.eq.service.CompanyService;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.function.Function;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("company")
|
|
||||||
public class CompanyController extends SimpleControllerSupport<Company, CompanyProxy, CompanyController.SaveItem, CompanyController.ListItem, CompanyController.DetailItem> {
|
|
||||||
public CompanyController(CompanyService service) {
|
|
||||||
super(service);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Function<SaveItem, Company> saveItemMapper() {
|
|
||||||
return item -> {
|
|
||||||
var company = new Company();
|
|
||||||
company.setId(item.id());
|
|
||||||
company.setName(item.name());
|
|
||||||
company.setMembers(item.members());
|
|
||||||
return company;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Function<Company, ListItem> listItemMapper() {
|
|
||||||
return company -> new ListItem(
|
|
||||||
company.getId(),
|
|
||||||
company.getName(),
|
|
||||||
company.getMembers()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Function<Company, DetailItem> detailItemMapper() {
|
|
||||||
return company -> new DetailItem(
|
|
||||||
company.getId(),
|
|
||||||
company.getName(),
|
|
||||||
company.getMembers(),
|
|
||||||
company.getCreatedTime(),
|
|
||||||
company.getModifiedTime()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public record SaveItem(
|
|
||||||
Long id,
|
|
||||||
String name,
|
|
||||||
Integer members
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public record ListItem(
|
|
||||||
Long id,
|
|
||||||
String name,
|
|
||||||
Integer members
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public record DetailItem(
|
|
||||||
Long id,
|
|
||||||
String name,
|
|
||||||
Integer members,
|
|
||||||
LocalDateTime createdTime,
|
|
||||||
LocalDateTime modifiedTime
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.eq.entity;
|
|
||||||
|
|
||||||
import com.easy.query.core.annotation.EntityProxy;
|
|
||||||
import com.easy.query.core.annotation.Navigate;
|
|
||||||
import com.easy.query.core.annotation.Table;
|
|
||||||
import com.easy.query.core.enums.RelationTypeEnum;
|
|
||||||
import com.easy.query.core.proxy.ProxyEntityAvailable;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.eq.entity.proxy.CompanyProxy;
|
|
||||||
import java.util.List;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.ToString;
|
|
||||||
import lombok.experimental.FieldNameConstants;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@ToString(callSuper = true)
|
|
||||||
@FieldNameConstants
|
|
||||||
@AllArgsConstructor
|
|
||||||
@NoArgsConstructor
|
|
||||||
@Builder
|
|
||||||
@Table
|
|
||||||
@EntityProxy
|
|
||||||
public class Company extends SimpleEntity implements ProxyEntityAvailable<Company, CompanyProxy> {
|
|
||||||
private String name;
|
|
||||||
private Integer members;
|
|
||||||
|
|
||||||
@Navigate(value = RelationTypeEnum.OneToMany, selfProperty = {"id"}, targetProperty = {"companyId"})
|
|
||||||
private List<Employee> employees;
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.eq.entity;
|
|
||||||
|
|
||||||
import com.easy.query.core.annotation.EntityProxy;
|
|
||||||
import com.easy.query.core.annotation.Navigate;
|
|
||||||
import com.easy.query.core.annotation.Table;
|
|
||||||
import com.easy.query.core.enums.RelationTypeEnum;
|
|
||||||
import com.easy.query.core.proxy.ProxyEntityAvailable;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.eq.entity.proxy.EmployeeProxy;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.ToString;
|
|
||||||
import lombok.experimental.FieldNameConstants;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@ToString(callSuper = true)
|
|
||||||
@FieldNameConstants
|
|
||||||
@AllArgsConstructor
|
|
||||||
@NoArgsConstructor
|
|
||||||
@Builder
|
|
||||||
@Table
|
|
||||||
@EntityProxy
|
|
||||||
public class Employee extends SimpleEntity implements ProxyEntityAvailable<Employee, EmployeeProxy> {
|
|
||||||
private String name;
|
|
||||||
private Integer age;
|
|
||||||
private Long companyId;
|
|
||||||
|
|
||||||
@Navigate(value = RelationTypeEnum.OneToOne, selfProperty = {"companyId"}, targetProperty = {"id"})
|
|
||||||
private Company company;
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.eq.service;
|
|
||||||
|
|
||||||
import com.easy.query.api.proxy.client.EasyEntityQuery;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.eq.entity.Company;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.eq.entity.proxy.CompanyProxy;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class CompanyService extends SimpleServiceSupport<Company, CompanyProxy> {
|
|
||||||
public CompanyService(EasyEntityQuery entityQuery) {
|
|
||||||
super(entityQuery, Company.class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.eq.service;
|
|
||||||
|
|
||||||
import com.easy.query.api.proxy.client.EasyEntityQuery;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.eq.entity.Employee;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.eq.entity.proxy.EmployeeProxy;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class EmployeeService extends SimpleServiceSupport<Employee, EmployeeProxy> {
|
|
||||||
public EmployeeService(EasyEntityQuery entityQuery) {
|
|
||||||
super(entityQuery, Employee.class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
spring:
|
|
||||||
profiles:
|
|
||||||
include: test
|
|
||||||
datasource:
|
|
||||||
url: "jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=MySQL;DATABASE_TO_LOWER=TRUE;INIT=runscript from '/Users/lanyuanxiaoyao/Project/IdeaProjects/spring-boot-service-template/spring-boot-service-template-database/spring-boot-service-template-database-eq/src/test/initial.sql'"
|
|
||||||
username: test
|
|
||||||
password: test
|
|
||||||
driver-class-name: org.h2.Driver
|
|
||||||
easy-query:
|
|
||||||
database: mysql
|
|
||||||
name-conversion: underlined
|
|
||||||
print-sql: false
|
|
||||||
print-nav-sql: false
|
|
||||||
@@ -1,911 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.jpa;
|
|
||||||
|
|
||||||
import com.blinkfox.fenix.EnableFenix;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.test.AbstractTestApplication;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.jpa.entity.Address;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.jpa.entity.Address_;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.jpa.entity.Company;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.jpa.entity.Company_;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.jpa.entity.Employee;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.jpa.entity.Employee_;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.jpa.entity.QEmployee;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.jpa.entity.Skill;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.jpa.entity.Skill_;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.jpa.repository.CompanyRepository;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.jpa.repository.EmployeeRepository;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.jpa.repository.ReportRepository;
|
|
||||||
import com.querydsl.core.types.dsl.CaseBuilder;
|
|
||||||
import com.querydsl.jpa.impl.JPAQueryFactory;
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.hibernate.Session;
|
|
||||||
import org.springframework.boot.SpringApplication;
|
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
|
||||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
|
||||||
import org.springframework.context.event.EventListener;
|
|
||||||
import org.springframework.data.domain.PageRequest;
|
|
||||||
import org.springframework.data.domain.Sort;
|
|
||||||
import org.springframework.data.jpa.domain.Specification;
|
|
||||||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
import org.springframework.util.Assert;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@SpringBootApplication
|
|
||||||
@EnableFenix
|
|
||||||
@EnableJpaAuditing
|
|
||||||
@Transactional
|
|
||||||
public class TestApplication extends AbstractTestApplication {
|
|
||||||
private final CompanyRepository companyRepository;
|
|
||||||
private final EmployeeRepository employeeRepository;
|
|
||||||
private final ReportRepository reportRepository;
|
|
||||||
private final Session session;
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
|
||||||
SpringApplication.run(TestApplication.class, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventListener(ApplicationReadyEvent.class)
|
|
||||||
public void runTests() {
|
|
||||||
testCrud();
|
|
||||||
testDelete();
|
|
||||||
testQuery();
|
|
||||||
testNative();
|
|
||||||
|
|
||||||
System.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void testDelete() {
|
|
||||||
formatLog("Delete JPA");
|
|
||||||
saveItem("company", randomCompany());
|
|
||||||
saveItem("company", randomCompany());
|
|
||||||
companyRepository.deleteAll();
|
|
||||||
|
|
||||||
formatLog("Delete JPA Batch");
|
|
||||||
saveItem("company", randomCompany());
|
|
||||||
saveItem("company", randomCompany());
|
|
||||||
companyRepository.deleteAllInBatch();
|
|
||||||
|
|
||||||
formatLog("Delete JPA by id");
|
|
||||||
var cid1 = saveItem("company", randomCompany()).get("data").asLong();
|
|
||||||
var cid2 = saveItem("company", randomCompany()).get("data").asLong();
|
|
||||||
companyRepository.deleteAllById(List.of(cid1, cid2));
|
|
||||||
|
|
||||||
formatLog("Delete Fenix by id");
|
|
||||||
cid1 = saveItem("company", randomCompany()).get("data").asLong();
|
|
||||||
cid2 = saveItem("company", randomCompany()).get("data").asLong();
|
|
||||||
companyRepository.deleteByIds(List.of(cid1, cid2));
|
|
||||||
|
|
||||||
formatLog("Delete Fenix Batch by id");
|
|
||||||
cid1 = saveItem("company", randomCompany()).get("data").asLong();
|
|
||||||
cid2 = saveItem("company", randomCompany()).get("data").asLong();
|
|
||||||
companyRepository.deleteBatchByIds(List.of(cid1, cid2));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void testQuery() {
|
|
||||||
var factory = new JPAQueryFactory(session);
|
|
||||||
|
|
||||||
formatLog("准备 Specification 查询的测试数据");
|
|
||||||
var company1 = companyRepository.save(Company.builder().name("TechCorp").members(100).build());
|
|
||||||
var company2 = companyRepository.save(Company.builder().name("DataInc").members(50).build());
|
|
||||||
var company3 = companyRepository.save(Company.builder().name("CloudSys").members(150).build());
|
|
||||||
|
|
||||||
// 准备 Skills 数据
|
|
||||||
var skill1 = Skill.builder().name("Java").description("Java 编程语言").build();
|
|
||||||
var skill2 = Skill.builder().name("Python").description("Python 编程语言").build();
|
|
||||||
var skill3 = Skill.builder().name("Spring").description("Spring 框架").build();
|
|
||||||
var skill4 = Skill.builder().name("MySQL").description("MySQL 数据库").build();
|
|
||||||
|
|
||||||
employeeRepository.save(Employee.builder()
|
|
||||||
.name("Alice").age(30).role(Employee.Role.ADMIN).code("E001")
|
|
||||||
.salary(new BigDecimal("50000.00")).bonus(new BigDecimal("5000.00"))
|
|
||||||
.active(true).company(company1)
|
|
||||||
.address(Address.builder().street("Main St").city("Beijing").state("Beijing").zipCode("100000").country("China").build())
|
|
||||||
.skills(Set.of(skill1, skill3))
|
|
||||||
.hobbies(List.of("Reading", "Swimming"))
|
|
||||||
.properties(Map.of("department", "Engineering", "level", "Senior"))
|
|
||||||
.connections(Map.of(Employee.ConnectionType.EMAIL, "alice@example.com"))
|
|
||||||
.build());
|
|
||||||
|
|
||||||
employeeRepository.save(Employee.builder()
|
|
||||||
.name("Bob").age(25).role(Employee.Role.USER).code("E002")
|
|
||||||
.salary(new BigDecimal("40000.00")).bonus(new BigDecimal("4000.00"))
|
|
||||||
.active(true).company(company2)
|
|
||||||
.address(Address.builder().street("Oak Ave").city("Shanghai").state("Shanghai").zipCode("200000").country("China").build())
|
|
||||||
.skills(Set.of(skill2))
|
|
||||||
.hobbies(List.of("Gaming"))
|
|
||||||
.properties(Map.of("department", "Marketing", "level", "Junior"))
|
|
||||||
.connections(Map.of(Employee.ConnectionType.PHONE, "1234567890"))
|
|
||||||
.build());
|
|
||||||
|
|
||||||
employeeRepository.save(Employee.builder()
|
|
||||||
.name("Charlie").age(35).role(Employee.Role.ADMIN).code("E003")
|
|
||||||
.salary(new BigDecimal("60000.00")).bonus(new BigDecimal("6000.00"))
|
|
||||||
.active(false).company(company1)
|
|
||||||
.address(Address.builder().street("Pine Rd").city("Shenzhen").state("Guangdong").zipCode("518000").country("China").build())
|
|
||||||
.skills(Set.of(skill1, skill2, skill3))
|
|
||||||
.hobbies(List.of("Reading", "Gaming", "Music"))
|
|
||||||
.properties(Map.of("department", "Engineering", "level", "Lead"))
|
|
||||||
.connections(Map.of(Employee.ConnectionType.EMAIL, "charlie@example.com", Employee.ConnectionType.PHONE, "0987654321"))
|
|
||||||
.build());
|
|
||||||
|
|
||||||
employeeRepository.save(Employee.builder()
|
|
||||||
.name("David").age(28).role(Employee.Role.USER).code("E004")
|
|
||||||
.salary(new BigDecimal("45000.00")).bonus(null)
|
|
||||||
.active(true).company(company3)
|
|
||||||
.address(Address.builder().street("Elm Ln").city("Guangzhou").state("Guangdong").zipCode("510000").country("China").build())
|
|
||||||
.skills(Set.of(skill4))
|
|
||||||
.hobbies(List.of())
|
|
||||||
.properties(Map.of())
|
|
||||||
.connections(Map.of())
|
|
||||||
.build());
|
|
||||||
|
|
||||||
employeeRepository.save(Employee.builder()
|
|
||||||
.name("Alice Smith").age(32).role(Employee.Role.USER).code("E005")
|
|
||||||
.salary(new BigDecimal("55000.00")).bonus(new BigDecimal("5500.00"))
|
|
||||||
.active(true).company(company2)
|
|
||||||
.address(Address.builder().street("Maple Dr").city("Hangzhou").state("Zhejiang").zipCode("310000").country("China").build())
|
|
||||||
.skills(Set.of(skill1, skill4))
|
|
||||||
.hobbies(List.of("Swimming", "Music"))
|
|
||||||
.properties(Map.of("department", "Sales", "level", "Middle"))
|
|
||||||
.connections(Map.of(Employee.ConnectionType.EMAIL, "alicesmith@example.com"))
|
|
||||||
.build());
|
|
||||||
|
|
||||||
formatLog("1. 基本比较操作符查询 JPA");
|
|
||||||
// 查找姓名为"Bob"、角色不是ADMIN、年龄在20-30之间、薪资在40000-45000之间的员工
|
|
||||||
var result1_jpa = employeeRepository.findAll((root, query, cb) -> cb.and(
|
|
||||||
cb.equal(root.get(Employee_.name), "Bob"),
|
|
||||||
cb.notEqual(root.get(Employee_.role), Employee.Role.ADMIN),
|
|
||||||
cb.greaterThan(root.get(Employee_.age), 20),
|
|
||||||
cb.lessThan(root.get(Employee_.age), 30),
|
|
||||||
cb.greaterThanOrEqualTo(root.get(Employee_.salary), new BigDecimal("40000.00")),
|
|
||||||
cb.lessThanOrEqualTo(root.get(Employee_.salary), new BigDecimal("45000.00"))
|
|
||||||
));
|
|
||||||
Assert.isTrue(result1_jpa.size() == 1, "基本比较操作符查询失败 (%d)".formatted(result1_jpa.size()));
|
|
||||||
|
|
||||||
formatLog("1. 基本比较操作符查询 Fenix");
|
|
||||||
var result1_fenix = employeeRepository.findAll(
|
|
||||||
builder -> builder.andEquals(Employee.Fields.name, "Bob")
|
|
||||||
.andNotEquals(Employee.Fields.role, Employee.Role.ADMIN)
|
|
||||||
.andGreaterThan(Employee.Fields.age, 20)
|
|
||||||
.andLessThan(Employee.Fields.age, 30)
|
|
||||||
.andGreaterThanEqual(Employee.Fields.salary, new BigDecimal("40000.00"))
|
|
||||||
.andLessThanEqual(Employee.Fields.salary, new BigDecimal("45000.00"))
|
|
||||||
.build()
|
|
||||||
);
|
|
||||||
Assert.isTrue(result1_fenix.size() == 1, "基本比较操作符查询失败 (%d)".formatted(result1_fenix.size()));
|
|
||||||
|
|
||||||
formatLog("1. 基本比较操作符查询 QueryDSL");
|
|
||||||
var result1_querydsl = employeeRepository.findAll(
|
|
||||||
QEmployee.employee.name.eq("Bob")
|
|
||||||
.and(QEmployee.employee.role.ne(Employee.Role.ADMIN))
|
|
||||||
.and(QEmployee.employee.age.gt(20))
|
|
||||||
.and(QEmployee.employee.age.lt(30))
|
|
||||||
.and(QEmployee.employee.salary.goe(new BigDecimal("40000.00")))
|
|
||||||
.and(QEmployee.employee.salary.loe(new BigDecimal("45000.00")))
|
|
||||||
);
|
|
||||||
Assert.isTrue(result1_querydsl.size() == 1, "基本比较操作符查询失败 (%d)".formatted(result1_querydsl.size()));
|
|
||||||
|
|
||||||
formatLog("1. 基本比较操作符查询 HQL");
|
|
||||||
var result1_hql = session.createQuery(
|
|
||||||
"""
|
|
||||||
from Employee employee
|
|
||||||
where employee.name = 'Bob'
|
|
||||||
and employee.role != com.lanyuanxiaoyao.service.template.database.jpa.entity.Employee.Role.ADMIN
|
|
||||||
and employee.age > 20
|
|
||||||
and employee.age < 30
|
|
||||||
and employee.salary >= 40000.00
|
|
||||||
and employee.salary <= 45000.00
|
|
||||||
""",
|
|
||||||
Employee.class
|
|
||||||
).list();
|
|
||||||
Assert.isTrue(result1_hql.size() == 1, "基本比较操作符查询失败 (%d)".formatted(result1_hql.size()));
|
|
||||||
|
|
||||||
formatLog("2. 区间和集合操作符查询 JPA");
|
|
||||||
// 查找年龄在25-30之间、年龄不在40-50之间、年龄在25/30/35中、角色是USER或ADMIN、姓名不在Charlie/David中的员工
|
|
||||||
var result2_jpa = employeeRepository.findAll((root, query, cb) -> cb.and(
|
|
||||||
cb.between(root.get(Employee_.age), 25, 30),
|
|
||||||
cb.between(root.get(Employee_.age), 40, 50).not(),
|
|
||||||
root.get(Employee_.age).in(25, 30, 35),
|
|
||||||
root.get(Employee_.role).in(Employee.Role.USER, Employee.Role.ADMIN),
|
|
||||||
cb.not(root.get(Employee_.name).in("Charlie", "David")),
|
|
||||||
root.get(Employee_.name).in("Charlie", "David").not()
|
|
||||||
));
|
|
||||||
Assert.isTrue(result2_jpa.size() == 2, "区间和集合操作符查询失败 (%d)".formatted(result2_jpa.size()));
|
|
||||||
|
|
||||||
formatLog("2. 区间和集合操作符查询 Fenix");
|
|
||||||
var result2_fenix = employeeRepository.findAll(
|
|
||||||
builder -> builder.andBetween(Employee.Fields.age, 25, 30)
|
|
||||||
.andNotBetween(Employee.Fields.age, 40, 50)
|
|
||||||
.andIn(Employee.Fields.age, List.of(25, 30, 35))
|
|
||||||
.andIn(Employee.Fields.role, List.of(Employee.Role.USER, Employee.Role.ADMIN))
|
|
||||||
.andNotIn(Employee.Fields.name, List.of("Charlie", "David"))
|
|
||||||
.build()
|
|
||||||
);
|
|
||||||
Assert.isTrue(result2_fenix.size() == 2, "区间和集合操作符查询失败 (%d)".formatted(result2_fenix.size()));
|
|
||||||
|
|
||||||
formatLog("2. 区间和集合操作符查询 QueryDSL");
|
|
||||||
var result2_querydsl = employeeRepository.findAll(
|
|
||||||
QEmployee.employee.age.between(25, 30)
|
|
||||||
.and(QEmployee.employee.age.between(40, 50).not())
|
|
||||||
.and(QEmployee.employee.age.in(25, 30, 35))
|
|
||||||
.and(QEmployee.employee.role.in(Employee.Role.USER, Employee.Role.ADMIN))
|
|
||||||
.and(QEmployee.employee.name.in("Charlie", "David").not())
|
|
||||||
.and(QEmployee.employee.name.in(List.of("Charlie", "David")).not())
|
|
||||||
);
|
|
||||||
Assert.isTrue(result2_querydsl.size() == 2, "区间和集合操作符查询失败 (%d)".formatted(result2_querydsl.size()));
|
|
||||||
|
|
||||||
formatLog("2. 区间和集合操作符查询 HQL");
|
|
||||||
var result2_hql = session.createQuery(
|
|
||||||
"""
|
|
||||||
from Employee employee
|
|
||||||
where employee.age between 25 and 30
|
|
||||||
and not (employee.age between 40 and 50)
|
|
||||||
and employee.age in (25, 30, 35)
|
|
||||||
and employee.role in (com.lanyuanxiaoyao.service.template.database.jpa.entity.Employee.Role.USER, com.lanyuanxiaoyao.service.template.database.jpa.entity.Employee.Role.ADMIN)
|
|
||||||
and employee.name not in ('Charlie', 'David')
|
|
||||||
""",
|
|
||||||
Employee.class
|
|
||||||
).list();
|
|
||||||
Assert.isTrue(result2_hql.size() == 2, "区间和集合操作符查询失败 (%d)".formatted(result2_hql.size()));
|
|
||||||
|
|
||||||
formatLog("3. 字符串操作符查询 JPA");
|
|
||||||
// 查找以A开头、不以C开头、包含"ali"(忽略大小写)、名称长度在4-10之间、前3个字符为"Ali"的员工
|
|
||||||
var result3_jpa = employeeRepository.findAll((root, query, cb) -> cb.and(
|
|
||||||
cb.like(root.get(Employee_.name), "A%"),
|
|
||||||
cb.notLike(root.get(Employee_.name), "C%"),
|
|
||||||
cb.like(cb.lower(root.get(Employee_.name)), "%ali%"),
|
|
||||||
cb.like(cb.upper(root.get(Employee_.name)), "%ALI%"),
|
|
||||||
cb.greaterThan(cb.length(root.get(Employee_.name)), 4),
|
|
||||||
cb.lessThanOrEqualTo(cb.length(root.get(Employee_.name)), 10),
|
|
||||||
cb.equal(cb.substring(root.get(Employee_.name), 0, 3), "Ali")
|
|
||||||
));
|
|
||||||
Assert.isTrue(result3_jpa.size() == 1, "字符串操作符查询失败 (%d)".formatted(result3_jpa.size()));
|
|
||||||
|
|
||||||
formatLog("3. 字符串操作符查询 Fenix");
|
|
||||||
log.info("Fenix框架当前版本不支持以下字符串操作符:");
|
|
||||||
log.info(" - cb.length() - 字符串长度函数");
|
|
||||||
log.info(" - cb.substring() - 子字符串提取函数");
|
|
||||||
log.info(" - cb.lower() / cb.upper() - 大小写转换函数 (不支持直接在条件中使用)");
|
|
||||||
log.info("Fenix支持的部分实现:");
|
|
||||||
var result3_fenix = employeeRepository.findAll(
|
|
||||||
builder -> builder.andStartsWith(Employee.Fields.name, "A")
|
|
||||||
.andNotStartsWith(Employee.Fields.name, "C")
|
|
||||||
.build()
|
|
||||||
);
|
|
||||||
log.info("Fenix查询结果: {} 条记录(仅支持部分条件)", result3_fenix.size());
|
|
||||||
|
|
||||||
formatLog("3. 字符串操作符查询 QueryDSL");
|
|
||||||
var result3_querydsl = employeeRepository.findAll(
|
|
||||||
QEmployee.employee.name.startsWith("A")
|
|
||||||
.and(QEmployee.employee.name.startsWith("C").not())
|
|
||||||
.and(QEmployee.employee.name.toLowerCase().contains("ali"))
|
|
||||||
.and(QEmployee.employee.name.toUpperCase().contains("ALI"))
|
|
||||||
.and(QEmployee.employee.name.length().gt(4))
|
|
||||||
.and(QEmployee.employee.name.length().loe(10))
|
|
||||||
.and(QEmployee.employee.name.substring(0, 3).eq("Ali"))
|
|
||||||
);
|
|
||||||
Assert.isTrue(result3_querydsl.size() == 1, "字符串操作符查询失败 (%d)".formatted(result3_querydsl.size()));
|
|
||||||
|
|
||||||
formatLog("3. 字符串操作符查询 HQL");
|
|
||||||
var result3_hql = session.createQuery(
|
|
||||||
"""
|
|
||||||
from Employee employee
|
|
||||||
where employee.name like 'A%'
|
|
||||||
and employee.name not like 'C%'
|
|
||||||
and lower(employee.name) like '%ali%'
|
|
||||||
and upper(employee.name) like '%ALI%'
|
|
||||||
and length(employee.name) > 4
|
|
||||||
and length(employee.name) <= 10
|
|
||||||
and substring(employee.name, 1, 3) = 'Ali'
|
|
||||||
""",
|
|
||||||
Employee.class
|
|
||||||
).list();
|
|
||||||
Assert.isTrue(result3_hql.size() == 1, "字符串操作符查询失败 (%d)".formatted(result3_hql.size()));
|
|
||||||
|
|
||||||
formatLog("4. NULL 和布尔操作符查询 JPA");
|
|
||||||
// 查找激活状态为true、bonus不为null、code不在E999中的员工
|
|
||||||
var result4_jpa = employeeRepository.findAll((root, query, cb) -> cb.and(
|
|
||||||
cb.isTrue(root.get(Employee_.active)),
|
|
||||||
cb.isNotNull(root.get(Employee_.bonus)),
|
|
||||||
cb.not(root.get(Employee_.code).in("E999"))
|
|
||||||
));
|
|
||||||
Assert.isTrue(!result4_jpa.isEmpty(), "NULL 和布尔操作符查询失败 (%d)".formatted(result4_jpa.size()));
|
|
||||||
|
|
||||||
formatLog("4. NULL 和布尔操作符查询 Fenix");
|
|
||||||
log.info("Fenix框架当前版本不支持以下布尔操作符:");
|
|
||||||
log.info(" - cb.isTrue() / cb.isFalse() - 专用布尔判断函数");
|
|
||||||
log.info("Fenix通过equals/notEquals处理布尔字段");
|
|
||||||
var result4_fenix = employeeRepository.findAll(
|
|
||||||
builder -> builder.andEquals(Employee.Fields.active, true)
|
|
||||||
.andIsNotNull(Employee.Fields.bonus)
|
|
||||||
.andNotIn(Employee.Fields.code, List.of("E999"))
|
|
||||||
.build()
|
|
||||||
);
|
|
||||||
Assert.isTrue(!result4_fenix.isEmpty(), "NULL 和布尔操作符查询失败 (%d)".formatted(result4_fenix.size()));
|
|
||||||
|
|
||||||
formatLog("4. NULL 和布尔操作符查询 QueryDSL");
|
|
||||||
var result4_querydsl = employeeRepository.findAll(
|
|
||||||
QEmployee.employee.active.isTrue()
|
|
||||||
.and(QEmployee.employee.bonus.isNotNull())
|
|
||||||
.and(QEmployee.employee.code.in("E999").not())
|
|
||||||
);
|
|
||||||
Assert.isTrue(!result4_querydsl.isEmpty(), "NULL 和布尔操作符查询失败 (%d)".formatted(result4_querydsl.size()));
|
|
||||||
|
|
||||||
formatLog("4. NULL 和布尔操作符查询 HQL");
|
|
||||||
var result4_hql = session.createQuery(
|
|
||||||
"""
|
|
||||||
from Employee employee
|
|
||||||
where employee.active is true
|
|
||||||
and employee.bonus is not null
|
|
||||||
and employee.code not in ('E999')
|
|
||||||
""",
|
|
||||||
Employee.class
|
|
||||||
).list();
|
|
||||||
Assert.isTrue(!result4_hql.isEmpty(), "NULL 和布尔操作符查询失败 (%d)".formatted(result4_hql.size()));
|
|
||||||
|
|
||||||
formatLog("5. 集合操作符查询 JPA");
|
|
||||||
// 查找技能集合非空、爱好集合非空、包含"Reading"爱好、不包含"Riding"爱好、爱好数量大于1、技能数量小于4的员工
|
|
||||||
var result5_jpa = employeeRepository.findAll((root, query, cb) -> cb.and(
|
|
||||||
cb.isNotEmpty(root.get(Employee_.skills)),
|
|
||||||
cb.isEmpty(root.get(Employee_.hobbies)).not(),
|
|
||||||
cb.isMember("Reading", root.get(Employee_.hobbies)),
|
|
||||||
cb.isNotMember("Riding", root.get(Employee_.hobbies)),
|
|
||||||
cb.greaterThan(cb.size(root.get(Employee_.hobbies)), 1),
|
|
||||||
cb.lessThan(cb.size(root.get(Employee_.skills)), 4)
|
|
||||||
));
|
|
||||||
Assert.isTrue(result5_jpa.size() == 2, "集合操作符查询失败 (%d)".formatted(result5_jpa.size()));
|
|
||||||
|
|
||||||
formatLog("5. 集合操作符查询 Fenix");
|
|
||||||
log.info("Fenix框架当前版本不支持以下集合操作符:");
|
|
||||||
log.info(" - cb.isNotEmpty() / cb.isEmpty() - 集合非空/空判断");
|
|
||||||
log.info(" - cb.isMember() / cb.isNotMember() - 集合成员判断");
|
|
||||||
log.info(" - cb.size() - 集合大小函数");
|
|
||||||
log.info("这些集合操作在JPA Criteria中需要复杂的join处理,Fenix当前不支持");
|
|
||||||
|
|
||||||
formatLog("5. 集合操作符查询 QueryDSL");
|
|
||||||
var result5_querydsl = employeeRepository.findAll(
|
|
||||||
QEmployee.employee.skills.isNotEmpty()
|
|
||||||
.and(QEmployee.employee.hobbies.isNotEmpty())
|
|
||||||
.and(QEmployee.employee.hobbies.contains("Reading"))
|
|
||||||
.and(QEmployee.employee.hobbies.contains("Riding").not())
|
|
||||||
.and(QEmployee.employee.hobbies.size().gt(1))
|
|
||||||
.and(QEmployee.employee.skills.size().lt(4))
|
|
||||||
);
|
|
||||||
Assert.isTrue(result5_querydsl.size() == 2, "集合操作符查询失败 (%d)".formatted(result5_querydsl.size()));
|
|
||||||
|
|
||||||
formatLog("5. 集合操作符查询 HQL");
|
|
||||||
var result5_hql = session.createQuery(
|
|
||||||
"""
|
|
||||||
from Employee employee
|
|
||||||
where employee.skills is not empty
|
|
||||||
and employee.hobbies is not empty
|
|
||||||
and 'Reading' member of employee.hobbies
|
|
||||||
and 'Riding' not member of employee.hobbies
|
|
||||||
and size(employee.hobbies) > 1
|
|
||||||
and size(employee.skills) < 4
|
|
||||||
""",
|
|
||||||
Employee.class
|
|
||||||
).list();
|
|
||||||
Assert.isTrue(result5_hql.size() == 2, "集合操作符查询失败 (%d)".formatted(result5_hql.size()));
|
|
||||||
|
|
||||||
formatLog("6. 逻辑操作符查询 JPA");
|
|
||||||
// 查找姓名为Alice或Bob、且姓名不为Charlie或David的员工
|
|
||||||
var result6_jpa = employeeRepository.findAll((root, query, cb) -> cb.and(
|
|
||||||
cb.or(
|
|
||||||
cb.equal(root.get(Employee_.name), "Alice"),
|
|
||||||
cb.equal(root.get(Employee_.name), "Bob")
|
|
||||||
),
|
|
||||||
cb.not(
|
|
||||||
cb.or(
|
|
||||||
cb.equal(root.get(Employee_.name), "Charlie"),
|
|
||||||
cb.equal(root.get(Employee_.name), "David")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
));
|
|
||||||
Assert.isTrue(result6_jpa.size() == 2, "逻辑操作符查询失败 (%d)".formatted(result6_jpa.size()));
|
|
||||||
|
|
||||||
formatLog("6. 逻辑操作符查询 Fenix");
|
|
||||||
log.info("Fenix框架当前版本不支持复杂的嵌套OR和NOT组合逻辑");
|
|
||||||
log.info("Fenix支持简单的orEquals,但不支持嵌套的or + not组合");
|
|
||||||
var result6_fenix = employeeRepository.findAll(
|
|
||||||
builder -> builder.orEquals(Employee.Fields.name, "Alice")
|
|
||||||
.orEquals(Employee.Fields.name, "Bob")
|
|
||||||
.andNotIn(Employee.Fields.name, List.of("Charlie", "David"))
|
|
||||||
.build()
|
|
||||||
);
|
|
||||||
Assert.isTrue(result6_fenix.size() == 3, "逻辑操作符查询失败 (%d)".formatted(result6_fenix.size()));
|
|
||||||
|
|
||||||
formatLog("6. 逻辑操作符查询 QueryDSL");
|
|
||||||
var result6_querydsl = employeeRepository.findAll(
|
|
||||||
QEmployee.employee.name.eq("Alice").or(QEmployee.employee.name.eq("Bob"))
|
|
||||||
.and(QEmployee.employee.name.eq("Charlie").or(QEmployee.employee.name.eq("David")).not())
|
|
||||||
);
|
|
||||||
Assert.isTrue(result6_querydsl.size() == 2, "逻辑操作符查询失败 (%d)".formatted(result6_querydsl.size()));
|
|
||||||
|
|
||||||
formatLog("6. 逻辑操作符查询 HQL");
|
|
||||||
var result6_hql = session.createQuery(
|
|
||||||
"""
|
|
||||||
from Employee employee
|
|
||||||
where (employee.name = 'Alice' or employee.name = 'Bob')
|
|
||||||
and not (employee.name = 'Charlie' or employee.name = 'David')
|
|
||||||
""",
|
|
||||||
Employee.class
|
|
||||||
).list();
|
|
||||||
Assert.isTrue(result6_hql.size() == 2, "逻辑操作符查询失败 (%d)".formatted(result6_hql.size()));
|
|
||||||
|
|
||||||
formatLog("7. Specification 链式调用查询 JPA");
|
|
||||||
// 链式组合:激活状态为true、年龄大于25、角色不是ADMIN、或姓名为Charlie、且姓名不为Alice Smith
|
|
||||||
var result7_jpa = employeeRepository.findAll(
|
|
||||||
Specification.<Employee>where((root, query, cb) -> cb.isTrue(root.get(Employee_.active)))
|
|
||||||
.and((root, query, cb) -> cb.greaterThan(root.get(Employee_.age), 25))
|
|
||||||
.and((root, query, cb) -> cb.notEqual(root.get(Employee_.role), Employee.Role.ADMIN))
|
|
||||||
.or((root, query, cb) -> cb.equal(root.get(Employee_.name), "Charlie"))
|
|
||||||
.and((root, query, cb) -> cb.notEqual(root.get(Employee_.name), "Alice Smith"))
|
|
||||||
);
|
|
||||||
Assert.isTrue(result7_jpa.size() == 2, "Specification 链式调用失败 (%d)".formatted(result7_jpa.size()));
|
|
||||||
|
|
||||||
formatLog("7. Specification 链式调用查询 Fenix");
|
|
||||||
var result7_fenix = employeeRepository.findAll(
|
|
||||||
builder -> builder.andEquals(Employee.Fields.active, true)
|
|
||||||
.andGreaterThan(Employee.Fields.age, 25)
|
|
||||||
.andNotEquals(Employee.Fields.role, Employee.Role.ADMIN)
|
|
||||||
.orEquals(Employee.Fields.name, "Charlie")
|
|
||||||
.andNotEquals(Employee.Fields.name, "Alice Smith")
|
|
||||||
.build()
|
|
||||||
);
|
|
||||||
Assert.isTrue(result7_fenix.size() == 2, "Specification 链式调用失败 (%d)".formatted(result7_fenix.size()));
|
|
||||||
|
|
||||||
formatLog("7. Specification 链式调用查询 QueryDSL");
|
|
||||||
var result7_querydsl = employeeRepository.findAll(
|
|
||||||
QEmployee.employee.active.isTrue()
|
|
||||||
.and(QEmployee.employee.age.gt(25))
|
|
||||||
.and(QEmployee.employee.role.ne(Employee.Role.ADMIN))
|
|
||||||
.or(QEmployee.employee.name.eq("Charlie"))
|
|
||||||
.and(QEmployee.employee.name.ne("Alice Smith"))
|
|
||||||
);
|
|
||||||
Assert.isTrue(result7_querydsl.size() == 2, "Specification 链式调用失败 (%d)".formatted(result7_querydsl.size()));
|
|
||||||
|
|
||||||
formatLog("7. Specification 链式调用查询 HQL");
|
|
||||||
var result7_hql = session.createQuery(
|
|
||||||
"""
|
|
||||||
from Employee employee
|
|
||||||
where employee.active is true
|
|
||||||
and employee.age > 25
|
|
||||||
and employee.role != com.lanyuanxiaoyao.service.template.database.jpa.entity.Employee.Role.ADMIN
|
|
||||||
and employee.name != 'Alice Smith'
|
|
||||||
or employee.name = 'Charlie'
|
|
||||||
""",
|
|
||||||
Employee.class
|
|
||||||
).list();
|
|
||||||
Assert.isTrue(result7_hql.size() == 2, "Specification 链式调用失败 (%d)".formatted(result7_hql.size()));
|
|
||||||
|
|
||||||
formatLog("8. Join 操作查询 JPA");
|
|
||||||
// 查找公司名为TechCorp、技能包含Java、属性值为Senior的员工(使用join、fetch、集合join、map join)
|
|
||||||
var result8_jpa = employeeRepository.findAll((root, query, cb) -> {
|
|
||||||
return cb.and(
|
|
||||||
// Company Join 条件
|
|
||||||
cb.equal(root.join(Employee_.company).get(Company_.name), "TechCorp"),
|
|
||||||
cb.notEqual(root.join(Employee_.company).get(Company_.name), "DataInc"),
|
|
||||||
// Skills Join 条件
|
|
||||||
cb.equal(root.join(Employee_.skills).get(Skill_.name), "Java"),
|
|
||||||
cb.notEqual(root.join(Employee_.skills).get(Skill_.name), "MySQL"),
|
|
||||||
// Map Join 条件
|
|
||||||
cb.equal(root.join(Employee_.properties).value(), "Senior"),
|
|
||||||
cb.notEqual(root.join(Employee_.properties).value(), "Junior")
|
|
||||||
);
|
|
||||||
});
|
|
||||||
Assert.isTrue(result8_jpa.size() == 1, "Join 操作查询失败 (%d)".formatted(result8_jpa.size()));
|
|
||||||
|
|
||||||
formatLog("8. Join 操作查询 Fenix");
|
|
||||||
log.info("Fenix框架当前版本不支持显式的join操作:");
|
|
||||||
log.info(" - root.join() - 显式关联查询");
|
|
||||||
log.info(" - root.fetch() - 显式抓取查询");
|
|
||||||
log.info(" - Map join (cb.equal(root.join().value(), ...))");
|
|
||||||
log.info("Fenix主要用于单表条件查询,复杂join操作建议使用JPA Specification原生方式或QueryDSL");
|
|
||||||
log.info("可以通过doAny使用原生CriteriaBuilder实现join操作");
|
|
||||||
log.info("注意:由于类型系统的限制,doAny中使用join可能会有类型推断问题");
|
|
||||||
log.info("建议:对于join等复杂查询,直接使用JPA Specification原生方式");
|
|
||||||
|
|
||||||
formatLog("8. Join 操作查询 QueryDSL");
|
|
||||||
var result8_querydsl = employeeRepository.findAll(
|
|
||||||
QEmployee.employee.company().name.eq("TechCorp")
|
|
||||||
.and(QEmployee.employee.company().name.ne("DataInc"))
|
|
||||||
.and(QEmployee.employee.skills.any().name.eq("Java"))
|
|
||||||
.and(QEmployee.employee.skills.any().name.ne("MySQL"))
|
|
||||||
.and(QEmployee.employee.properties.containsValue("Senior"))
|
|
||||||
.and(QEmployee.employee.properties.containsValue("Junior").not())
|
|
||||||
);
|
|
||||||
Assert.isTrue(result8_querydsl.size() == 1, "Join 操作查询失败 (%d)".formatted(result8_querydsl.size()));
|
|
||||||
|
|
||||||
formatLog("8. Join 操作查询 HQL");
|
|
||||||
var result8_hql = session.createQuery(
|
|
||||||
"""
|
|
||||||
from Employee employee
|
|
||||||
join employee.company as company
|
|
||||||
join employee.skills as skill
|
|
||||||
join employee.properties as prop
|
|
||||||
where company.name = 'TechCorp'
|
|
||||||
and company.name != 'DataInc'
|
|
||||||
and skill.name = 'Java'
|
|
||||||
and skill.name != 'MySQL'
|
|
||||||
and value(prop) = 'Senior'
|
|
||||||
and value(prop) != 'Junior'
|
|
||||||
""",
|
|
||||||
Employee.class
|
|
||||||
).list();
|
|
||||||
Assert.isTrue(result8_hql.size() == 1, "Join 操作查询失败 (%d)".formatted(result8_hql.size()));
|
|
||||||
|
|
||||||
formatLog("9. 子查询和聚合函数查询 JPA");
|
|
||||||
// 查找薪资高于平均薪资、总记录数不为5、总薪酬在55000-70000之间、激活状态为true、姓名不为David、年龄大于28、角色不是USER的员工
|
|
||||||
var result9_jpa = employeeRepository.findAll((root, query, cb) -> {
|
|
||||||
var avgSalarySubquery = query.subquery(Double.class);
|
|
||||||
var avgSubRoot = avgSalarySubquery.from(Employee.class);
|
|
||||||
avgSalarySubquery.select(cb.avg(avgSubRoot.get(Employee_.salary)));
|
|
||||||
|
|
||||||
var countSubquery = query.subquery(Long.class);
|
|
||||||
var countSubRoot = countSubquery.from(Employee.class);
|
|
||||||
countSubquery.select(cb.count(countSubRoot));
|
|
||||||
|
|
||||||
var salary = root.get(Employee_.salary).as(BigDecimal.class);
|
|
||||||
var bonus = root.get(Employee_.bonus).as(BigDecimal.class);
|
|
||||||
var totalCompensation = cb.sum(salary, cb.coalesce(bonus, cb.literal(new BigDecimal("0.00"))));
|
|
||||||
|
|
||||||
return cb.and(
|
|
||||||
cb.greaterThan(root.get(Employee_.salary).as(Double.class), avgSalarySubquery),
|
|
||||||
cb.notEqual(cb.literal(5L), countSubquery),
|
|
||||||
cb.greaterThan(totalCompensation, cb.literal(new BigDecimal("55000.00"))),
|
|
||||||
cb.lessThan(totalCompensation, cb.literal(new BigDecimal("70000.00"))),
|
|
||||||
cb.isTrue(root.get(Employee_.active)),
|
|
||||||
cb.notEqual(root.get(Employee_.name), "David"),
|
|
||||||
cb.greaterThan(root.get(Employee_.age), 28),
|
|
||||||
cb.notEqual(root.get(Employee_.role), Employee.Role.USER)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
Assert.isTrue(result9_jpa.isEmpty(), "子查询(聚合函数)+ 数学运算失败 (%d)".formatted(result9_jpa.size()));
|
|
||||||
|
|
||||||
formatLog("9. 子查询和聚合函数查询 Fenix");
|
|
||||||
log.info("Fenix框架当前版本不支持以下高级查询特性:");
|
|
||||||
log.info(" - query.subquery() - 子查询");
|
|
||||||
log.info(" - cb.avg(), cb.count(), cb.sum() - 聚合函数");
|
|
||||||
log.info(" - cb.coalesce() - 空值替换函数");
|
|
||||||
log.info(" - cb.sum() - 数值加法运算");
|
|
||||||
log.info("这些是SQL级别的复杂查询,Fenix主要用于动态条件构建");
|
|
||||||
log.info("可以通过doAny使用原生CriteriaBuilder实现部分聚合操作");
|
|
||||||
|
|
||||||
formatLog("9. 子查询和聚合函数查询 QueryDSL");
|
|
||||||
var avgQuery = factory.select(QEmployee.employee.salary.avg());
|
|
||||||
var countQuery = factory.select(QEmployee.employee.count());
|
|
||||||
var result9_querydsl = employeeRepository.findAll(
|
|
||||||
QEmployee.employee.salary.gt(avgQuery)
|
|
||||||
.and(countQuery.ne(5L))
|
|
||||||
.and(QEmployee.employee.salary.add(QEmployee.employee.bonus.coalesce(new BigDecimal("0.00"))).gt(new BigDecimal("55000.00")))
|
|
||||||
.and(QEmployee.employee.salary.add(QEmployee.employee.bonus.coalesce(new BigDecimal("0.00"))).lt(new BigDecimal("70000.00")))
|
|
||||||
.and(QEmployee.employee.active.isTrue())
|
|
||||||
.and(QEmployee.employee.name.ne("David"))
|
|
||||||
.and(QEmployee.employee.age.gt(28))
|
|
||||||
.and(QEmployee.employee.role.ne(Employee.Role.USER))
|
|
||||||
);
|
|
||||||
Assert.isTrue(result9_querydsl.isEmpty(), "子查询(聚合函数)+ 数学运算失败 (%d)".formatted(result9_querydsl.size()));
|
|
||||||
|
|
||||||
formatLog("9. 子查询和聚合函数查询 HQL");
|
|
||||||
var result9_hql = session.createQuery(
|
|
||||||
"""
|
|
||||||
from Employee employee
|
|
||||||
where employee.salary > (select avg(e.salary) from Employee e)
|
|
||||||
and 5 <> (select count(e.id) from Employee e)
|
|
||||||
and employee.salary + coalesce(employee.bonus, 0.00) > 55000.00
|
|
||||||
and employee.salary + coalesce(employee.bonus, 0.00) < 70000.00
|
|
||||||
and employee.active is true
|
|
||||||
and employee.name != 'David'
|
|
||||||
and employee.age > 28
|
|
||||||
and employee.role != com.lanyuanxiaoyao.service.template.database.jpa.entity.Employee.Role.USER
|
|
||||||
""",
|
|
||||||
Employee.class
|
|
||||||
).list();
|
|
||||||
Assert.isTrue(result9_hql.isEmpty(), "子查询(聚合函数)+ 数学运算失败 (%d)".formatted(result9_hql.size()));
|
|
||||||
|
|
||||||
formatLog("10. 排序查询 JPA");
|
|
||||||
// 查找激活状态为true、角色不是ADMIN、年龄大于20、薪资小于60000的员工,按年龄降序、姓名升序排序
|
|
||||||
var result10_jpa = employeeRepository.findAll(
|
|
||||||
(root, query, cb) -> cb.and(
|
|
||||||
cb.isTrue(root.get(Employee_.active)),
|
|
||||||
cb.notEqual(root.get(Employee_.role), Employee.Role.ADMIN),
|
|
||||||
cb.greaterThan(root.get(Employee_.age), 20),
|
|
||||||
cb.lessThan(root.get(Employee_.salary), new BigDecimal("60000.00"))
|
|
||||||
),
|
|
||||||
Sort.by(
|
|
||||||
Sort.Order.desc(Employee_.AGE),
|
|
||||||
Sort.Order.asc(Employee_.NAME)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
Assert.isTrue(result10_jpa.size() == 3, "排序查询失败 (%d)".formatted(result10_jpa.size()));
|
|
||||||
|
|
||||||
formatLog("10. 排序查询 Fenix");
|
|
||||||
log.info("Fenix框架使用Spring Data JPA原生的Sort对象进行排序");
|
|
||||||
log.info("Fenix构建查询条件,Sort对象通过repository.findAll()的第二个参数传入");
|
|
||||||
var result10_fenix = employeeRepository.findAll(
|
|
||||||
builder -> builder.andEquals(Employee.Fields.active, true)
|
|
||||||
.andNotEquals(Employee.Fields.role, Employee.Role.ADMIN)
|
|
||||||
.andGreaterThan(Employee.Fields.age, 20)
|
|
||||||
.andLessThan(Employee.Fields.salary, new BigDecimal("60000.00"))
|
|
||||||
.build(),
|
|
||||||
Sort.by(
|
|
||||||
Sort.Order.desc(Employee.Fields.age),
|
|
||||||
Sort.Order.asc(Employee.Fields.name)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
Assert.isTrue(result10_fenix.size() == 3, "排序查询失败 (%d)".formatted(result10_fenix.size()));
|
|
||||||
|
|
||||||
formatLog("10. 排序查询 QueryDSL");
|
|
||||||
var result10_querydsl = employeeRepository.findAll(
|
|
||||||
QEmployee.employee.active.isTrue()
|
|
||||||
.and(QEmployee.employee.role.ne(Employee.Role.ADMIN))
|
|
||||||
.and(QEmployee.employee.age.gt(20))
|
|
||||||
.and(QEmployee.employee.salary.lt(new BigDecimal("60000.00"))),
|
|
||||||
QEmployee.employee.age.desc(),
|
|
||||||
QEmployee.employee.name.asc()
|
|
||||||
);
|
|
||||||
Assert.isTrue(result10_querydsl.size() == 3, "排序查询失败 (%d)".formatted(result10_querydsl.size()));
|
|
||||||
|
|
||||||
formatLog("10. 排序查询 HQL");
|
|
||||||
var result10_hql = session.createQuery(
|
|
||||||
"""
|
|
||||||
from Employee employee
|
|
||||||
where employee.active is true
|
|
||||||
and employee.role != com.lanyuanxiaoyao.service.template.database.jpa.entity.Employee.Role.ADMIN
|
|
||||||
and employee.age > 20
|
|
||||||
and employee.salary < 60000.00
|
|
||||||
order by employee.age desc, employee.name asc
|
|
||||||
""",
|
|
||||||
Employee.class
|
|
||||||
).list();
|
|
||||||
Assert.isTrue(result10_hql.size() == 3, "排序查询失败 (%d)".formatted(result10_hql.size()));
|
|
||||||
|
|
||||||
formatLog("11. 分页查询 JPA");
|
|
||||||
// 分页查找激活状态为true、角色不是ADMIN、年龄大于20、薪资小于60000的员工,每页2条,按年龄排序
|
|
||||||
var page11_jpa = employeeRepository.findAll(
|
|
||||||
(root, query, cb) -> cb.and(
|
|
||||||
cb.isTrue(root.get(Employee_.active)),
|
|
||||||
cb.notEqual(root.get(Employee_.role), Employee.Role.ADMIN),
|
|
||||||
cb.greaterThan(root.get(Employee_.age), 20),
|
|
||||||
cb.lessThan(root.get(Employee_.salary), new BigDecimal("60000.00"))
|
|
||||||
),
|
|
||||||
PageRequest.of(0, 2, Sort.by(Employee_.AGE))
|
|
||||||
);
|
|
||||||
Assert.isTrue(page11_jpa.getContent().size() == 2, "分页大小不正确 (%d)".formatted(page11_jpa.getContent().size()));
|
|
||||||
Assert.isTrue(page11_jpa.getTotalElements() == 3, "总元素数不正确 (%d)".formatted(page11_jpa.getTotalElements()));
|
|
||||||
|
|
||||||
formatLog("11. 分页查询 Fenix");
|
|
||||||
log.info("Fenix框架使用Spring Data JPA原生的Pageable对象进行分页");
|
|
||||||
log.info("Fenix构建查询条件,Pageable对象通过repository.findAll()的第二个参数传入");
|
|
||||||
var page11_fenix = employeeRepository.findAll(
|
|
||||||
builder -> builder.andEquals(Employee.Fields.active, true)
|
|
||||||
.andNotEquals(Employee.Fields.role, Employee.Role.ADMIN)
|
|
||||||
.andGreaterThan(Employee.Fields.age, 20)
|
|
||||||
.andLessThan(Employee.Fields.salary, new BigDecimal("60000.00"))
|
|
||||||
.build(),
|
|
||||||
PageRequest.of(0, 2, Sort.by(Employee.Fields.age))
|
|
||||||
);
|
|
||||||
Assert.isTrue(page11_fenix.getContent().size() == 2, "分页大小不正确 (%d)".formatted(page11_fenix.getContent().size()));
|
|
||||||
Assert.isTrue(page11_fenix.getTotalElements() == 3, "总元素数不正确 (%d)".formatted(page11_fenix.getTotalElements()));
|
|
||||||
|
|
||||||
formatLog("11. 分页查询 QueryDSL");
|
|
||||||
log.info("QueryDSL支持分页查询:");
|
|
||||||
log.info(" - offset() - 跳过记录数");
|
|
||||||
log.info(" - limit() - 限制记录数");
|
|
||||||
log.info(" - 也可以结合Spring Data JPA的Pageable对象");
|
|
||||||
var page11_querydsl = employeeRepository.findAll(
|
|
||||||
QEmployee.employee.active.isTrue()
|
|
||||||
.and(QEmployee.employee.role.ne(Employee.Role.ADMIN))
|
|
||||||
.and(QEmployee.employee.age.gt(20))
|
|
||||||
.and(QEmployee.employee.salary.lt(new BigDecimal("60000.00"))),
|
|
||||||
PageRequest.of(0, 2, Sort.by(QEmployee.employee.age.getMetadata().getName()))
|
|
||||||
);
|
|
||||||
Assert.isTrue(page11_querydsl.getContent().size() == 2, "分页大小不正确 (%d)".formatted(page11_querydsl.getContent().size()));
|
|
||||||
Assert.isTrue(page11_querydsl.getTotalElements() == 3, "总元素数不正确 (%d)".formatted(page11_querydsl.getTotalElements()));
|
|
||||||
|
|
||||||
formatLog("11. 分页查询 HQL");
|
|
||||||
var page11_hql = session.createQuery(
|
|
||||||
"""
|
|
||||||
from Employee employee
|
|
||||||
where employee.active is true
|
|
||||||
and employee.role != com.lanyuanxiaoyao.service.template.database.jpa.entity.Employee.Role.ADMIN
|
|
||||||
and employee.age > 20
|
|
||||||
and employee.salary < 60000.00
|
|
||||||
order by employee.age
|
|
||||||
""",
|
|
||||||
Employee.class
|
|
||||||
).setFirstResult(0).setMaxResults(2).list();
|
|
||||||
var total11_hql = session.createQuery(
|
|
||||||
"""
|
|
||||||
from Employee employee
|
|
||||||
where employee.active is true
|
|
||||||
and employee.role != com.lanyuanxiaoyao.service.template.database.jpa.entity.Employee.Role.ADMIN
|
|
||||||
and employee.age > 20
|
|
||||||
and employee.salary < 60000.00
|
|
||||||
""",
|
|
||||||
Employee.class
|
|
||||||
).list().size();
|
|
||||||
Assert.isTrue(page11_hql.size() == 2, "分页大小不正确 (%d)".formatted(page11_hql.size()));
|
|
||||||
Assert.isTrue(total11_hql == 3, "总元素数不正确 (%d)".formatted(total11_hql));
|
|
||||||
|
|
||||||
formatLog("12. CASE WHEN 条件表达式查询 JPA");
|
|
||||||
// 查找年龄大于30(Senior)或年龄在25-30之间(Middle)的员工,排除Junior级别的员工
|
|
||||||
var result12_jpa = employeeRepository.findAll((root, query, cb) -> cb.and(
|
|
||||||
cb.equal(
|
|
||||||
cb.selectCase()
|
|
||||||
.when(cb.greaterThan(root.get(Employee_.age), 30), "Senior")
|
|
||||||
.when(cb.between(root.get(Employee_.age), 25, 30), "Middle")
|
|
||||||
.otherwise("Junior"),
|
|
||||||
"Senior"
|
|
||||||
),
|
|
||||||
cb.notEqual(
|
|
||||||
cb.selectCase()
|
|
||||||
.when(cb.greaterThan(root.get(Employee_.age), 30), "Senior")
|
|
||||||
.when(cb.between(root.get(Employee_.age), 25, 30), "Middle")
|
|
||||||
.otherwise("Junior"),
|
|
||||||
"Junior"
|
|
||||||
)
|
|
||||||
));
|
|
||||||
Assert.isTrue(result12_jpa.size() == 2, "CASE WHEN 查询失败 (%d)".formatted(result12_jpa.size()));
|
|
||||||
|
|
||||||
formatLog("12. CASE WHEN 条件表达式查询 Fenix");
|
|
||||||
log.info("Fenix框架当前版本不支持CASE WHEN条件表达式:");
|
|
||||||
log.info(" - cb.selectCase() - SQL CASE WHEN表达式");
|
|
||||||
log.info(" - .when() - CASE WHEN条件分支");
|
|
||||||
log.info(" - .otherwise() - CASE ELSE分支");
|
|
||||||
log.info("CASE WHEN是SQL级别的条件表达式,Fenix主要用于动态条件构建");
|
|
||||||
log.info("可以通过doAny使用原生CriteriaBuilder实现CASE WHEN操作");
|
|
||||||
log.info("注意:由于类型系统的限制,doAny中使用CriteriaBuilder的复杂表达式可能会有类型推断问题");
|
|
||||||
log.info("建议:对于CASE WHEN等复杂查询,直接使用JPA Specification原生方式");
|
|
||||||
var result12_fenix = employeeRepository.findAll(builder -> {
|
|
||||||
return builder.doAny(null, null, (cb, root, fieldName, value) -> {
|
|
||||||
// 使用原生JPA Criteria实现CASE WHEN(简化版本)
|
|
||||||
var caseExpr = cb.selectCase()
|
|
||||||
.when(cb.greaterThan(root.get("age"), 30), "Senior")
|
|
||||||
.when(cb.between(root.get("age"), 25, 30), "Middle")
|
|
||||||
.otherwise("Junior");
|
|
||||||
return cb.and(
|
|
||||||
cb.equal(caseExpr, "Senior"),
|
|
||||||
cb.notEqual(caseExpr, "Junior")
|
|
||||||
);
|
|
||||||
}).build();
|
|
||||||
});
|
|
||||||
Assert.isTrue(result12_fenix.size() == 2, "CASE WHEN 查询失败 (%d)".formatted(result12_fenix.size()));
|
|
||||||
|
|
||||||
formatLog("12. CASE WHEN 条件表达式查询 QueryDSL");
|
|
||||||
var caseExpr = new CaseBuilder()
|
|
||||||
.when(QEmployee.employee.age.gt(30)).then("Senior")
|
|
||||||
.when(QEmployee.employee.age.between(25, 30)).then("Middle")
|
|
||||||
.otherwise("Junior");
|
|
||||||
var result12_querydsl = employeeRepository.findAll(
|
|
||||||
caseExpr.eq("Senior").and(caseExpr.ne("Junior"))
|
|
||||||
);
|
|
||||||
Assert.isTrue(result12_querydsl.size() == 2, "CASE WHEN 查询失败 (%d)".formatted(result12_querydsl.size()));
|
|
||||||
|
|
||||||
formatLog("12. CASE WHEN 条件表达式查询 HQL");
|
|
||||||
var result12_hql = session.createQuery(
|
|
||||||
"""
|
|
||||||
from Employee employee
|
|
||||||
where (case when employee.age > 30 then 'Senior'
|
|
||||||
when employee.age between 25 and 30 then 'Middle'
|
|
||||||
else 'Junior'
|
|
||||||
end) = 'Senior'
|
|
||||||
and (case when employee.age > 30 then 'Senior'
|
|
||||||
when employee.age between 25 and 30 then 'Middle'
|
|
||||||
else 'Junior'
|
|
||||||
end) != 'Junior'
|
|
||||||
""",
|
|
||||||
Employee.class
|
|
||||||
).list();
|
|
||||||
Assert.isTrue(result12_hql.size() == 2, "CASE WHEN 查询失败 (%d)".formatted(result12_hql.size()));
|
|
||||||
|
|
||||||
formatLog("13. 综合多条件查询 JPA");
|
|
||||||
// 综合查询:公司名为TechCorp、技能包含Java、城市为Beijing、技能和爱好非空、属性非空、创建和修改时间不为null、激活状态为true、姓名不为Alice Smith、年龄大于25、角色不是USER
|
|
||||||
var result13_jpa = employeeRepository.findAll((root, query, cb) -> {
|
|
||||||
query.distinct(true);
|
|
||||||
return cb.and(
|
|
||||||
// Company Join 条件
|
|
||||||
cb.equal(root.join(Employee_.company).get(Company_.name), "TechCorp"),
|
|
||||||
cb.notEqual(root.join(Employee_.company).get(Company_.name), "DataInc"),
|
|
||||||
// Skills Join 条件
|
|
||||||
cb.equal(root.join(Employee_.skills).get(Skill_.name), "Java"),
|
|
||||||
cb.notEqual(root.join(Employee_.skills).get(Skill_.name), "MySQL"),
|
|
||||||
// Embedded 对象条件
|
|
||||||
cb.equal(root.get(Employee_.address).get(Address_.city), "Beijing"),
|
|
||||||
cb.notEqual(root.get(Employee_.address).get(Address_.city), "Shanghai"),
|
|
||||||
// 集合条件
|
|
||||||
cb.isNotEmpty(root.get(Employee_.skills)),
|
|
||||||
cb.not(cb.isEmpty(root.get(Employee_.hobbies))),
|
|
||||||
// Map 条件
|
|
||||||
cb.isNotEmpty(root.get(Employee_.properties)),
|
|
||||||
// 日期时间字段查询
|
|
||||||
cb.isNotNull(root.get(Employee_.createdTime)),
|
|
||||||
cb.isNotNull(root.get(Employee_.modifiedTime)),
|
|
||||||
// 其他条件
|
|
||||||
cb.isTrue(root.get(Employee_.active)),
|
|
||||||
cb.notEqual(root.get(Employee_.name), "Alice Smith"),
|
|
||||||
cb.greaterThan(root.get(Employee_.age), 25),
|
|
||||||
cb.notEqual(root.get(Employee_.role), Employee.Role.USER)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
Assert.isTrue(result13_jpa.size() == 1 && result13_jpa.get(0).getName().equals("Alice"), "综合多条件查询失败 (%d)".formatted(result13_jpa.size()));
|
|
||||||
|
|
||||||
formatLog("13. 综合多条件查询 Fenix");
|
|
||||||
log.info("Fenix框架不支持综合多条件查询中的复杂join和集合操作:");
|
|
||||||
log.info(" - 显式join操作(company, skills, properties)");
|
|
||||||
log.info(" - 集合isEmpty/isNotEmpty/isMember操作");
|
|
||||||
log.info(" - Map join操作");
|
|
||||||
log.info(" - 嵌入式对象查询(address.city)");
|
|
||||||
log.info("Fenix主要支持简单的单表字段查询");
|
|
||||||
log.info("可以通过doAny使用原生CriteriaBuilder实现复杂综合查询");
|
|
||||||
|
|
||||||
formatLog("13. 综合多条件查询 QueryDSL");
|
|
||||||
var result13_querydsl = employeeRepository.findAll(
|
|
||||||
// Company Join 条件
|
|
||||||
QEmployee.employee.company().name.eq("TechCorp")
|
|
||||||
.and(QEmployee.employee.company().name.ne("DataInc"))
|
|
||||||
// Skills Join 条件
|
|
||||||
.and(QEmployee.employee.skills.any().name.eq("Java"))
|
|
||||||
.and(QEmployee.employee.skills.any().name.ne("MySQL"))
|
|
||||||
// Embedded 对象条件
|
|
||||||
.and(QEmployee.employee.address().city.eq("Beijing"))
|
|
||||||
.and(QEmployee.employee.address().city.ne("Shanghai"))
|
|
||||||
// 集合条件
|
|
||||||
.and(QEmployee.employee.skills.isNotEmpty())
|
|
||||||
.and(QEmployee.employee.hobbies.isNotEmpty())
|
|
||||||
// Map 条件
|
|
||||||
// .and(QEmployee.employee.properties.isNotEmpty())
|
|
||||||
// 日期时间字段查询
|
|
||||||
.and(QEmployee.employee.createdTime.isNotNull())
|
|
||||||
.and(QEmployee.employee.modifiedTime.isNotNull())
|
|
||||||
// 其他条件
|
|
||||||
.and(QEmployee.employee.active.isTrue())
|
|
||||||
.and(QEmployee.employee.name.ne("Alice Smith"))
|
|
||||||
.and(QEmployee.employee.age.gt(25))
|
|
||||||
.and(QEmployee.employee.role.ne(Employee.Role.USER))
|
|
||||||
);
|
|
||||||
Assert.isTrue(result13_querydsl.size() == 1 && result13_querydsl.get(0).getName().equals("Alice"), "综合多条件查询失败 (%d)".formatted(result13_querydsl.size()));
|
|
||||||
|
|
||||||
formatLog("13. 综合多条件查询 HQL");
|
|
||||||
var result13_hql = session.createQuery(
|
|
||||||
"""
|
|
||||||
select distinct employee
|
|
||||||
from Employee employee
|
|
||||||
join employee.company as company
|
|
||||||
join employee.skills as skill
|
|
||||||
where company.name = 'TechCorp'
|
|
||||||
and company.name != 'DataInc'
|
|
||||||
and skill.name = 'Java'
|
|
||||||
and skill.name != 'MySQL'
|
|
||||||
and employee.address.city = 'Beijing'
|
|
||||||
and employee.address.city != 'Shanghai'
|
|
||||||
and employee.skills is not empty
|
|
||||||
and employee.hobbies is not empty
|
|
||||||
and employee.properties is not empty
|
|
||||||
and employee.createdTime is not null
|
|
||||||
and employee.modifiedTime is not null
|
|
||||||
and employee.active is true
|
|
||||||
and employee.name != 'Alice Smith'
|
|
||||||
and employee.age > 25
|
|
||||||
and employee.role != com.lanyuanxiaoyao.service.template.database.jpa.entity.Employee.Role.USER
|
|
||||||
""",
|
|
||||||
Employee.class
|
|
||||||
).list();
|
|
||||||
Assert.isTrue(result13_hql.size() == 1 && result13_hql.get(0).getName().equals("Alice"), "综合多条件查询失败 (%d)".formatted(result13_hql.size()));
|
|
||||||
|
|
||||||
formatLog("清理测试数据");
|
|
||||||
employeeRepository.deleteAllInBatch();
|
|
||||||
companyRepository.deleteAllInBatch();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void testNative() {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.jpa.controller;
|
|
||||||
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.jpa.entity.Company;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.jpa.service.CompanyService;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.function.Function;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("company")
|
|
||||||
public class CompanyController extends SimpleControllerSupport<Company, CompanyController.SaveItem, CompanyController.ListItem, CompanyController.DetailItem> {
|
|
||||||
public CompanyController(CompanyService service) {
|
|
||||||
super(service);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Function<SaveItem, Company> saveItemMapper() {
|
|
||||||
return item -> {
|
|
||||||
var company = new Company();
|
|
||||||
company.setId(item.id());
|
|
||||||
company.setName(item.name());
|
|
||||||
company.setMembers(item.members());
|
|
||||||
return company;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Function<Company, ListItem> listItemMapper() {
|
|
||||||
return company -> new ListItem(
|
|
||||||
company.getId(),
|
|
||||||
company.getName(),
|
|
||||||
company.getMembers()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Function<Company, DetailItem> detailItemMapper() {
|
|
||||||
return company -> new DetailItem(
|
|
||||||
company.getId(),
|
|
||||||
company.getName(),
|
|
||||||
company.getMembers(),
|
|
||||||
company.getCreatedTime(),
|
|
||||||
company.getModifiedTime()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public record SaveItem(
|
|
||||||
Long id,
|
|
||||||
String name,
|
|
||||||
Integer members
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public record ListItem(
|
|
||||||
Long id,
|
|
||||||
String name,
|
|
||||||
Integer members
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public record DetailItem(
|
|
||||||
Long id,
|
|
||||||
String name,
|
|
||||||
Integer members,
|
|
||||||
LocalDateTime createdTime,
|
|
||||||
LocalDateTime modifiedTime
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.jpa.controller;
|
|
||||||
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.jpa.entity.Employee;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.jpa.service.CompanyService;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.jpa.service.EmployeeService;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.function.Function;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("employee")
|
|
||||||
public class EmployeeController extends SimpleControllerSupport<Employee, EmployeeController.SaveItem, EmployeeController.ListItem, EmployeeController.DetailItem> {
|
|
||||||
private final CompanyService companyService;
|
|
||||||
|
|
||||||
public EmployeeController(EmployeeService service, CompanyService companyService) {
|
|
||||||
super(service);
|
|
||||||
this.companyService = companyService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Function<SaveItem, Employee> saveItemMapper() {
|
|
||||||
return item -> {
|
|
||||||
var employee = new Employee();
|
|
||||||
employee.setId(item.id());
|
|
||||||
employee.setName(item.name());
|
|
||||||
employee.setAge(item.age());
|
|
||||||
employee.setRole(Employee.Role.USER);
|
|
||||||
employee.setCompany(companyService.detailOrThrow(item.companyId()));
|
|
||||||
return employee;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Function<Employee, ListItem> listItemMapper() {
|
|
||||||
return employee -> new ListItem(
|
|
||||||
employee.getId(),
|
|
||||||
employee.getName(),
|
|
||||||
employee.getAge(),
|
|
||||||
employee.getRole()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Function<Employee, DetailItem> detailItemMapper() {
|
|
||||||
return employee -> new DetailItem(
|
|
||||||
employee.getId(),
|
|
||||||
employee.getCompany().getId(),
|
|
||||||
employee.getName(),
|
|
||||||
employee.getAge(),
|
|
||||||
employee.getRole(),
|
|
||||||
employee.getCreatedTime(),
|
|
||||||
employee.getModifiedTime()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public record SaveItem(
|
|
||||||
Long id,
|
|
||||||
Long companyId,
|
|
||||||
String name,
|
|
||||||
Integer age,
|
|
||||||
Employee.Role role
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public record ListItem(
|
|
||||||
Long id,
|
|
||||||
String name,
|
|
||||||
Integer age,
|
|
||||||
Employee.Role role
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public record DetailItem(
|
|
||||||
Long id,
|
|
||||||
Long companyId,
|
|
||||||
String name,
|
|
||||||
Integer age,
|
|
||||||
Employee.Role role,
|
|
||||||
LocalDateTime createdTime,
|
|
||||||
LocalDateTime modifiedTime
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.jpa.controller;
|
|
||||||
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.jpa.entity.Report;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.jpa.service.EmployeeService;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.jpa.service.ReportService;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.function.Function;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("report")
|
|
||||||
public class ReportController extends SimpleControllerSupport<Report, ReportController.SaveItem, ReportController.ListItem, ReportController.DetailItem> {
|
|
||||||
private final EmployeeService employeeService;
|
|
||||||
|
|
||||||
public ReportController(ReportService service, EmployeeService employeeService) {
|
|
||||||
super(service);
|
|
||||||
this.employeeService = employeeService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Function<SaveItem, Report> saveItemMapper() {
|
|
||||||
return item -> {
|
|
||||||
var report = new Report();
|
|
||||||
report.setId(item.id());
|
|
||||||
report.setScore(item.score());
|
|
||||||
report.setLevel(item.level());
|
|
||||||
report.setEmployeeId(item.employeeId());
|
|
||||||
return report;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Function<Report, ListItem> listItemMapper() {
|
|
||||||
return report -> {
|
|
||||||
var employee = employeeService.detailOrThrow(report.getEmployeeId());
|
|
||||||
return new ListItem(
|
|
||||||
report.getId(),
|
|
||||||
employee.getId(),
|
|
||||||
employee.getName(),
|
|
||||||
report.getScore(),
|
|
||||||
report.getLevel()
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Function<Report, DetailItem> detailItemMapper() {
|
|
||||||
return report -> {
|
|
||||||
var employee = employeeService.detailOrThrow(report.getEmployeeId());
|
|
||||||
return new DetailItem(
|
|
||||||
report.getId(),
|
|
||||||
employee.getId(),
|
|
||||||
employee.getName(),
|
|
||||||
report.getScore(),
|
|
||||||
report.getLevel(),
|
|
||||||
report.getCreatedTime(),
|
|
||||||
report.getModifiedTime()
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public record SaveItem(
|
|
||||||
Long id,
|
|
||||||
Double score,
|
|
||||||
Report.Level level,
|
|
||||||
Long employeeId
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public record ListItem(
|
|
||||||
Long id,
|
|
||||||
Long employeeId,
|
|
||||||
String employeeName,
|
|
||||||
Double score,
|
|
||||||
Report.Level level
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public record DetailItem(
|
|
||||||
Long id,
|
|
||||||
Long employeeId,
|
|
||||||
String employeeName,
|
|
||||||
Double score,
|
|
||||||
Report.Level level,
|
|
||||||
LocalDateTime createdTime,
|
|
||||||
LocalDateTime modifiedTime
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.jpa.entity;
|
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
|
||||||
import jakarta.persistence.Embeddable;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.ToString;
|
|
||||||
|
|
||||||
@Embeddable
|
|
||||||
@Setter
|
|
||||||
@Getter
|
|
||||||
@ToString
|
|
||||||
@AllArgsConstructor
|
|
||||||
@NoArgsConstructor
|
|
||||||
@Builder
|
|
||||||
public class Address {
|
|
||||||
@Column(comment = "街道")
|
|
||||||
private String street;
|
|
||||||
|
|
||||||
@Column(comment = "城市")
|
|
||||||
private String city;
|
|
||||||
|
|
||||||
@Column(comment = "省/州")
|
|
||||||
private String state;
|
|
||||||
|
|
||||||
@Column(comment = "邮政编码")
|
|
||||||
private String zipCode;
|
|
||||||
|
|
||||||
@Column(comment = "国家")
|
|
||||||
private String country;
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.jpa.entity;
|
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.EntityListeners;
|
|
||||||
import jakarta.persistence.Table;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.ToString;
|
|
||||||
import lombok.experimental.FieldNameConstants;
|
|
||||||
import org.hibernate.annotations.DynamicInsert;
|
|
||||||
import org.hibernate.annotations.DynamicUpdate;
|
|
||||||
import org.hibernate.annotations.SoftDelete;
|
|
||||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
|
||||||
|
|
||||||
@Setter
|
|
||||||
@Getter
|
|
||||||
@ToString(callSuper = true)
|
|
||||||
@FieldNameConstants
|
|
||||||
@AllArgsConstructor
|
|
||||||
@NoArgsConstructor
|
|
||||||
@Builder
|
|
||||||
@Entity
|
|
||||||
@SoftDelete
|
|
||||||
@DynamicUpdate
|
|
||||||
@DynamicInsert
|
|
||||||
@EntityListeners(AuditingEntityListener.class)
|
|
||||||
@Table(comment = "企业")
|
|
||||||
public class Company extends SimpleEntity {
|
|
||||||
@Column(nullable = false, comment = "名称")
|
|
||||||
private String name;
|
|
||||||
@Column(nullable = false, comment = "成员数")
|
|
||||||
private Integer members;
|
|
||||||
}
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.jpa.entity;
|
|
||||||
|
|
||||||
import jakarta.persistence.CascadeType;
|
|
||||||
import jakarta.persistence.Column;
|
|
||||||
import jakarta.persistence.ConstraintMode;
|
|
||||||
import jakarta.persistence.ElementCollection;
|
|
||||||
import jakarta.persistence.Embedded;
|
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.EntityListeners;
|
|
||||||
import jakarta.persistence.EnumType;
|
|
||||||
import jakarta.persistence.Enumerated;
|
|
||||||
import jakarta.persistence.FetchType;
|
|
||||||
import jakarta.persistence.ForeignKey;
|
|
||||||
import jakarta.persistence.Index;
|
|
||||||
import jakarta.persistence.JoinColumn;
|
|
||||||
import jakarta.persistence.JoinTable;
|
|
||||||
import jakarta.persistence.Lob;
|
|
||||||
import jakarta.persistence.ManyToMany;
|
|
||||||
import jakarta.persistence.ManyToOne;
|
|
||||||
import jakarta.persistence.MapKeyEnumerated;
|
|
||||||
import jakarta.persistence.OrderColumn;
|
|
||||||
import jakarta.persistence.Table;
|
|
||||||
import jakarta.persistence.Version;
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.ToString;
|
|
||||||
import lombok.experimental.FieldNameConstants;
|
|
||||||
import org.hibernate.annotations.ColumnDefault;
|
|
||||||
import org.hibernate.annotations.DynamicInsert;
|
|
||||||
import org.hibernate.annotations.DynamicUpdate;
|
|
||||||
import org.hibernate.annotations.Formula;
|
|
||||||
import org.hibernate.annotations.SoftDelete;
|
|
||||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
|
||||||
|
|
||||||
@Setter
|
|
||||||
@Getter
|
|
||||||
@ToString(callSuper = true)
|
|
||||||
@FieldNameConstants
|
|
||||||
@AllArgsConstructor
|
|
||||||
@NoArgsConstructor
|
|
||||||
@Builder
|
|
||||||
@Entity
|
|
||||||
@SoftDelete
|
|
||||||
@DynamicUpdate
|
|
||||||
@DynamicInsert
|
|
||||||
@EntityListeners(AuditingEntityListener.class)
|
|
||||||
@Table(
|
|
||||||
comment = "员工",
|
|
||||||
indexes = {
|
|
||||||
@Index(name = "idx_employee_name", columnList = "name"),
|
|
||||||
@Index(name = "idx_employee_salary", columnList = "salary"),
|
|
||||||
@Index(name = "idx_employee_active", columnList = "active")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
public class Employee extends SimpleEntity {
|
|
||||||
@Column(nullable = false, length = 100, comment = "名称")
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
@Column(nullable = false, comment = "年龄")
|
|
||||||
private Integer age;
|
|
||||||
|
|
||||||
@Column(nullable = false, comment = "角色")
|
|
||||||
@Enumerated(EnumType.STRING)
|
|
||||||
private Role role;
|
|
||||||
|
|
||||||
@Column(unique = true, length = 50, comment = "工号")
|
|
||||||
private String code;
|
|
||||||
|
|
||||||
@Column(nullable = false, comment = "薪资")
|
|
||||||
private BigDecimal salary;
|
|
||||||
|
|
||||||
@Column(precision = 19, scale = 4, comment = "奖金")
|
|
||||||
private BigDecimal bonus;
|
|
||||||
|
|
||||||
@Column(comment = "是否激活")
|
|
||||||
@ColumnDefault("true")
|
|
||||||
private Boolean active;
|
|
||||||
|
|
||||||
@Lob
|
|
||||||
@Column(comment = "简历(大文本)")
|
|
||||||
private String resume;
|
|
||||||
|
|
||||||
@Version
|
|
||||||
private Long version;
|
|
||||||
|
|
||||||
@Column(insertable = false, updatable = false)
|
|
||||||
@Formula("salary + COALESCE(bonus, 0)")
|
|
||||||
private BigDecimal earnings;
|
|
||||||
|
|
||||||
@Embedded
|
|
||||||
private Address address;
|
|
||||||
|
|
||||||
@ManyToOne
|
|
||||||
@JoinColumn(nullable = false, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
|
|
||||||
@ToString.Exclude
|
|
||||||
private Company company;
|
|
||||||
|
|
||||||
@ManyToMany(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.MERGE})
|
|
||||||
@JoinTable(foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT), inverseForeignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
|
|
||||||
@ToString.Exclude
|
|
||||||
@Builder.Default
|
|
||||||
private Set<Skill> skills = new HashSet<>();
|
|
||||||
|
|
||||||
@ElementCollection
|
|
||||||
@JoinTable(joinColumns = @JoinColumn(nullable = false, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)))
|
|
||||||
@Column(comment = "兴趣")
|
|
||||||
@OrderColumn
|
|
||||||
@ToString.Exclude
|
|
||||||
@Builder.Default
|
|
||||||
private List<String> hobbies = new ArrayList<>();
|
|
||||||
|
|
||||||
@ElementCollection
|
|
||||||
@JoinTable(foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT), inverseForeignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
|
|
||||||
@Column(comment = "属性")
|
|
||||||
@Builder.Default
|
|
||||||
private Map<String, String> properties = new HashMap<>();
|
|
||||||
|
|
||||||
@ElementCollection
|
|
||||||
@JoinTable(foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT), inverseForeignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
|
|
||||||
@MapKeyEnumerated(EnumType.STRING)
|
|
||||||
@Column(nullable = false)
|
|
||||||
@Builder.Default
|
|
||||||
private Map<ConnectionType, String> connections = new HashMap<>();
|
|
||||||
|
|
||||||
public enum Role {
|
|
||||||
USER,
|
|
||||||
ADMIN,
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum ConnectionType {
|
|
||||||
EMAIL,
|
|
||||||
PHONE,
|
|
||||||
ADDRESS,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.jpa.entity;
|
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.EntityListeners;
|
|
||||||
import jakarta.persistence.EnumType;
|
|
||||||
import jakarta.persistence.Enumerated;
|
|
||||||
import jakarta.persistence.Table;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.ToString;
|
|
||||||
import lombok.experimental.FieldNameConstants;
|
|
||||||
import org.hibernate.annotations.DynamicInsert;
|
|
||||||
import org.hibernate.annotations.DynamicUpdate;
|
|
||||||
import org.hibernate.annotations.SoftDelete;
|
|
||||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
|
||||||
|
|
||||||
@Setter
|
|
||||||
@Getter
|
|
||||||
@ToString(callSuper = true)
|
|
||||||
@FieldNameConstants
|
|
||||||
@AllArgsConstructor
|
|
||||||
@NoArgsConstructor
|
|
||||||
@Builder
|
|
||||||
@Entity
|
|
||||||
@SoftDelete
|
|
||||||
@DynamicUpdate
|
|
||||||
@DynamicInsert
|
|
||||||
@EntityListeners(AuditingEntityListener.class)
|
|
||||||
@Table(comment = "报告")
|
|
||||||
public class Report extends SimpleEntity {
|
|
||||||
@Column(nullable = false, comment = "分数")
|
|
||||||
@Builder.Default
|
|
||||||
private Double score = 0.0;
|
|
||||||
@Column(nullable = false, comment = "等级")
|
|
||||||
@Enumerated(EnumType.STRING)
|
|
||||||
private Level level;
|
|
||||||
|
|
||||||
@Column(nullable = false, comment = "员工 ID")
|
|
||||||
private Long employeeId;
|
|
||||||
|
|
||||||
public enum Level {
|
|
||||||
A, B, C, D, E
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.jpa.entity;
|
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.EntityListeners;
|
|
||||||
import jakarta.persistence.Table;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.ToString;
|
|
||||||
import lombok.experimental.FieldNameConstants;
|
|
||||||
import org.hibernate.annotations.DynamicInsert;
|
|
||||||
import org.hibernate.annotations.DynamicUpdate;
|
|
||||||
import org.hibernate.annotations.SoftDelete;
|
|
||||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
|
||||||
|
|
||||||
@Setter
|
|
||||||
@Getter
|
|
||||||
@ToString(callSuper = true)
|
|
||||||
@FieldNameConstants
|
|
||||||
@AllArgsConstructor
|
|
||||||
@NoArgsConstructor
|
|
||||||
@Builder
|
|
||||||
@Entity
|
|
||||||
@SoftDelete
|
|
||||||
@DynamicUpdate
|
|
||||||
@DynamicInsert
|
|
||||||
@EntityListeners(AuditingEntityListener.class)
|
|
||||||
@Table
|
|
||||||
public class Skill extends SimpleEntity {
|
|
||||||
@Column(nullable = false, length = 100, unique = true, comment = "技能名称")
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
@Column(length = 500, comment = "技能描述")
|
|
||||||
private String description;
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.jpa.entity.vo;
|
|
||||||
|
|
||||||
public record EmployeeWithCompanyName(
|
|
||||||
String name,
|
|
||||||
String companyName,
|
|
||||||
Integer age,
|
|
||||||
String role
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.jpa.repository;
|
|
||||||
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.jpa.entity.Company;
|
|
||||||
import org.springframework.stereotype.Repository;
|
|
||||||
|
|
||||||
@Repository
|
|
||||||
public interface CompanyRepository extends SimpleRepository<Company> {
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.jpa.repository;
|
|
||||||
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.jpa.entity.Employee;
|
|
||||||
import java.util.Optional;
|
|
||||||
import org.springframework.data.jpa.domain.Specification;
|
|
||||||
import org.springframework.data.jpa.repository.EntityGraph;
|
|
||||||
import org.springframework.stereotype.Repository;
|
|
||||||
|
|
||||||
@SuppressWarnings("NullableProblems")
|
|
||||||
@Repository
|
|
||||||
public interface EmployeeRepository extends SimpleRepository<Employee> {
|
|
||||||
@EntityGraph(attributePaths = {"company"})
|
|
||||||
@Override
|
|
||||||
Optional<Employee> findOne(Specification<Employee> specification);
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.jpa.repository;
|
|
||||||
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.jpa.entity.Report;
|
|
||||||
import org.springframework.stereotype.Repository;
|
|
||||||
|
|
||||||
@Repository
|
|
||||||
public interface ReportRepository extends SimpleRepository<Report> {
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.jpa.service;
|
|
||||||
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.jpa.entity.Company;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.jpa.repository.CompanyRepository;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class CompanyService extends SimpleServiceSupport<Company> {
|
|
||||||
public CompanyService(CompanyRepository repository) {
|
|
||||||
super(repository);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.jpa.service;
|
|
||||||
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.jpa.entity.Employee;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.jpa.repository.EmployeeRepository;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class EmployeeService extends SimpleServiceSupport<Employee> {
|
|
||||||
public EmployeeService(EmployeeRepository repository) {
|
|
||||||
super(repository);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.jpa.service;
|
|
||||||
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.jpa.entity.Report;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.jpa.repository.ReportRepository;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class ReportService extends SimpleServiceSupport<Report> {
|
|
||||||
public ReportService(ReportRepository repository) {
|
|
||||||
super(repository);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
spring:
|
|
||||||
profiles:
|
|
||||||
include: test
|
|
||||||
datasource:
|
|
||||||
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
|
|
||||||
username: test
|
|
||||||
password: test
|
|
||||||
driver-class-name: org.h2.Driver
|
|
||||||
jpa:
|
|
||||||
generate-ddl: true
|
|
||||||
fenix:
|
|
||||||
print-banner: false
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
||||||
<modelVersion>4.0.0</modelVersion>
|
|
||||||
<parent>
|
|
||||||
<groupId>com.lanyuanxiaoyao</groupId>
|
|
||||||
<artifactId>spring-boot-service-template</artifactId>
|
|
||||||
<version>1.1.0-SNAPSHOT</version>
|
|
||||||
</parent>
|
|
||||||
|
|
||||||
<artifactId>spring-boot-service-template-database-xbatis</artifactId>
|
|
||||||
|
|
||||||
<dependencies>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.lanyuanxiaoyao</groupId>
|
|
||||||
<artifactId>spring-boot-service-template-database-common</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
|
||||||
<scope>provided</scope>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>cn.xbatis</groupId>
|
|
||||||
<artifactId>xbatis-spring-boot-starter</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.lanyuanxiaoyao</groupId>
|
|
||||||
<artifactId>spring-boot-service-template-database-common-test</artifactId>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
|
|
||||||
<build>
|
|
||||||
<plugins>
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-source-plugin</artifactId>
|
|
||||||
<executions>
|
|
||||||
<execution>
|
|
||||||
<phase>package</phase>
|
|
||||||
<goals>
|
|
||||||
<goal>jar-no-fork</goal>
|
|
||||||
</goals>
|
|
||||||
</execution>
|
|
||||||
</executions>
|
|
||||||
</plugin>
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
|
||||||
<configuration>
|
|
||||||
<annotationProcessorPaths>
|
|
||||||
<path>
|
|
||||||
<groupId>org.projectlombok</groupId>
|
|
||||||
<artifactId>lombok</artifactId>
|
|
||||||
</path>
|
|
||||||
</annotationProcessorPaths>
|
|
||||||
</configuration>
|
|
||||||
</plugin>
|
|
||||||
</plugins>
|
|
||||||
</build>
|
|
||||||
</project>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.xbatis.configuration;
|
|
||||||
|
|
||||||
import cn.xbatis.core.incrementer.GeneratorFactory;
|
|
||||||
import cn.xbatis.core.mybatis.mapper.BasicMapper;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.xbatis.entity.SnowflakeIdGenerator;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.xbatis.mapper.MybatisBasicMapper;
|
|
||||||
import org.mybatis.spring.annotation.MapperScan;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
@MapperScan(basePackageClasses = MybatisBasicMapper.class, markerInterface = BasicMapper.class)
|
|
||||||
public class MybatisConfiguration {
|
|
||||||
static {
|
|
||||||
GeneratorFactory.register("snowflake", new SnowflakeIdGenerator());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.xbatis.controller;
|
|
||||||
|
|
||||||
import com.lanyuanxiaoyao.service.template.common.helper.ObjectHelper;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.controller.SimpleController;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.entity.GlobalResponse;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.entity.Query;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.xbatis.entity.SimpleEntity;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.xbatis.service.SimpleServiceSupport;
|
|
||||||
import java.util.function.Function;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 简单控制器支持类,提供基础的CRUD操作实现
|
|
||||||
* <p>
|
|
||||||
* 该类实现了基本的增删改查功能,通过泛型支持不同类型的数据转换。
|
|
||||||
* 子类需要实现对应的Mapper函数来完成实体类与传输对象之间的转换。
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* <h3>设计特点</h3>
|
|
||||||
* <ul>
|
|
||||||
* <li>泛型设计,支持任意实体类型和数据转换</li>
|
|
||||||
* <li>统一的异常处理和事务管理</li>
|
|
||||||
* <li>支持条件查询、分页查询和详情查询</li>
|
|
||||||
* <li>提供抽象的Mapper方法,便于子类实现数据转换逻辑</li>
|
|
||||||
* </ul>
|
|
||||||
*
|
|
||||||
* <h3>使用说明</h3>
|
|
||||||
* <p>子类需要实现以下抽象方法:</p>
|
|
||||||
* <ul>
|
|
||||||
* <li>saveItemMapper(): 保存项到实体的转换函数</li>
|
|
||||||
* <li>listItemMapper(): 实体到列表项的转换函数</li>
|
|
||||||
* <li>detailItemMapper(): 实体到详情项的转换函数</li>
|
|
||||||
* </ul>
|
|
||||||
*
|
|
||||||
* @param <ENTITY> 实体类型,必须继承SimpleEntity
|
|
||||||
* @param <SAVE_ITEM> 保存项类型
|
|
||||||
* @param <LIST_ITEM> 列表项类型
|
|
||||||
* @param <DETAIL_ITEM> 详情项类型
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public abstract class SimpleControllerSupport<ENTITY extends SimpleEntity, SAVE_ITEM, LIST_ITEM, DETAIL_ITEM> implements SimpleController<SAVE_ITEM, LIST_ITEM, DETAIL_ITEM> {
|
|
||||||
protected final SimpleServiceSupport<ENTITY> service;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造函数
|
|
||||||
*
|
|
||||||
* @param service 简单服务支持类实例
|
|
||||||
*/
|
|
||||||
public SimpleControllerSupport(SimpleServiceSupport<ENTITY> service) {
|
|
||||||
this.service = service;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 保存实体对象
|
|
||||||
* <p>
|
|
||||||
* 将保存项转换为实体对象后保存,返回保存后的实体ID。
|
|
||||||
* 支持新增和更新操作,通过事务保证数据一致性。
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param item 需要保存的项
|
|
||||||
* @return 返回保存后的实体ID响应对象,格式:{status: 0, message: "OK", data: 实体ID}
|
|
||||||
* @throws Exception 保存过程中可能抛出的异常
|
|
||||||
*/
|
|
||||||
@Transactional(rollbackFor = Throwable.class)
|
|
||||||
@PostMapping(SAVE)
|
|
||||||
@Override
|
|
||||||
public GlobalResponse<Long> save(@RequestBody SAVE_ITEM item) throws Exception {
|
|
||||||
var mapper = saveItemMapper();
|
|
||||||
return GlobalResponse.responseSuccess(service.save(mapper.apply(item)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有实体列表
|
|
||||||
* <p>
|
|
||||||
* 查询所有记录,不带任何过滤条件,返回分页格式的数据。
|
|
||||||
* 将实体对象转换为列表项对象后返回。
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @return 返回实体列表响应对象,格式:{status: 0, message: "OK", data: {items: [...], total: total}}
|
|
||||||
* @throws Exception 查询过程中可能抛出的异常
|
|
||||||
*/
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
@GetMapping(LIST)
|
|
||||||
@Override
|
|
||||||
public GlobalResponse<GlobalResponse.ListItem<LIST_ITEM>> list() throws Exception {
|
|
||||||
var mapper = listItemMapper();
|
|
||||||
var result = service.list();
|
|
||||||
return GlobalResponse.responseListData(
|
|
||||||
result
|
|
||||||
.stream()
|
|
||||||
.map(entity -> {
|
|
||||||
try {
|
|
||||||
return mapper.apply(entity);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.toList(),
|
|
||||||
result.size()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据查询条件获取实体列表
|
|
||||||
* <p>
|
|
||||||
* 支持复杂的查询条件、排序和分页,返回符合条件的数据。
|
|
||||||
* 将实体对象转换为列表项对象后返回。
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param query 查询条件对象,包含过滤条件、排序规则和分页信息
|
|
||||||
* @return 返回符合条件的实体列表响应对象,格式:{status: 0, message: "OK", data: {items: [...], total: total}}
|
|
||||||
* @throws Exception 查询过程中可能抛出的异常
|
|
||||||
*/
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
@PostMapping(LIST)
|
|
||||||
@Override
|
|
||||||
public GlobalResponse<GlobalResponse.ListItem<LIST_ITEM>> list(@RequestBody Query query) throws Exception {
|
|
||||||
if (ObjectHelper.isNull(query)) {
|
|
||||||
return GlobalResponse.responseListData();
|
|
||||||
}
|
|
||||||
var mapper = listItemMapper();
|
|
||||||
var result = service.list(query);
|
|
||||||
return GlobalResponse.responseListData(
|
|
||||||
result.items()
|
|
||||||
.stream()
|
|
||||||
.map(entity -> {
|
|
||||||
try {
|
|
||||||
return mapper.apply(entity);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.toList(),
|
|
||||||
result.total()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据ID获取实体详情
|
|
||||||
* <p>
|
|
||||||
* 根据主键ID查询单条记录的详细信息,转换为详情项对象后返回。
|
|
||||||
* 如果记录不存在则抛出异常。
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param id 实体主键ID
|
|
||||||
* @return 返回实体详情响应对象,格式:{status: 0, message: "OK", data: 详情数据}
|
|
||||||
* @throws Exception 查询过程中可能抛出的异常
|
|
||||||
*/
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
@GetMapping(DETAIL)
|
|
||||||
@Override
|
|
||||||
public GlobalResponse<DETAIL_ITEM> detail(@PathVariable("id") Long id) throws Exception {
|
|
||||||
var mapper = detailItemMapper();
|
|
||||||
return GlobalResponse.responseSuccess(mapper.apply(service.detailOrThrow(id)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据ID删除实体对象
|
|
||||||
* <p>
|
|
||||||
* 根据主键ID删除指定的记录,执行成功后返回成功响应。
|
|
||||||
* 通过事务保证删除操作的一致性。
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param id 需要删除的实体主键ID
|
|
||||||
* @return 返回删除结果响应对象,格式:{status: 0, message: "OK", data: null}
|
|
||||||
* @throws Exception 删除过程中可能抛出的异常
|
|
||||||
*/
|
|
||||||
@Transactional(rollbackFor = Throwable.class)
|
|
||||||
@GetMapping(REMOVE)
|
|
||||||
@Override
|
|
||||||
public GlobalResponse<Object> remove(@PathVariable("id") Long id) throws Exception {
|
|
||||||
service.remove(id);
|
|
||||||
return GlobalResponse.responseSuccess();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 保存项映射器,将保存项转换为实体对象
|
|
||||||
* <p>
|
|
||||||
* 子类需要实现此方法,定义保存项到实体的转换逻辑。
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @return Function<SAVE_ITEM, ENTITY> 保存项到实体的转换函数
|
|
||||||
*/
|
|
||||||
protected abstract Function<SAVE_ITEM, ENTITY> saveItemMapper();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 列表项映射器,将实体对象转换为列表项
|
|
||||||
* <p>
|
|
||||||
* 子类需要实现此方法,定义实体到列表项的转换逻辑。
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @return Function<ENTITY, LIST_ITEM> 实体到列表项的转换函数
|
|
||||||
*/
|
|
||||||
protected abstract Function<ENTITY, LIST_ITEM> listItemMapper();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 详情项映射器,将实体对象转换为详情项
|
|
||||||
* <p>
|
|
||||||
* 子类需要实现此方法,定义实体到详情项的转换逻辑。
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @return Function<ENTITY, DETAIL_ITEM> 实体到详情项的转换函数
|
|
||||||
*/
|
|
||||||
protected abstract Function<ENTITY, DETAIL_ITEM> detailItemMapper();
|
|
||||||
|
|
||||||
public interface Mapper<S, T> {
|
|
||||||
T map(S source) throws Exception;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.xbatis.entity;
|
|
||||||
|
|
||||||
import cn.xbatis.db.IdAutoType;
|
|
||||||
import cn.xbatis.db.annotations.TableId;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.ToString;
|
|
||||||
import lombok.experimental.FieldNameConstants;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@ToString
|
|
||||||
@FieldNameConstants
|
|
||||||
public class IdOnlyEntity {
|
|
||||||
@TableId(value = IdAutoType.GENERATOR, generator = "snowflake")
|
|
||||||
private Long id;
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.xbatis.entity;
|
|
||||||
|
|
||||||
import cn.xbatis.db.annotations.LogicDelete;
|
|
||||||
import cn.xbatis.db.annotations.LogicDeleteTime;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.ToString;
|
|
||||||
import lombok.experimental.FieldNameConstants;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@ToString
|
|
||||||
@FieldNameConstants
|
|
||||||
public class LogicDeleteEntity extends IdOnlyEntity {
|
|
||||||
@LogicDelete
|
|
||||||
private Boolean deleted = false;
|
|
||||||
@LogicDeleteTime
|
|
||||||
private LocalDateTime deletedTime;
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.xbatis.entity;
|
|
||||||
|
|
||||||
import cn.xbatis.db.annotations.TableField;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.ToString;
|
|
||||||
import lombok.experimental.FieldNameConstants;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@ToString(callSuper = true)
|
|
||||||
@FieldNameConstants
|
|
||||||
public class SimpleEntity extends LogicDeleteEntity {
|
|
||||||
@TableField(defaultValue = "{NOW}", defaultValueFillAlways = true)
|
|
||||||
private LocalDateTime createdTime;
|
|
||||||
@TableField(defaultValue = "{NOW}", defaultValueFillAlways = true, updateDefaultValue = "{NOW}", updateDefaultValueFillAlways = true)
|
|
||||||
private LocalDateTime modifiedTime;
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.xbatis.entity;
|
|
||||||
|
|
||||||
import cn.xbatis.core.incrementer.Generator;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.helper.SnowflakeHelper;
|
|
||||||
|
|
||||||
public class SnowflakeIdGenerator implements Generator<Long> {
|
|
||||||
@Override
|
|
||||||
public Long nextId(Class<?> entity) {
|
|
||||||
return SnowflakeHelper.next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.xbatis.mapper;
|
|
||||||
|
|
||||||
import cn.xbatis.core.mybatis.mapper.BasicMapper;
|
|
||||||
|
|
||||||
public interface MybatisBasicMapper extends BasicMapper {
|
|
||||||
}
|
|
||||||
@@ -1,252 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.xbatis.service;
|
|
||||||
|
|
||||||
import cn.xbatis.core.mybatis.mapper.context.Pager;
|
|
||||||
import cn.xbatis.core.sql.MybatisCmdFactory;
|
|
||||||
import cn.xbatis.core.sql.executor.chain.QueryChain;
|
|
||||||
import com.lanyuanxiaoyao.service.template.common.helper.ObjectHelper;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.entity.Page;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.entity.Query;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.exception.IdNotFoundException;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.service.QueryParser;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.service.SimpleService;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.xbatis.entity.SimpleEntity;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.xbatis.mapper.MybatisBasicMapper;
|
|
||||||
import db.sql.api.cmd.LikeMode;
|
|
||||||
import db.sql.api.impl.cmd.basic.OrderByDirection;
|
|
||||||
import db.sql.api.impl.cmd.struct.Where;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.mapstruct.Named;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
public abstract class SimpleServiceSupport<ENTITY extends SimpleEntity> implements SimpleService<ENTITY> {
|
|
||||||
private static final int DEFAULT_PAGE_INDEX = 1;
|
|
||||||
private static final int DEFAULT_PAGE_SIZE = 10;
|
|
||||||
|
|
||||||
protected final MybatisBasicMapper mapper;
|
|
||||||
private final Class<ENTITY> target;
|
|
||||||
|
|
||||||
public SimpleServiceSupport(Class<ENTITY> target, MybatisBasicMapper mapper) {
|
|
||||||
this.target = target;
|
|
||||||
this.mapper = mapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional(rollbackFor = Throwable.class)
|
|
||||||
@Override
|
|
||||||
public Long save(ENTITY entity) {
|
|
||||||
mapper.saveOrUpdate(entity);
|
|
||||||
return entity.getId();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional(rollbackFor = Throwable.class)
|
|
||||||
@Override
|
|
||||||
public void save(Iterable<ENTITY> entities) {
|
|
||||||
mapper.saveOrUpdate(entities);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Long count() {
|
|
||||||
return (long) mapper.countAll(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<ENTITY> list() {
|
|
||||||
return mapper.listAll(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<ENTITY> list(Set<Long> ids) {
|
|
||||||
return mapper.listByIds(target, ids);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void commonPredicates(Where where) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Page<ENTITY> list(Query query) {
|
|
||||||
var chain = QueryChain.of(mapper, target);
|
|
||||||
var factory = chain.$();
|
|
||||||
|
|
||||||
var paging = Pager.<ENTITY>of(DEFAULT_PAGE_INDEX, DEFAULT_PAGE_SIZE);
|
|
||||||
if (ObjectHelper.isNotNull(query.page())) {
|
|
||||||
var index = Math.max(ObjectHelper.defaultIfNull(query.page().index(), DEFAULT_PAGE_INDEX), 1);
|
|
||||||
var size = Math.max(ObjectHelper.defaultIfNull(query.page().size(), DEFAULT_PAGE_SIZE), 1);
|
|
||||||
paging = Pager.of(index, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ObjectHelper.isNotEmpty(query.sort())) {
|
|
||||||
query.sort().forEach(sort -> chain.orderBy(OrderByDirection.valueOf(sort.direction().name()), sort.column()));
|
|
||||||
}
|
|
||||||
|
|
||||||
var where = chain.where();
|
|
||||||
commonPredicates(where);
|
|
||||||
new XBatisQueryParser<>(query.query(), where, target, factory).build();
|
|
||||||
|
|
||||||
var pager = chain.paging(paging);
|
|
||||||
|
|
||||||
return new Page<>(pager.getResults(), pager.getTotal());
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<ENTITY> detailOptional(Long id) {
|
|
||||||
if (ObjectHelper.isNull(id)) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
return mapper.getOptionalById(target, id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Named("detail")
|
|
||||||
@Override
|
|
||||||
public ENTITY detail(Long id) {
|
|
||||||
return detailOptional(id).orElse(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Named("detailOrThrow")
|
|
||||||
@Override
|
|
||||||
public ENTITY detailOrThrow(Long id) {
|
|
||||||
return detailOptional(id).orElseThrow(() -> new IdNotFoundException(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional(rollbackFor = Throwable.class)
|
|
||||||
@Override
|
|
||||||
public void remove(Long id) {
|
|
||||||
mapper.deleteById(target, id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional(rollbackFor = Throwable.class)
|
|
||||||
@Override
|
|
||||||
public void remove(Set<Long> ids) {
|
|
||||||
mapper.deleteByIds(target, ids);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class XBatisQueryParser<ENTITY> extends QueryParser<Where> {
|
|
||||||
private final Class<ENTITY> target;
|
|
||||||
private final MybatisCmdFactory factory;
|
|
||||||
|
|
||||||
private XBatisQueryParser(Query.Queryable queryable, Where where, Class<ENTITY> target, MybatisCmdFactory factory) {
|
|
||||||
super(queryable, where);
|
|
||||||
this.target = target;
|
|
||||||
this.factory = factory;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void nullEqual(Query.Queryable queryable, Where where) {
|
|
||||||
queryable.nullEqual().forEach(column -> where.isNull(factory.field(target, column)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void notNullEqual(Query.Queryable queryable, Where where) {
|
|
||||||
queryable.notNullEqual().forEach(column -> where.isNotNull(factory.field(target, column)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void empty(Query.Queryable queryable, Where where) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void notEmpty(Query.Queryable queryable, Where where) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void equal(Query.Queryable queryable, Where where) {
|
|
||||||
queryable.equal().forEach((column, value) -> where.eq(factory.field(target, column), value));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void notEqual(Query.Queryable queryable, Where where) {
|
|
||||||
queryable.notEqual().forEach((column, value) -> where.ne(factory.field(target, column), value));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void like(Query.Queryable queryable, Where where) {
|
|
||||||
queryable.like().forEach((column, value) -> where.like(LikeMode.NONE, factory.field(target, column), value));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void notLike(Query.Queryable queryable, Where where) {
|
|
||||||
queryable.notLike().forEach((column, value) -> where.notLike(LikeMode.NONE, factory.field(target, column), value));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void contain(Query.Queryable queryable, Where where) {
|
|
||||||
queryable.contain().forEach((column, value) -> where.like(factory.field(target, column), value));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void notContain(Query.Queryable queryable, Where where) {
|
|
||||||
queryable.notContain().forEach((column, value) -> where.notLike(factory.field(target, column), value));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void startWith(Query.Queryable queryable, Where where) {
|
|
||||||
queryable.startWith().forEach((column, value) -> where.like(LikeMode.RIGHT, factory.field(target, column), value));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void notStartWith(Query.Queryable queryable, Where where) {
|
|
||||||
queryable.notStartWith().forEach((column, value) -> where.notLike(LikeMode.RIGHT, factory.field(target, column), value));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void endWith(Query.Queryable queryable, Where where) {
|
|
||||||
queryable.endWith().forEach((column, value) -> where.like(LikeMode.LEFT, factory.field(target, column), value));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void notEndWith(Query.Queryable queryable, Where where) {
|
|
||||||
queryable.notEndWith().forEach((column, value) -> where.notLike(LikeMode.LEFT, factory.field(target, column), value));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void great(Query.Queryable queryable, Where where) {
|
|
||||||
queryable.great().forEach((column, value) -> where.gt(factory.field(target, column), value));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void less(Query.Queryable queryable, Where where) {
|
|
||||||
queryable.less().forEach((column, value) -> where.lt(factory.field(target, column), value));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void greatEqual(Query.Queryable queryable, Where where) {
|
|
||||||
queryable.greatEqual().forEach((column, value) -> where.gte(factory.field(target, column), value));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void lessEqual(Query.Queryable queryable, Where where) {
|
|
||||||
queryable.lessEqual().forEach((column, value) -> where.lte(factory.field(target, column), value));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void inside(Query.Queryable queryable, Where where) {
|
|
||||||
queryable.inside()
|
|
||||||
.entrySet()
|
|
||||||
.stream()
|
|
||||||
.filter(entry -> ObjectHelper.isNotEmpty(entry.getValue()))
|
|
||||||
.forEach(entry -> where.in(factory.field(target, entry.getKey()), entry.getValue()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void notInside(Query.Queryable queryable, Where where) {
|
|
||||||
queryable.notInside()
|
|
||||||
.entrySet()
|
|
||||||
.stream()
|
|
||||||
.filter(entry -> ObjectHelper.isNotEmpty(entry.getValue()))
|
|
||||||
.forEach(entry -> where.notIn(factory.field(target, entry.getKey()), entry.getValue()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void between(Query.Queryable queryable, Where where) {
|
|
||||||
queryable.between().forEach((column, value) -> where.between(factory.field(target, column), value.start(), value.end()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void notBetween(Query.Queryable queryable, Where where) {
|
|
||||||
queryable.notBetween().forEach((column, value) -> where.notBetween(factory.field(target, column), value.start(), value.end()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
create table if not exists Company
|
|
||||||
(
|
|
||||||
id bigint primary key,
|
|
||||||
name varchar(255) not null,
|
|
||||||
members int not null,
|
|
||||||
created_time timestamp not null,
|
|
||||||
modified_time timestamp not null,
|
|
||||||
deleted tinyint not null default 0,
|
|
||||||
deleted_time timestamp
|
|
||||||
);
|
|
||||||
|
|
||||||
create table if not exists Employee
|
|
||||||
(
|
|
||||||
id bigint primary key,
|
|
||||||
name varchar(255) not null,
|
|
||||||
age int not null,
|
|
||||||
company_id bigint not null,
|
|
||||||
created_time timestamp not null,
|
|
||||||
modified_time timestamp not null,
|
|
||||||
deleted tinyint not null default 0,
|
|
||||||
deleted_time timestamp
|
|
||||||
);
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.xbatis;
|
|
||||||
|
|
||||||
import cn.xbatis.core.sql.executor.chain.QueryChain;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.test.AbstractTestApplication;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.xbatis.entity.Company;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.xbatis.entity.Employee;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.xbatis.entity.vo.EmployeeWithCompanyName;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.xbatis.mapper.MybatisBasicMapper;
|
|
||||||
import db.sql.api.cmd.LikeMode;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.mybatis.spring.annotation.MapperScan;
|
|
||||||
import org.springframework.boot.SpringApplication;
|
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
|
||||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
|
||||||
import org.springframework.context.event.EventListener;
|
|
||||||
import org.springframework.util.Assert;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@MapperScan("com.lanyuanxiaoyao.service.template.database.xbatis.mapper")
|
|
||||||
@SpringBootApplication
|
|
||||||
public class TestApplication extends AbstractTestApplication {
|
|
||||||
private final MybatisBasicMapper mapper;
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
|
||||||
SpringApplication.run(TestApplication.class, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventListener(ApplicationReadyEvent.class)
|
|
||||||
public void runTests() {
|
|
||||||
testCrud();
|
|
||||||
testDelete();
|
|
||||||
testQuery();
|
|
||||||
|
|
||||||
System.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void testDelete() {
|
|
||||||
formatLog("Delete");
|
|
||||||
saveItem("company", randomCompany());
|
|
||||||
saveItem("company", randomCompany());
|
|
||||||
mapper.deleteAll(Company.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void testQuery() {
|
|
||||||
formatLog("Added");
|
|
||||||
var company1 = Company.builder().name(randomString(5)).members(randomInt(100)).build();
|
|
||||||
mapper.saveOrUpdate(company1);
|
|
||||||
var company2 = Company.builder().name(randomString(5)).members(randomInt(100)).build();
|
|
||||||
mapper.saveOrUpdate(company2);
|
|
||||||
var employee1 = Employee.builder().name("Tom").age(randomInt(100)).companyId(company1.getId()).build();
|
|
||||||
mapper.saveOrUpdate(employee1);
|
|
||||||
var employee2 = Employee.builder().name(randomString(10)).age(randomInt(100)).companyId(company2.getId()).build();
|
|
||||||
mapper.saveOrUpdate(employee2);
|
|
||||||
|
|
||||||
formatLog("Query");
|
|
||||||
var employees1 = QueryChain.of(mapper, Employee.class)
|
|
||||||
.isNotNull(Employee::getName)
|
|
||||||
.eq(Employee::getName, "Tom")
|
|
||||||
.like(Employee::getName, "To")
|
|
||||||
.like(LikeMode.RIGHT, Employee::getName, "To")
|
|
||||||
.like(LikeMode.LEFT, Employee::getName, "om")
|
|
||||||
.lt(Employee::getAge, 200)
|
|
||||||
.gt(Employee::getAge, 0)
|
|
||||||
.in(Employee::getName, "Tom", "Mike")
|
|
||||||
.between(Employee::getAge, 0, 200)
|
|
||||||
.list();
|
|
||||||
Assert.isTrue(employees1.size() == 1, "查询数量错误");
|
|
||||||
|
|
||||||
formatLog("Query Join");
|
|
||||||
var employees2 = QueryChain.of(mapper, Employee.class)
|
|
||||||
.select(Employee::getName, Employee::getAge)
|
|
||||||
.select(Company::getId, c -> c.as(EmployeeWithCompanyName::getCompanyName))
|
|
||||||
.leftJoin(Employee.class, Company.class, on -> on.eq(Employee::getCompanyId, Company::getId).gt(Company::getMembers, 0))
|
|
||||||
.eq(Employee::getName, "Tom")
|
|
||||||
.lt(Company::getMembers, 200)
|
|
||||||
.returnType(EmployeeWithCompanyName.class)
|
|
||||||
.list();
|
|
||||||
Assert.isTrue(employees2.size() == 1, "查询数量错误");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.xbatis.controller;
|
|
||||||
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.xbatis.entity.Company;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.xbatis.service.CompanyService;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.function.Function;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("company")
|
|
||||||
public class CompanyController extends SimpleControllerSupport<Company, CompanyController.SaveItem, CompanyController.ListItem, CompanyController.DetailItem> {
|
|
||||||
public CompanyController(CompanyService service) {
|
|
||||||
super(service);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Function<SaveItem, Company> saveItemMapper() {
|
|
||||||
return item -> {
|
|
||||||
var company = new Company();
|
|
||||||
company.setId(item.id());
|
|
||||||
company.setName(item.name());
|
|
||||||
company.setMembers(item.members());
|
|
||||||
return company;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Function<Company, ListItem> listItemMapper() {
|
|
||||||
return company -> new ListItem(
|
|
||||||
company.getId(),
|
|
||||||
company.getName(),
|
|
||||||
company.getMembers()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Function<Company, DetailItem> detailItemMapper() {
|
|
||||||
return company -> new DetailItem(
|
|
||||||
company.getId(),
|
|
||||||
company.getName(),
|
|
||||||
company.getMembers(),
|
|
||||||
company.getCreatedTime(),
|
|
||||||
company.getModifiedTime()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public record SaveItem(
|
|
||||||
Long id,
|
|
||||||
String name,
|
|
||||||
Integer members
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public record ListItem(
|
|
||||||
Long id,
|
|
||||||
String name,
|
|
||||||
Integer members
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public record DetailItem(
|
|
||||||
Long id,
|
|
||||||
String name,
|
|
||||||
Integer members,
|
|
||||||
LocalDateTime createdTime,
|
|
||||||
LocalDateTime modifiedTime
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.xbatis.entity;
|
|
||||||
|
|
||||||
import cn.xbatis.db.annotations.Table;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.ToString;
|
|
||||||
import lombok.experimental.FieldNameConstants;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@ToString(callSuper = true)
|
|
||||||
@FieldNameConstants
|
|
||||||
@AllArgsConstructor
|
|
||||||
@NoArgsConstructor
|
|
||||||
@Builder
|
|
||||||
@Table
|
|
||||||
public class Company extends SimpleEntity {
|
|
||||||
private String name;
|
|
||||||
private Integer members;
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.xbatis.entity;
|
|
||||||
|
|
||||||
import cn.xbatis.db.annotations.Table;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.ToString;
|
|
||||||
import lombok.experimental.FieldNameConstants;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@ToString(callSuper = true)
|
|
||||||
@FieldNameConstants
|
|
||||||
@AllArgsConstructor
|
|
||||||
@NoArgsConstructor
|
|
||||||
@Builder
|
|
||||||
@Table
|
|
||||||
public class Employee extends SimpleEntity {
|
|
||||||
private String name;
|
|
||||||
private Integer age;
|
|
||||||
|
|
||||||
private Long companyId;
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.xbatis.entity.vo;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.ToString;
|
|
||||||
import lombok.experimental.FieldNameConstants;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@ToString(callSuper = true)
|
|
||||||
@FieldNameConstants
|
|
||||||
public class EmployeeWithCompanyName {
|
|
||||||
private String name;
|
|
||||||
private Integer age;
|
|
||||||
private String companyName;
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.xbatis.service;
|
|
||||||
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.xbatis.entity.Company;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.xbatis.mapper.MybatisBasicMapper;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class CompanyService extends SimpleServiceSupport<Company> {
|
|
||||||
public CompanyService(MybatisBasicMapper mapper) {
|
|
||||||
super(Company.class, mapper);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.xbatis.service;
|
|
||||||
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.xbatis.entity.Employee;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.xbatis.mapper.MybatisBasicMapper;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class EmployeeService extends SimpleServiceSupport<Employee> {
|
|
||||||
public EmployeeService(MybatisBasicMapper mapper) {
|
|
||||||
super(Employee.class, mapper);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
spring:
|
|
||||||
profiles:
|
|
||||||
include: test
|
|
||||||
datasource:
|
|
||||||
url: "jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=MySQL;DATABASE_TO_LOWER=TRUE;INIT=runscript from '/Users/lanyuanxiaoyao/Project/IdeaProjects/spring-boot-service-template/spring-boot-service-template-database/spring-boot-service-template-database-xbatis/src/test/initial.sql'"
|
|
||||||
username: test
|
|
||||||
password: test
|
|
||||||
driver-class-name: org.h2.Driver
|
|
||||||
mybatis:
|
|
||||||
configuration:
|
|
||||||
banner: false
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.lanyuanxiaoyao.service.template.database.configuration;
|
||||||
|
|
||||||
|
import com.querydsl.jpa.impl.JPAQueryFactory;
|
||||||
|
import jakarta.persistence.EntityManager;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class QueryDSLConfiguration {
|
||||||
|
@Bean
|
||||||
|
public JPAQueryFactory jpaQueryFactory(EntityManager manager) {
|
||||||
|
return new JPAQueryFactory(manager);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.common.controller;
|
package com.lanyuanxiaoyao.service.template.database.controller;
|
||||||
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.entity.GlobalResponse;
|
import com.lanyuanxiaoyao.service.template.database.entity.GlobalResponse;
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.entity.Query;
|
import com.lanyuanxiaoyao.service.template.database.entity.Query;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询控制器接口,用于定义统一的查询实体详情和列表的接口规范
|
* 查询控制器接口,用于定义统一的查询实体详情和列表的接口规范
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.common.controller;
|
package com.lanyuanxiaoyao.service.template.database.controller;
|
||||||
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.entity.GlobalResponse;
|
import com.lanyuanxiaoyao.service.template.database.entity.GlobalResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除控制器接口,用于定义统一的删除实体对象的接口规范
|
* 删除控制器接口,用于定义统一的删除实体对象的接口规范
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.common.controller;
|
package com.lanyuanxiaoyao.service.template.database.controller;
|
||||||
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.entity.GlobalResponse;
|
import com.lanyuanxiaoyao.service.template.database.entity.GlobalResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存控制器接口,用于定义统一的保存实体对象的接口规范
|
* 保存控制器接口,用于定义统一的保存实体对象的接口规范
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.common.controller;
|
package com.lanyuanxiaoyao.service.template.database.controller;
|
||||||
|
|
||||||
public interface SimpleController<SAVE_ITEM, LIST_ITEM, DETAIL_ITEM> extends SaveController<SAVE_ITEM>, QueryController<LIST_ITEM, DETAIL_ITEM>, RemoveController {
|
public interface SimpleController<SAVE_ITEM, LIST_ITEM, DETAIL_ITEM> extends SaveController<SAVE_ITEM>, QueryController<LIST_ITEM, DETAIL_ITEM>, RemoveController {
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.jpa.controller;
|
package com.lanyuanxiaoyao.service.template.database.controller;
|
||||||
|
|
||||||
import com.lanyuanxiaoyao.service.template.common.helper.ObjectHelper;
|
import com.lanyuanxiaoyao.service.template.common.helper.ObjectHelper;
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.controller.SimpleController;
|
import com.lanyuanxiaoyao.service.template.database.entity.GlobalResponse;
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.entity.GlobalResponse;
|
import com.lanyuanxiaoyao.service.template.database.entity.Query;
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.entity.Query;
|
import com.lanyuanxiaoyao.service.template.database.entity.SimpleEntity;
|
||||||
import com.lanyuanxiaoyao.service.template.database.jpa.entity.SimpleEntity;
|
import com.lanyuanxiaoyao.service.template.database.service.SimpleServiceSupport;
|
||||||
import com.lanyuanxiaoyao.service.template.database.jpa.service.SimpleServiceSupport;
|
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.common.entity;
|
package com.lanyuanxiaoyao.service.template.database.entity;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.jpa.entity;
|
package com.lanyuanxiaoyao.service.template.database.entity;
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
import jakarta.persistence.Column;
|
||||||
import jakarta.persistence.EntityListeners;
|
import jakarta.persistence.EntityListeners;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.common.entity;
|
package com.lanyuanxiaoyao.service.template.database.entity;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.common.entity;
|
package com.lanyuanxiaoyao.service.template.database.entity;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.jpa.entity;
|
package com.lanyuanxiaoyao.service.template.database.entity;
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
import jakarta.persistence.Column;
|
||||||
import jakarta.persistence.EntityListeners;
|
import jakarta.persistence.EntityListeners;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.jpa.entity;
|
package com.lanyuanxiaoyao.service.template.database.entity;
|
||||||
|
|
||||||
import java.lang.annotation.ElementType;
|
import java.lang.annotation.ElementType;
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.jpa.entity;
|
package com.lanyuanxiaoyao.service.template.database.entity;
|
||||||
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.helper.SnowflakeHelper;
|
import com.lanyuanxiaoyao.service.template.database.helper.SnowflakeHelper;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.hibernate.engine.spi.SharedSessionContractImplementor;
|
import org.hibernate.engine.spi.SharedSessionContractImplementor;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.common.exception;
|
package com.lanyuanxiaoyao.service.template.database.exception;
|
||||||
|
|
||||||
public class IdNotFoundException extends RuntimeException {
|
public class IdNotFoundException extends RuntimeException {
|
||||||
public IdNotFoundException(Long id) {
|
public IdNotFoundException(Long id) {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.common.exception;
|
package com.lanyuanxiaoyao.service.template.database.exception;
|
||||||
|
|
||||||
public class NotCollectionException extends RuntimeException {
|
public class NotCollectionException extends RuntimeException {
|
||||||
public NotCollectionException(String variable) {
|
public NotCollectionException(String variable) {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.common.exception;
|
package com.lanyuanxiaoyao.service.template.database.exception;
|
||||||
|
|
||||||
public class NotComparableException extends RuntimeException {
|
public class NotComparableException extends RuntimeException {
|
||||||
public NotComparableException(String variable) {
|
public NotComparableException(String variable) {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.common.exception;
|
package com.lanyuanxiaoyao.service.template.database.exception;
|
||||||
|
|
||||||
public class NotStringException extends RuntimeException {
|
public class NotStringException extends RuntimeException {
|
||||||
public NotStringException(String variable) {
|
public NotStringException(String variable) {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.jpa.helper;
|
package com.lanyuanxiaoyao.service.template.database.helper;
|
||||||
|
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -115,7 +115,7 @@ public class DatabaseHelper {
|
|||||||
package %s.repository;
|
package %s.repository;
|
||||||
|
|
||||||
import %s;
|
import %s;
|
||||||
import com.lanyuanxiaoyao.service.template.repository.SimpleRepository;
|
import com.lanyuanxiaoyao.service.template.database.repository.SimpleRepository;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
@@ -134,7 +134,7 @@ public class DatabaseHelper {
|
|||||||
|
|
||||||
import %s;
|
import %s;
|
||||||
import %s.repository.%sRepository;
|
import %s.repository.%sRepository;
|
||||||
import com.lanyuanxiaoyao.service.template.service.SimpleServiceSupport;
|
import com.lanyuanxiaoyao.service.template.database.service.SimpleServiceSupport;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -158,7 +158,7 @@ public class DatabaseHelper {
|
|||||||
|
|
||||||
import %s;
|
import %s;
|
||||||
import %s.service.%sService;
|
import %s.service.%sService;
|
||||||
import com.lanyuanxiaoyao.service.template.controller.SimpleControllerSupport;
|
import com.lanyuanxiaoyao.service.template.database.controller.SimpleControllerSupport;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
@@ -197,7 +197,7 @@ public class DatabaseHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
""".formatted(projectRootPackage, className, projectRootPackage, name, camelConvert(name), name, name, name, name, name, name, name, name, name, name));
|
""".formatted(projectRootPackage, className, projectRootPackage, name, camelConvert(name), name, name, name, name, name, name, name, name, name, name, name));
|
||||||
}
|
}
|
||||||
} catch (ClassNotFoundException e) {
|
} catch (ClassNotFoundException e) {
|
||||||
throw new RuntimeException("Failed to load entity class: " + className, e);
|
throw new RuntimeException("Failed to load entity class: " + className, e);
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.common.helper;
|
package com.lanyuanxiaoyao.service.template.database.helper;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.jpa.repository;
|
package com.lanyuanxiaoyao.service.template.database.repository;
|
||||||
|
|
||||||
import com.blinkfox.fenix.jpa.FenixJpaRepository;
|
import com.blinkfox.fenix.jpa.FenixJpaRepository;
|
||||||
import com.blinkfox.fenix.specification.FenixJpaSpecificationExecutor;
|
import com.blinkfox.fenix.specification.FenixJpaSpecificationExecutor;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.common.service;
|
package com.lanyuanxiaoyao.service.template.database.service;
|
||||||
|
|
||||||
import com.lanyuanxiaoyao.service.template.common.helper.ObjectHelper;
|
import com.lanyuanxiaoyao.service.template.common.helper.ObjectHelper;
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.entity.Query;
|
import com.lanyuanxiaoyao.service.template.database.entity.Query;
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.common.service;
|
package com.lanyuanxiaoyao.service.template.database.service;
|
||||||
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.entity.Page;
|
import com.lanyuanxiaoyao.service.template.database.entity.Page;
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.entity.Query;
|
import com.lanyuanxiaoyao.service.template.database.entity.Query;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.common.service;
|
package com.lanyuanxiaoyao.service.template.database.service;
|
||||||
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.common.service;
|
package com.lanyuanxiaoyao.service.template.database.service;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存服务接口,用于定义统一的保存实体对象的服务规范
|
* 保存服务接口,用于定义统一的保存实体对象的服务规范
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.common.service;
|
package com.lanyuanxiaoyao.service.template.database.service;
|
||||||
|
|
||||||
public interface SimpleService<ENTITY> extends SaveService<ENTITY>, QueryService<ENTITY>, RemoveService<ENTITY> {
|
public interface SimpleService<ENTITY> extends SaveService<ENTITY>, QueryService<ENTITY>, RemoveService<ENTITY> {
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,15 @@
|
|||||||
package com.lanyuanxiaoyao.service.template.database.jpa.service;
|
package com.lanyuanxiaoyao.service.template.database.service;
|
||||||
|
|
||||||
import com.lanyuanxiaoyao.service.template.common.helper.ObjectHelper;
|
import com.lanyuanxiaoyao.service.template.common.helper.ObjectHelper;
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.entity.Page;
|
import com.lanyuanxiaoyao.service.template.database.entity.Page;
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.entity.Query;
|
import com.lanyuanxiaoyao.service.template.database.entity.Query;
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.exception.IdNotFoundException;
|
import com.lanyuanxiaoyao.service.template.database.exception.IdNotFoundException;
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.exception.NotCollectionException;
|
import com.lanyuanxiaoyao.service.template.database.exception.NotCollectionException;
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.exception.NotComparableException;
|
import com.lanyuanxiaoyao.service.template.database.exception.NotComparableException;
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.exception.NotStringException;
|
import com.lanyuanxiaoyao.service.template.database.exception.NotStringException;
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.service.QueryParser;
|
import com.lanyuanxiaoyao.service.template.database.entity.IdOnlyEntity;
|
||||||
import com.lanyuanxiaoyao.service.template.database.common.service.SimpleService;
|
import com.lanyuanxiaoyao.service.template.database.entity.SimpleEntity;
|
||||||
import com.lanyuanxiaoyao.service.template.database.jpa.entity.IdOnlyEntity;
|
import com.lanyuanxiaoyao.service.template.database.repository.SimpleRepository;
|
||||||
import com.lanyuanxiaoyao.service.template.database.jpa.entity.SimpleEntity;
|
|
||||||
import com.lanyuanxiaoyao.service.template.database.jpa.repository.SimpleRepository;
|
|
||||||
import jakarta.persistence.criteria.CriteriaBuilder;
|
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||||
import jakarta.persistence.criteria.CriteriaQuery;
|
import jakarta.persistence.criteria.CriteriaQuery;
|
||||||
import jakarta.persistence.criteria.Path;
|
import jakarta.persistence.criteria.Path;
|
||||||
@@ -77,16 +75,6 @@ public abstract class SimpleServiceSupport<ENTITY extends SimpleEntity> implemen
|
|||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 保存实体对象
|
|
||||||
* <p>
|
|
||||||
* 使用saveOrUpdateByNotNullProperties方法保存实体,只更新非空字段。
|
|
||||||
* 该方法具有事务性,遇到任何异常都会回滚。
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param entity 需要保存的实体对象
|
|
||||||
* @return 返回保存后的实体ID
|
|
||||||
*/
|
|
||||||
@Transactional(rollbackFor = Throwable.class)
|
@Transactional(rollbackFor = Throwable.class)
|
||||||
@Override
|
@Override
|
||||||
public Long save(ENTITY entity) {
|
public Long save(ENTITY entity) {
|
||||||
@@ -94,56 +82,25 @@ public abstract class SimpleServiceSupport<ENTITY extends SimpleEntity> implemen
|
|||||||
return entity.getId();
|
return entity.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量保存实体对象集合
|
|
||||||
* <p>
|
|
||||||
* 使用saveOrUpdateAllByNotNullProperties方法,只更新非空字段。
|
|
||||||
* 该方法具有事务性,遇到任何异常都会回滚。
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param entities 需要保存的实体对象集合
|
|
||||||
*/
|
|
||||||
@Transactional(rollbackFor = Throwable.class)
|
@Transactional(rollbackFor = Throwable.class)
|
||||||
@Override
|
@Override
|
||||||
public void save(Iterable<ENTITY> entities) {
|
public void save(Iterable<ENTITY> entities) {
|
||||||
repository.saveOrUpdateAllByNotNullProperties(entities);
|
repository.saveOrUpdateAllByNotNullProperties(entities);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Transactional(readOnly = true)
|
||||||
* 统计符合条件的实体数量
|
|
||||||
* <p>
|
|
||||||
* 根据listPredicate方法构建的条件统计实体数量。
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @return 返回符合条件的实体数量
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public Long count() {
|
public Long count() {
|
||||||
return repository.count(this::commonPredicates);
|
return repository.count(this::commonPredicates);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Transactional(readOnly = true)
|
||||||
* 获取所有符合条件的实体列表
|
|
||||||
* <p>
|
|
||||||
* 根据listPredicate方法构建的条件查询所有实体。
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @return 返回符合条件的实体列表
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public List<ENTITY> list() {
|
public List<ENTITY> list() {
|
||||||
return repository.findAll(this::commonPredicates);
|
return repository.findAll(this::commonPredicates);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Transactional(readOnly = true)
|
||||||
* 根据ID集合获取实体列表
|
|
||||||
* <p>
|
|
||||||
* 根据提供的ID集合查询对应的实体列表,并结合listPredicate方法构建的条件。
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param ids ID集合
|
|
||||||
* @return 返回ID集合对应的实体列表
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public List<ENTITY> list(Set<Long> ids) {
|
public List<ENTITY> list(Set<Long> ids) {
|
||||||
if (ObjectHelper.isEmpty(ids)) {
|
if (ObjectHelper.isEmpty(ids)) {
|
||||||
@@ -164,16 +121,7 @@ public abstract class SimpleServiceSupport<ENTITY extends SimpleEntity> implemen
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Transactional(readOnly = true)
|
||||||
* 根据查询条件分页获取实体列表
|
|
||||||
* <p>
|
|
||||||
* 支持复杂的查询条件和分页功能。
|
|
||||||
* 默认分页参数:第1页,每页10条记录,按创建时间降序排列。
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param listQuery 查询条件对象
|
|
||||||
* @return 返回分页查询结果
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public Page<ENTITY> list(Query listQuery) {
|
public Page<ENTITY> list(Query listQuery) {
|
||||||
var pageRequest = PageRequest.of(DEFAULT_PAGE_INDEX - 1, DEFAULT_PAGE_SIZE, Sort.by(SimpleEntity.Fields.createdTime).descending());
|
var pageRequest = PageRequest.of(DEFAULT_PAGE_INDEX - 1, DEFAULT_PAGE_SIZE, Sort.by(SimpleEntity.Fields.createdTime).descending());
|
||||||
@@ -232,46 +180,20 @@ public abstract class SimpleServiceSupport<ENTITY extends SimpleEntity> implemen
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据ID获取实体详情
|
|
||||||
* <p>
|
|
||||||
* 如果实体不存在则返回null。
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param id 实体ID
|
|
||||||
* @return 返回实体详情,不存在时返回null
|
|
||||||
*/
|
|
||||||
@Named("detail")
|
@Named("detail")
|
||||||
|
@Transactional(readOnly = true)
|
||||||
@Override
|
@Override
|
||||||
public ENTITY detail(Long id) {
|
public ENTITY detail(Long id) {
|
||||||
return detailOptional(id).orElse(null);
|
return detailOptional(id).orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据ID获取实体详情,不存在时抛出异常
|
|
||||||
* <p>
|
|
||||||
* 如果实体不存在则抛出IdNotFoundException异常。
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param id 实体ID
|
|
||||||
* @return 返回实体详情
|
|
||||||
* @throws IdNotFoundException 当实体不存在时抛出
|
|
||||||
*/
|
|
||||||
@Named("detailOrThrow")
|
@Named("detailOrThrow")
|
||||||
|
@Transactional(readOnly = true)
|
||||||
@Override
|
@Override
|
||||||
public ENTITY detailOrThrow(Long id) {
|
public ENTITY detailOrThrow(Long id) {
|
||||||
return detailOptional(id).orElseThrow(() -> new IdNotFoundException(id));
|
return detailOptional(id).orElseThrow(() -> new IdNotFoundException(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据ID删除实体
|
|
||||||
* <p>
|
|
||||||
* 具有事务性,遇到任何异常都会回滚。
|
|
||||||
* 如果ID为空则不执行任何操作。
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param id 实体主键ID
|
|
||||||
*/
|
|
||||||
@Transactional(rollbackFor = Throwable.class)
|
@Transactional(rollbackFor = Throwable.class)
|
||||||
@Override
|
@Override
|
||||||
public void remove(Long id) {
|
public void remove(Long id) {
|
||||||
@@ -280,16 +202,6 @@ public abstract class SimpleServiceSupport<ENTITY extends SimpleEntity> implemen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据ID集合批量删除实体
|
|
||||||
* <p>
|
|
||||||
* 使用deleteAllById方法根据ID集合批量删除实体。
|
|
||||||
* 该方法具有事务性,遇到任何异常都会回滚。
|
|
||||||
* 如果ID集合为空则不执行任何操作。
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param ids 实体主键ID集合
|
|
||||||
*/
|
|
||||||
@Transactional(rollbackFor = Throwable.class)
|
@Transactional(rollbackFor = Throwable.class)
|
||||||
@Override
|
@Override
|
||||||
public void remove(Set<Long> ids) {
|
public void remove(Set<Long> ids) {
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package com.lanyuanxiaoyao.service.template.database.helper;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.RepeatedTest;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
@DisplayName("SnowflakeHelper Tests")
|
||||||
|
class SnowflakeHelperTest {
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("ID Generation")
|
||||||
|
class IdGenerationTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("next returns positive number")
|
||||||
|
void next_returnsPositiveNumber() {
|
||||||
|
long id = SnowflakeHelper.next();
|
||||||
|
assertThat(id).isPositive();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("next returns monotonically increasing IDs")
|
||||||
|
void next_returnsMonotonicallyIncreasingIds() {
|
||||||
|
long id1 = SnowflakeHelper.next();
|
||||||
|
long id2 = SnowflakeHelper.next();
|
||||||
|
long id3 = SnowflakeHelper.next();
|
||||||
|
|
||||||
|
assertThat(id2).isGreaterThan(id1);
|
||||||
|
assertThat(id3).isGreaterThan(id2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Uniqueness")
|
||||||
|
class UniquenessTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("batch generation produces unique IDs")
|
||||||
|
void batchGeneration_producesUniqueIds() {
|
||||||
|
int count = 10000;
|
||||||
|
Set<Long> ids = Collections.synchronizedSet(new HashSet<>());
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
ids.add(SnowflakeHelper.next());
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(ids).hasSize(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("concurrent generation produces unique IDs")
|
||||||
|
void concurrentGeneration_producesUniqueIds() throws InterruptedException {
|
||||||
|
int threadCount = 10;
|
||||||
|
int idsPerThread = 1000;
|
||||||
|
Set<Long> ids = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||||
|
CountDownLatch latch = new CountDownLatch(threadCount);
|
||||||
|
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
|
||||||
|
|
||||||
|
for (int i = 0; i < threadCount; i++) {
|
||||||
|
executor.submit(() -> {
|
||||||
|
try {
|
||||||
|
for (int j = 0; j < idsPerThread; j++) {
|
||||||
|
ids.add(SnowflakeHelper.next());
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
latch.countDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
latch.await();
|
||||||
|
executor.shutdown();
|
||||||
|
|
||||||
|
assertThat(ids).hasSize(threadCount * idsPerThread);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Performance")
|
||||||
|
class PerformanceTests {
|
||||||
|
|
||||||
|
@RepeatedTest(5)
|
||||||
|
@DisplayName("generates 1000 IDs quickly")
|
||||||
|
void generatesIdsQuickly() {
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
for (int i = 0; i < 1000; i++) {
|
||||||
|
SnowflakeHelper.next();
|
||||||
|
}
|
||||||
|
long elapsed = System.currentTimeMillis() - start;
|
||||||
|
assertThat(elapsed).isLessThan(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.lanyuanxiaoyao.service.template.database.integration;
|
||||||
|
|
||||||
|
import com.blinkfox.fenix.EnableFenix;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
@EnableFenix
|
||||||
|
@EnableJpaAuditing
|
||||||
|
public class IntegrationTestConfiguration {
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
package com.lanyuanxiaoyao.service.template.database.integration;
|
||||||
|
|
||||||
|
import com.lanyuanxiaoyao.service.template.database.entity.Query;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class QueryBuilder {
|
||||||
|
private List<String> nullEqual;
|
||||||
|
private List<String> notNullEqual;
|
||||||
|
private List<String> empty;
|
||||||
|
private List<String> notEmpty;
|
||||||
|
private Map<String, ? extends Serializable> equal;
|
||||||
|
private Map<String, ? extends Serializable> notEqual;
|
||||||
|
private Map<String, String> like;
|
||||||
|
private Map<String, String> notLike;
|
||||||
|
private Map<String, String> contain;
|
||||||
|
private Map<String, String> notContain;
|
||||||
|
private Map<String, String> startWith;
|
||||||
|
private Map<String, String> notStartWith;
|
||||||
|
private Map<String, String> endWith;
|
||||||
|
private Map<String, String> notEndWith;
|
||||||
|
private Map<String, ? extends Serializable> great;
|
||||||
|
private Map<String, ? extends Serializable> less;
|
||||||
|
private Map<String, ? extends Serializable> greatEqual;
|
||||||
|
private Map<String, ? extends Serializable> lessEqual;
|
||||||
|
private Map<String, List<? extends Serializable>> inside;
|
||||||
|
private Map<String, List<? extends Serializable>> notInside;
|
||||||
|
private Map<String, Query.Queryable.Between> between;
|
||||||
|
private Map<String, Query.Queryable.Between> notBetween;
|
||||||
|
|
||||||
|
public QueryBuilder nullEqual(List<String> nullEqual) {
|
||||||
|
this.nullEqual = nullEqual;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public QueryBuilder notNullEqual(List<String> notNullEqual) {
|
||||||
|
this.notNullEqual = notNullEqual;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public QueryBuilder empty(List<String> empty) {
|
||||||
|
this.empty = empty;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public QueryBuilder notEmpty(List<String> notEmpty) {
|
||||||
|
this.notEmpty = notEmpty;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public QueryBuilder equal(Map<String, ? extends Serializable> equal) {
|
||||||
|
this.equal = equal;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public QueryBuilder notEqual(Map<String, ? extends Serializable> notEqual) {
|
||||||
|
this.notEqual = notEqual;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public QueryBuilder like(Map<String, String> like) {
|
||||||
|
this.like = like;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public QueryBuilder notLike(Map<String, String> notLike) {
|
||||||
|
this.notLike = notLike;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public QueryBuilder contain(Map<String, String> contain) {
|
||||||
|
this.contain = contain;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public QueryBuilder notContain(Map<String, String> notContain) {
|
||||||
|
this.notContain = notContain;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public QueryBuilder startWith(Map<String, String> startWith) {
|
||||||
|
this.startWith = startWith;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public QueryBuilder notStartWith(Map<String, String> notStartWith) {
|
||||||
|
this.notStartWith = notStartWith;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public QueryBuilder endWith(Map<String, String> endWith) {
|
||||||
|
this.endWith = endWith;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public QueryBuilder notEndWith(Map<String, String> notEndWith) {
|
||||||
|
this.notEndWith = notEndWith;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public QueryBuilder great(Map<String, ? extends Serializable> great) {
|
||||||
|
this.great = great;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public QueryBuilder less(Map<String, ? extends Serializable> less) {
|
||||||
|
this.less = less;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public QueryBuilder greatEqual(Map<String, ? extends Serializable> greatEqual) {
|
||||||
|
this.greatEqual = greatEqual;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public QueryBuilder lessEqual(Map<String, ? extends Serializable> lessEqual) {
|
||||||
|
this.lessEqual = lessEqual;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public QueryBuilder inside(Map<String, List<? extends Serializable>> inside) {
|
||||||
|
this.inside = inside;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public QueryBuilder notInside(Map<String, List<? extends Serializable>> notInside) {
|
||||||
|
this.notInside = notInside;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public QueryBuilder between(Map<String, Query.Queryable.Between> between) {
|
||||||
|
this.between = between;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public QueryBuilder notBetween(Map<String, Query.Queryable.Between> notBetween) {
|
||||||
|
this.notBetween = notBetween;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Query build() {
|
||||||
|
return new Query(new Query.Queryable(
|
||||||
|
nullEqual, notNullEqual, empty, notEmpty, equal, notEqual,
|
||||||
|
like, notLike, contain, notContain, startWith, notStartWith,
|
||||||
|
endWith, notEndWith, great, less, greatEqual, lessEqual,
|
||||||
|
inside, notInside, between, notBetween
|
||||||
|
), null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
package com.lanyuanxiaoyao.service.template.database.integration;
|
||||||
|
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
import com.lanyuanxiaoyao.service.template.database.entity.Query;
|
||||||
|
import com.lanyuanxiaoyao.service.template.database.integration.controller.TestController;
|
||||||
|
import com.lanyuanxiaoyao.service.template.database.integration.entity.TestEntity;
|
||||||
|
import com.lanyuanxiaoyao.service.template.database.integration.entity.TestStatus;
|
||||||
|
import com.lanyuanxiaoyao.service.template.database.integration.repository.TestRepository;
|
||||||
|
import com.lanyuanxiaoyao.service.template.database.integration.service.TestService;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.data.jpa.test.autoconfigure.AutoConfigureDataJpa;
|
||||||
|
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
|
||||||
|
import org.springframework.context.annotation.Import;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
@WebMvcTest(TestController.class)
|
||||||
|
@AutoConfigureDataJpa
|
||||||
|
@ActiveProfiles("test")
|
||||||
|
@Import({IntegrationTestConfiguration.class, TestService.class})
|
||||||
|
@DisplayName("SimpleControllerSupport Integration Tests")
|
||||||
|
class SimpleControllerSupportIntegrationTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
MockMvc mockMvc;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
TestRepository repository;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void cleanUp() {
|
||||||
|
repository.deleteAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("POST /save")
|
||||||
|
class SaveTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("save with valid data returns id")
|
||||||
|
void save_withValidData_returnsId() throws Exception {
|
||||||
|
var request = new TestController.SaveItem("张三", 25, TestStatus.ACTIVE, 5000.0, "java");
|
||||||
|
|
||||||
|
mockMvc.perform(post("/test/save")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(objectMapper.writeValueAsString(request)))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.status").value(0))
|
||||||
|
.andExpect(jsonPath("$.message").value("OK"))
|
||||||
|
.andExpect(jsonPath("$.data").isNumber());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("saveItemMapper converts correctly")
|
||||||
|
void saveItemMapper_convertsCorrectly() throws Exception {
|
||||||
|
var request = new TestController.SaveItem("张三", 25, TestStatus.ACTIVE, 5000.0, "java,spring");
|
||||||
|
|
||||||
|
String response = mockMvc.perform(post("/test/save")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(objectMapper.writeValueAsString(request)))
|
||||||
|
.andReturn().getResponse().getContentAsString();
|
||||||
|
|
||||||
|
Long id = objectMapper.readTree(response).get("data").asLong();
|
||||||
|
TestEntity saved = repository.findById(id).orElseThrow();
|
||||||
|
|
||||||
|
assertThat(saved.getName()).isEqualTo("张三");
|
||||||
|
assertThat(saved.getAge()).isEqualTo(25);
|
||||||
|
assertThat(saved.getStatus()).isEqualTo(TestStatus.ACTIVE);
|
||||||
|
assertThat(saved.getSalary()).isEqualTo(5000.0);
|
||||||
|
assertThat(saved.getTags()).isEqualTo("java,spring");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("GET /list")
|
||||||
|
class ListTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("list returns all entities")
|
||||||
|
void list_returnsAllEntities() throws Exception {
|
||||||
|
repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"));
|
||||||
|
repository.save(new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python"));
|
||||||
|
|
||||||
|
mockMvc.perform(get("/test/list"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.status").value(0))
|
||||||
|
.andExpect(jsonPath("$.message").value("OK"))
|
||||||
|
.andExpect(jsonPath("$.data.items").isArray())
|
||||||
|
.andExpect(jsonPath("$.data.items.length()").value(2))
|
||||||
|
.andExpect(jsonPath("$.data.total").value(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("listItemMapper converts correctly")
|
||||||
|
void listItemMapper_convertsCorrectly() throws Exception {
|
||||||
|
var entity = repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"));
|
||||||
|
|
||||||
|
mockMvc.perform(get("/test/list"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.data.items[0].id").value(entity.getId()))
|
||||||
|
.andExpect(jsonPath("$.data.items[0].name").value("张三"))
|
||||||
|
.andExpect(jsonPath("$.data.items[0].age").value(25))
|
||||||
|
.andExpect(jsonPath("$.data.items[0].status").value("ACTIVE"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("POST /list")
|
||||||
|
class ListWithQueryTests {
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"));
|
||||||
|
repository.save(new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("list with query filters correctly")
|
||||||
|
void listWithQuery_filtersCorrectly() throws Exception {
|
||||||
|
var query = new QueryBuilder().equal(java.util.Map.of("name", "张三")).build();
|
||||||
|
|
||||||
|
mockMvc.perform(post("/test/list")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(objectMapper.writeValueAsString(query)))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.data.items.length()").value(1))
|
||||||
|
.andExpect(jsonPath("$.data.items[0].name").value("张三"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("list with pagination returns correct page")
|
||||||
|
void listWithPagination_returnsCorrectPage() throws Exception {
|
||||||
|
var query = new Query(null, null, new Query.Pageable(1, 1));
|
||||||
|
|
||||||
|
mockMvc.perform(post("/test/list")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(objectMapper.writeValueAsString(query)))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.data.items.length()").value(1))
|
||||||
|
.andExpect(jsonPath("$.data.total").value(2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("GET /detail/{id}")
|
||||||
|
class DetailTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("detail with existing id returns entity")
|
||||||
|
void detail_withExistingId_returnsEntity() throws Exception {
|
||||||
|
var entity = repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java,spring"));
|
||||||
|
|
||||||
|
mockMvc.perform(get("/test/detail/{id}", entity.getId()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.status").value(0))
|
||||||
|
.andExpect(jsonPath("$.data.id").value(entity.getId()))
|
||||||
|
.andExpect(jsonPath("$.data.name").value("张三"))
|
||||||
|
.andExpect(jsonPath("$.data.age").value(25))
|
||||||
|
.andExpect(jsonPath("$.data.status").value("ACTIVE"))
|
||||||
|
.andExpect(jsonPath("$.data.salary").value(5000.0))
|
||||||
|
.andExpect(jsonPath("$.data.tags").value("java,spring"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("detailItemMapper converts correctly")
|
||||||
|
void detailItemMapper_convertsCorrectly() throws Exception {
|
||||||
|
var entity = repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java,spring"));
|
||||||
|
|
||||||
|
mockMvc.perform(get("/test/detail/{id}", entity.getId()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.data.id").value(entity.getId()))
|
||||||
|
.andExpect(jsonPath("$.data.name").value("张三"))
|
||||||
|
.andExpect(jsonPath("$.data.tags").value("java,spring"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("GET /remove/{id}")
|
||||||
|
class RemoveTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("remove deletes entity")
|
||||||
|
void remove_deletesEntity() throws Exception {
|
||||||
|
var entity = repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"));
|
||||||
|
|
||||||
|
mockMvc.perform(get("/test/remove/{id}", entity.getId()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.status").value(0))
|
||||||
|
.andExpect(jsonPath("$.data").isEmpty());
|
||||||
|
|
||||||
|
assertThat(repository.findById(entity.getId())).isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,662 @@
|
|||||||
|
package com.lanyuanxiaoyao.service.template.database.integration;
|
||||||
|
|
||||||
|
import com.lanyuanxiaoyao.service.template.database.entity.Query;
|
||||||
|
import com.lanyuanxiaoyao.service.template.database.exception.IdNotFoundException;
|
||||||
|
import com.lanyuanxiaoyao.service.template.database.exception.NotCollectionException;
|
||||||
|
import com.lanyuanxiaoyao.service.template.database.exception.NotComparableException;
|
||||||
|
import com.lanyuanxiaoyao.service.template.database.exception.NotStringException;
|
||||||
|
import com.lanyuanxiaoyao.service.template.database.integration.entity.TestEntity;
|
||||||
|
import com.lanyuanxiaoyao.service.template.database.integration.entity.TestStatus;
|
||||||
|
import com.lanyuanxiaoyao.service.template.database.integration.repository.TestRepository;
|
||||||
|
import com.lanyuanxiaoyao.service.template.database.integration.service.TestService;
|
||||||
|
import jakarta.persistence.EntityManager;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
|
||||||
|
import org.springframework.context.annotation.Import;
|
||||||
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
|
@DataJpaTest
|
||||||
|
@ActiveProfiles("test")
|
||||||
|
@Import({IntegrationTestConfiguration.class, TestService.class})
|
||||||
|
@DisplayName("SimpleServiceSupport Integration Tests")
|
||||||
|
class SimpleServiceSupportIntegrationTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
TestService service;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
TestRepository repository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
EntityManager entityManager;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void cleanUp() {
|
||||||
|
repository.deleteAllInBatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("CRUD Operations")
|
||||||
|
class CrudTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("save with new entity returns generated id")
|
||||||
|
void save_withNewEntity_returnsId() {
|
||||||
|
var entity = new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java,spring");
|
||||||
|
|
||||||
|
Long id = service.save(entity);
|
||||||
|
|
||||||
|
assertThat(id).isNotNull().isPositive();
|
||||||
|
var saved = repository.findById(id).orElseThrow();
|
||||||
|
assertThat(saved.getName()).isEqualTo("张三");
|
||||||
|
assertThat(saved.getAge()).isEqualTo(25);
|
||||||
|
assertThat(saved.getStatus()).isEqualTo(TestStatus.ACTIVE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("save with existing entity updates non-null fields")
|
||||||
|
void save_withExistingEntity_updatesFields() {
|
||||||
|
var entity = new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java");
|
||||||
|
Long id = service.save(entity);
|
||||||
|
|
||||||
|
var update = new TestEntity(null, 26, null, 6000.0, null);
|
||||||
|
update.setId(id);
|
||||||
|
service.save(update);
|
||||||
|
|
||||||
|
var updated = repository.findById(id).orElseThrow();
|
||||||
|
assertThat(updated.getName()).isEqualTo("张三");
|
||||||
|
assertThat(updated.getAge()).isEqualTo(26);
|
||||||
|
assertThat(updated.getStatus()).isEqualTo(TestStatus.ACTIVE);
|
||||||
|
assertThat(updated.getSalary()).isEqualTo(6000.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("save multiple entities saves all")
|
||||||
|
void save_multipleEntities_savesAll() {
|
||||||
|
var entities = List.of(
|
||||||
|
new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"),
|
||||||
|
new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python")
|
||||||
|
);
|
||||||
|
|
||||||
|
service.save(entities);
|
||||||
|
|
||||||
|
assertThat(repository.count()).isEqualTo(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("count returns correct total")
|
||||||
|
void count_returnsCorrectTotal() {
|
||||||
|
repository.saveAll(List.of(
|
||||||
|
new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"),
|
||||||
|
new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python")
|
||||||
|
));
|
||||||
|
|
||||||
|
Long count = service.count();
|
||||||
|
|
||||||
|
assertThat(count).isEqualTo(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("list returns all entities")
|
||||||
|
void list_returnsAllEntities() {
|
||||||
|
repository.saveAll(List.of(
|
||||||
|
new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"),
|
||||||
|
new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python")
|
||||||
|
));
|
||||||
|
|
||||||
|
List<TestEntity> result = service.list();
|
||||||
|
|
||||||
|
assertThat(result).hasSize(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("list with ids returns matching entities")
|
||||||
|
void list_withIds_returnsMatching() {
|
||||||
|
var e1 = repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"));
|
||||||
|
var e2 = repository.save(new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python"));
|
||||||
|
repository.save(new TestEntity("王五", 35, TestStatus.DELETED, 7000.0, "go"));
|
||||||
|
|
||||||
|
List<TestEntity> result = service.list(Set.of(e1.getId(), e2.getId()));
|
||||||
|
|
||||||
|
assertThat(result).hasSize(2)
|
||||||
|
.extracting(TestEntity::getName)
|
||||||
|
.containsExactlyInAnyOrder("张三", "李四");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("detail with existing id returns entity")
|
||||||
|
void detail_withExistingId_returnsEntity() {
|
||||||
|
var saved = repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"));
|
||||||
|
|
||||||
|
TestEntity result = service.detail(saved.getId());
|
||||||
|
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.getName()).isEqualTo("张三");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("detail with non-existing id returns null")
|
||||||
|
void detail_withNonExistingId_returnsNull() {
|
||||||
|
TestEntity result = service.detail(999L);
|
||||||
|
|
||||||
|
assertThat(result).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("detailOrThrow with non-existing id throws exception")
|
||||||
|
void detailOrThrow_withNonExistingId_throwsException() {
|
||||||
|
assertThatThrownBy(() -> service.detailOrThrow(999L))
|
||||||
|
.isInstanceOf(IdNotFoundException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("remove deletes entity")
|
||||||
|
void remove_deletesEntity() {
|
||||||
|
var saved = repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"));
|
||||||
|
repository.flush();
|
||||||
|
entityManager.clear();
|
||||||
|
|
||||||
|
service.remove(saved.getId());
|
||||||
|
repository.flush();
|
||||||
|
|
||||||
|
var nativeQuery = entityManager.createNativeQuery("SELECT COUNT(*) FROM test_entity WHERE id = ?");
|
||||||
|
nativeQuery.setParameter(1, saved.getId());
|
||||||
|
var count = ((Number) nativeQuery.getSingleResult()).longValue();
|
||||||
|
assertThat(count).isZero();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("remove multiple entities deletes all")
|
||||||
|
void remove_multipleEntities_deletesAll() {
|
||||||
|
var e1 = repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"));
|
||||||
|
var e2 = repository.save(new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python"));
|
||||||
|
|
||||||
|
service.remove(Set.of(e1.getId(), e2.getId()));
|
||||||
|
repository.flush();
|
||||||
|
|
||||||
|
assertThat(repository.count()).isZero();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Equality Conditions")
|
||||||
|
class EqualityConditionTests {
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
repository.saveAll(List.of(
|
||||||
|
new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"),
|
||||||
|
new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, null),
|
||||||
|
new TestEntity("王五", 35, TestStatus.DELETED, 7000.0, "go")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("equal condition filters correctly")
|
||||||
|
void equalCondition_filtersCorrectly() {
|
||||||
|
var query = new QueryBuilder().equal(Map.of("name", "张三")).build();
|
||||||
|
|
||||||
|
var result = service.list(query);
|
||||||
|
|
||||||
|
assertThat(result.items()).hasSize(1);
|
||||||
|
assertThat(result.items().get(0).getName()).isEqualTo("张三");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("notEqual condition filters correctly")
|
||||||
|
void notEqualCondition_filtersCorrectly() {
|
||||||
|
var query = new QueryBuilder().notEqual(Map.of("name", "张三")).build();
|
||||||
|
|
||||||
|
var result = service.list(query);
|
||||||
|
|
||||||
|
assertThat(result.items()).hasSize(2);
|
||||||
|
assertThat(result.items()).noneMatch(e -> "张三".equals(e.getName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("nullEqual condition filters null values")
|
||||||
|
void nullEqualCondition_filtersNullValues() {
|
||||||
|
var query = new QueryBuilder().nullEqual(List.of("tags")).build();
|
||||||
|
|
||||||
|
var result = service.list(query);
|
||||||
|
|
||||||
|
assertThat(result.items()).hasSize(1);
|
||||||
|
assertThat(result.items().get(0).getTags()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("notNullEqual condition filters non-null values")
|
||||||
|
void notNullEqualCondition_filtersNonNullValues() {
|
||||||
|
var query = new QueryBuilder().notNullEqual(List.of("tags")).build();
|
||||||
|
|
||||||
|
var result = service.list(query);
|
||||||
|
|
||||||
|
assertThat(result.items()).hasSize(2);
|
||||||
|
assertThat(result.items()).allMatch(e -> e.getTags() != null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("String Conditions")
|
||||||
|
class StringConditionTests {
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
repository.saveAll(List.of(
|
||||||
|
new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java,spring"),
|
||||||
|
new TestEntity("李四三", 30, TestStatus.INACTIVE, 6000.0, "python"),
|
||||||
|
new TestEntity("王五", 35, TestStatus.DELETED, 7000.0, "go")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("like condition filters correctly")
|
||||||
|
void likeCondition_filtersCorrectly() {
|
||||||
|
var query = new QueryBuilder().like(Map.of("name", "%三%")).build();
|
||||||
|
|
||||||
|
var result = service.list(query);
|
||||||
|
|
||||||
|
assertThat(result.items()).hasSize(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("notLike condition filters correctly")
|
||||||
|
void notLikeCondition_filtersCorrectly() {
|
||||||
|
var query = new QueryBuilder().notLike(Map.of("name", "%三%")).build();
|
||||||
|
|
||||||
|
var result = service.list(query);
|
||||||
|
|
||||||
|
assertThat(result.items()).hasSize(1);
|
||||||
|
assertThat(result.items().get(0).getName()).isEqualTo("王五");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("contain condition filters correctly")
|
||||||
|
void containCondition_filtersCorrectly() {
|
||||||
|
var query = new QueryBuilder().contain(Map.of("name", "三")).build();
|
||||||
|
|
||||||
|
var result = service.list(query);
|
||||||
|
|
||||||
|
assertThat(result.items()).hasSize(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("notContain condition filters correctly")
|
||||||
|
void notContainCondition_filtersCorrectly() {
|
||||||
|
var query = new QueryBuilder().notContain(Map.of("name", "三")).build();
|
||||||
|
|
||||||
|
var result = service.list(query);
|
||||||
|
|
||||||
|
assertThat(result.items()).hasSize(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("startWith condition filters correctly")
|
||||||
|
void startWithCondition_filtersCorrectly() {
|
||||||
|
var query = new QueryBuilder().startWith(Map.of("name", "张")).build();
|
||||||
|
|
||||||
|
var result = service.list(query);
|
||||||
|
|
||||||
|
assertThat(result.items()).hasSize(1);
|
||||||
|
assertThat(result.items().get(0).getName()).isEqualTo("张三");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("notStartWith condition filters correctly")
|
||||||
|
void notStartWithCondition_filtersCorrectly() {
|
||||||
|
var query = new QueryBuilder().notStartWith(Map.of("name", "张")).build();
|
||||||
|
|
||||||
|
var result = service.list(query);
|
||||||
|
|
||||||
|
assertThat(result.items()).hasSize(2);
|
||||||
|
assertThat(result.items()).noneMatch(e -> e.getName().startsWith("张"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("endWith condition filters correctly")
|
||||||
|
void endWithCondition_filtersCorrectly() {
|
||||||
|
var query = new QueryBuilder().endWith(Map.of("name", "三")).build();
|
||||||
|
|
||||||
|
var result = service.list(query);
|
||||||
|
|
||||||
|
assertThat(result.items()).hasSize(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("notEndWith condition filters correctly")
|
||||||
|
void notEndWithCondition_filtersCorrectly() {
|
||||||
|
var query = new QueryBuilder().notEndWith(Map.of("name", "三")).build();
|
||||||
|
|
||||||
|
var result = service.list(query);
|
||||||
|
|
||||||
|
assertThat(result.items()).hasSize(1);
|
||||||
|
assertThat(result.items().get(0).getName()).isEqualTo("王五");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Comparison Conditions")
|
||||||
|
class ComparisonConditionTests {
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
repository.saveAll(List.of(
|
||||||
|
new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"),
|
||||||
|
new TestEntity("李四", 35, TestStatus.INACTIVE, 6000.0, "python"),
|
||||||
|
new TestEntity("王五", 45, TestStatus.DELETED, 7000.0, "go")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("great condition filters correctly")
|
||||||
|
void greatCondition_filtersCorrectly() {
|
||||||
|
var query = new QueryBuilder().great(Map.of("age", 30)).build();
|
||||||
|
|
||||||
|
var result = service.list(query);
|
||||||
|
|
||||||
|
assertThat(result.items()).hasSize(2);
|
||||||
|
assertThat(result.items()).allMatch(e -> e.getAge() > 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("less condition filters correctly")
|
||||||
|
void lessCondition_filtersCorrectly() {
|
||||||
|
var query = new QueryBuilder().less(Map.of("age", 40)).build();
|
||||||
|
|
||||||
|
var result = service.list(query);
|
||||||
|
|
||||||
|
assertThat(result.items()).hasSize(2);
|
||||||
|
assertThat(result.items()).allMatch(e -> e.getAge() < 40);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("greatEqual condition filters correctly")
|
||||||
|
void greatEqualCondition_filtersCorrectly() {
|
||||||
|
var query = new QueryBuilder().greatEqual(Map.of("age", 35)).build();
|
||||||
|
|
||||||
|
var result = service.list(query);
|
||||||
|
|
||||||
|
assertThat(result.items()).hasSize(2);
|
||||||
|
assertThat(result.items()).allMatch(e -> e.getAge() >= 35);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("lessEqual condition filters correctly")
|
||||||
|
void lessEqualCondition_filtersCorrectly() {
|
||||||
|
var query = new QueryBuilder().lessEqual(Map.of("age", 35)).build();
|
||||||
|
|
||||||
|
var result = service.list(query);
|
||||||
|
|
||||||
|
assertThat(result.items()).hasSize(2);
|
||||||
|
assertThat(result.items()).allMatch(e -> e.getAge() <= 35);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("between condition filters correctly")
|
||||||
|
void betweenCondition_filtersCorrectly() {
|
||||||
|
var query = new QueryBuilder().between(Map.of("age", new Query.Queryable.Between(25, 35))).build();
|
||||||
|
|
||||||
|
var result = service.list(query);
|
||||||
|
|
||||||
|
assertThat(result.items()).hasSize(2);
|
||||||
|
assertThat(result.items()).allMatch(e -> e.getAge() >= 25 && e.getAge() <= 35);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("notBetween condition filters correctly")
|
||||||
|
void notBetweenCondition_filtersCorrectly() {
|
||||||
|
var query = new QueryBuilder().notBetween(Map.of("age", new Query.Queryable.Between(26, 44))).build();
|
||||||
|
|
||||||
|
var result = service.list(query);
|
||||||
|
|
||||||
|
assertThat(result.items()).hasSize(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Collection Conditions")
|
||||||
|
class CollectionConditionTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("inside condition filters correctly")
|
||||||
|
void insideCondition_filtersCorrectly() {
|
||||||
|
repository.saveAll(List.of(
|
||||||
|
new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"),
|
||||||
|
new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python"),
|
||||||
|
new TestEntity("王五", 35, TestStatus.DELETED, 7000.0, "go")
|
||||||
|
));
|
||||||
|
|
||||||
|
var query = new QueryBuilder().inside(Map.of("age", List.of(25, 35))).build();
|
||||||
|
|
||||||
|
var result = service.list(query);
|
||||||
|
|
||||||
|
assertThat(result.items()).hasSize(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("notInside condition filters correctly")
|
||||||
|
void notInsideCondition_filtersCorrectly() {
|
||||||
|
repository.saveAll(List.of(
|
||||||
|
new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"),
|
||||||
|
new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python"),
|
||||||
|
new TestEntity("王五", 35, TestStatus.DELETED, 7000.0, "go")
|
||||||
|
));
|
||||||
|
|
||||||
|
var query = new QueryBuilder().notInside(Map.of("age", List.of(25, 35))).build();
|
||||||
|
|
||||||
|
var result = service.list(query);
|
||||||
|
|
||||||
|
assertThat(result.items()).hasSize(1);
|
||||||
|
assertThat(result.items().get(0).getAge()).isEqualTo(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("empty condition filters correctly")
|
||||||
|
void emptyCondition_filtersCorrectly() {
|
||||||
|
repository.saveAll(List.of(
|
||||||
|
new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"),
|
||||||
|
new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python"),
|
||||||
|
new TestEntity("王五", 35, TestStatus.DELETED, 7000.0, "go")
|
||||||
|
));
|
||||||
|
|
||||||
|
var query = new QueryBuilder().empty(List.of("tags")).build();
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> service.list(query))
|
||||||
|
.isInstanceOf(NotCollectionException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("notEmpty condition filters correctly")
|
||||||
|
void notEmptyCondition_filtersCorrectly() {
|
||||||
|
repository.saveAll(List.of(
|
||||||
|
new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"),
|
||||||
|
new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python"),
|
||||||
|
new TestEntity("王五", 35, TestStatus.DELETED, 7000.0, "go")
|
||||||
|
));
|
||||||
|
|
||||||
|
var query = new QueryBuilder().notEmpty(List.of("tags")).build();
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> service.list(query))
|
||||||
|
.isInstanceOf(NotCollectionException.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Enum Condition")
|
||||||
|
class EnumConditionTests {
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
repository.saveAll(List.of(
|
||||||
|
new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"),
|
||||||
|
new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python"),
|
||||||
|
new TestEntity("王五", 35, TestStatus.DELETED, 7000.0, "go")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("enum condition filters correctly")
|
||||||
|
void enumCondition_filtersCorrectly() {
|
||||||
|
var query = new QueryBuilder().equal(Map.of("status", "ACTIVE")).build();
|
||||||
|
|
||||||
|
var result = service.list(query);
|
||||||
|
|
||||||
|
assertThat(result.items()).hasSize(1);
|
||||||
|
assertThat(result.items().get(0).getStatus()).isEqualTo(TestStatus.ACTIVE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Combined Conditions")
|
||||||
|
class CombinedConditionTests {
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
repository.saveAll(List.of(
|
||||||
|
new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"),
|
||||||
|
new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python"),
|
||||||
|
new TestEntity("王五", 35, TestStatus.ACTIVE, 7000.0, "go"),
|
||||||
|
new TestEntity("赵六", 40, TestStatus.ACTIVE, 8000.0, "rust")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("combined conditions filter correctly")
|
||||||
|
void combinedConditions_filterCorrectly() {
|
||||||
|
var query = new QueryBuilder()
|
||||||
|
.equal(Map.of("status", "ACTIVE"))
|
||||||
|
.great(Map.of("age", 30))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
var result = service.list(query);
|
||||||
|
|
||||||
|
assertThat(result.items()).hasSize(2);
|
||||||
|
assertThat(result.items()).allMatch(e -> e.getStatus() == TestStatus.ACTIVE && e.getAge() > 30);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Pagination and Sorting")
|
||||||
|
class PaginationSortingTests {
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
repository.deleteAllInBatch();
|
||||||
|
entityManager.flush();
|
||||||
|
entityManager.clear();
|
||||||
|
for (int i = 1; i <= 25; i++) {
|
||||||
|
repository.save(new TestEntity("用户" + i, 20 + i, TestStatus.ACTIVE, 5000.0 + i * 100, "tag" + i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("default pagination returns first page with 10 items")
|
||||||
|
void defaultPagination_returnsFirstPage() {
|
||||||
|
var query = new Query(null, null, null);
|
||||||
|
|
||||||
|
var result = service.list(query);
|
||||||
|
|
||||||
|
assertThat(result.items()).hasSize(10);
|
||||||
|
assertThat(result.total()).isEqualTo(25);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("custom pagination returns correct page")
|
||||||
|
void customPagination_returnsCorrectPage() {
|
||||||
|
var query = new Query(null, null, new Query.Pageable(1, 5));
|
||||||
|
|
||||||
|
var result = service.list(query);
|
||||||
|
|
||||||
|
assertThat(result.items()).hasSize(5);
|
||||||
|
assertThat(result.total()).isEqualTo(25);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("custom sorting sorts correctly")
|
||||||
|
void customSorting_sortsCorrectly() {
|
||||||
|
var query = new Query(null, List.of(new Query.Sortable("age", Query.Sortable.Direction.ASC)), new Query.Pageable(1, 10));
|
||||||
|
|
||||||
|
var result = service.list(query);
|
||||||
|
|
||||||
|
assertThat(result.items()).hasSize(10);
|
||||||
|
assertThat(result.items().get(0).getAge()).isEqualTo(21);
|
||||||
|
assertThat(result.items()).isSortedAccordingTo((a, b) -> a.getAge().compareTo(b.getAge()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("multiple sort fields sort correctly")
|
||||||
|
void multipleSortFields_sortCorrectly() {
|
||||||
|
repository.deleteAllInBatch();
|
||||||
|
entityManager.flush();
|
||||||
|
entityManager.clear();
|
||||||
|
|
||||||
|
repository.saveAll(List.of(
|
||||||
|
new TestEntity("A", 20, TestStatus.ACTIVE, 5000.0, null),
|
||||||
|
new TestEntity("B", 30, TestStatus.ACTIVE, 3000.0, null),
|
||||||
|
new TestEntity("C", 30, TestStatus.ACTIVE, 4000.0, null)
|
||||||
|
));
|
||||||
|
|
||||||
|
var query = new Query(null, List.of(
|
||||||
|
new Query.Sortable("age", Query.Sortable.Direction.ASC),
|
||||||
|
new Query.Sortable("salary", Query.Sortable.Direction.DESC)
|
||||||
|
), new Query.Pageable(1, 10));
|
||||||
|
|
||||||
|
var result = service.list(query);
|
||||||
|
|
||||||
|
assertThat(result.items()).hasSize(3);
|
||||||
|
assertThat(result.items().get(0).getName()).isEqualTo("A");
|
||||||
|
assertThat(result.items().get(1).getName()).isEqualTo("C");
|
||||||
|
assertThat(result.items().get(2).getName()).isEqualTo("B");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Exception Handling")
|
||||||
|
class ExceptionTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("like on non-string field throws NotStringException")
|
||||||
|
void likeOnNonStringField_throwsNotStringException() {
|
||||||
|
repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"));
|
||||||
|
|
||||||
|
var query = new QueryBuilder().like(Map.of("age", "%5%")).build();
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> service.list(query))
|
||||||
|
.isInstanceOf(NotStringException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("great on enum field with enum value throws exception")
|
||||||
|
void greatOnEnumField_withEnumValue_throwsException() {
|
||||||
|
repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"));
|
||||||
|
|
||||||
|
var query = new QueryBuilder().great(Map.of("status", TestStatus.ACTIVE)).build();
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> service.list(query))
|
||||||
|
.isInstanceOf(org.springframework.dao.InvalidDataAccessApiUsageException.class)
|
||||||
|
.hasMessageContaining("枚举类型字段需要 String 类型的值");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("empty on non-collection field throws NotCollectionException")
|
||||||
|
void emptyOnNonCollectionField_throwsNotCollectionException() {
|
||||||
|
repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"));
|
||||||
|
|
||||||
|
var query = new QueryBuilder().empty(List.of("name")).build();
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> service.list(query))
|
||||||
|
.isInstanceOf(NotCollectionException.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
package com.lanyuanxiaoyao.service.template.database.integration;
|
||||||
|
|
||||||
|
import com.lanyuanxiaoyao.service.template.database.integration.entity.TestSoftDeleteEntity;
|
||||||
|
import com.lanyuanxiaoyao.service.template.database.integration.repository.TestSoftDeleteRepository;
|
||||||
|
import com.lanyuanxiaoyao.service.template.database.integration.service.TestSoftDeleteService;
|
||||||
|
import jakarta.persistence.EntityManager;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
|
||||||
|
import org.springframework.context.annotation.Import;
|
||||||
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
@DataJpaTest
|
||||||
|
@ActiveProfiles("test")
|
||||||
|
@Import({IntegrationTestConfiguration.class, TestSoftDeleteService.class})
|
||||||
|
@DisplayName("@SoftDelete Integration Tests")
|
||||||
|
class SoftDeleteIntegrationTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
TestSoftDeleteService service;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
TestSoftDeleteRepository repository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
EntityManager entityManager;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void cleanUp() {
|
||||||
|
repository.deleteAllInBatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Save and Delete Behavior")
|
||||||
|
class SaveDeleteTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("save entity sets deleted to false")
|
||||||
|
void saveEntity_setsDeletedFalse() {
|
||||||
|
var entity = new TestSoftDeleteEntity("张三", 25);
|
||||||
|
|
||||||
|
Long id = service.save(entity);
|
||||||
|
|
||||||
|
var saved = repository.findById(id).orElseThrow();
|
||||||
|
assertThat(saved.getName()).isEqualTo("张三");
|
||||||
|
|
||||||
|
var nativeQuery = entityManager.createNativeQuery("SELECT deleted FROM test_soft_delete_entity WHERE id = ?");
|
||||||
|
nativeQuery.setParameter(1, id);
|
||||||
|
var deleted = (Boolean) nativeQuery.getSingleResult();
|
||||||
|
assertThat(deleted).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("remove sets deleted to true")
|
||||||
|
void remove_setsDeletedTrue() {
|
||||||
|
var entity = new TestSoftDeleteEntity("张三", 25);
|
||||||
|
Long id = service.save(entity);
|
||||||
|
repository.flush();
|
||||||
|
entityManager.clear();
|
||||||
|
|
||||||
|
service.remove(id);
|
||||||
|
repository.flush();
|
||||||
|
|
||||||
|
var nativeQuery = entityManager.createNativeQuery("SELECT deleted FROM test_soft_delete_entity WHERE id = ?");
|
||||||
|
nativeQuery.setParameter(1, id);
|
||||||
|
var deleted = (Boolean) nativeQuery.getSingleResult();
|
||||||
|
assertThat(deleted).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("batch remove sets deleted to true")
|
||||||
|
void batchRemove_setsDeletedTrue() {
|
||||||
|
var e1 = new TestSoftDeleteEntity("张三", 25);
|
||||||
|
var e2 = new TestSoftDeleteEntity("李四", 30);
|
||||||
|
Long id1 = service.save(e1);
|
||||||
|
Long id2 = service.save(e2);
|
||||||
|
|
||||||
|
service.remove(Set.of(id1, id2));
|
||||||
|
|
||||||
|
var nativeQuery = entityManager.createNativeQuery("SELECT COUNT(*) FROM test_soft_delete_entity WHERE deleted = true");
|
||||||
|
var count = ((Number) nativeQuery.getSingleResult()).longValue();
|
||||||
|
assertThat(count).isEqualTo(2);
|
||||||
|
|
||||||
|
assertThat(repository.findAll()).isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Query Behavior")
|
||||||
|
class QueryTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("list filters out deleted entities")
|
||||||
|
void list_filtersDeletedEntities() {
|
||||||
|
var e1 = repository.save(new TestSoftDeleteEntity("张三", 25));
|
||||||
|
repository.save(new TestSoftDeleteEntity("李四", 30));
|
||||||
|
|
||||||
|
entityManager.createNativeQuery("UPDATE test_soft_delete_entity SET deleted = true WHERE id = ?")
|
||||||
|
.setParameter(1, e1.getId())
|
||||||
|
.executeUpdate();
|
||||||
|
entityManager.clear();
|
||||||
|
|
||||||
|
List<TestSoftDeleteEntity> result = service.list();
|
||||||
|
|
||||||
|
assertThat(result).hasSize(1);
|
||||||
|
assertThat(result.get(0).getName()).isEqualTo("李四");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("count excludes deleted entities")
|
||||||
|
void count_excludesDeletedEntities() {
|
||||||
|
var e1 = repository.save(new TestSoftDeleteEntity("张三", 25));
|
||||||
|
repository.save(new TestSoftDeleteEntity("李四", 30));
|
||||||
|
|
||||||
|
entityManager.createNativeQuery("UPDATE test_soft_delete_entity SET deleted = true WHERE id = ?")
|
||||||
|
.setParameter(1, e1.getId())
|
||||||
|
.executeUpdate();
|
||||||
|
entityManager.clear();
|
||||||
|
|
||||||
|
Long count = service.count();
|
||||||
|
|
||||||
|
assertThat(count).isEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("detail returns null for deleted entity")
|
||||||
|
void detail_returnsNullForDeletedEntity() {
|
||||||
|
var entity = repository.save(new TestSoftDeleteEntity("张三", 25));
|
||||||
|
|
||||||
|
entityManager.createNativeQuery("UPDATE test_soft_delete_entity SET deleted = true WHERE id = ?")
|
||||||
|
.setParameter(1, entity.getId())
|
||||||
|
.executeUpdate();
|
||||||
|
entityManager.clear();
|
||||||
|
|
||||||
|
TestSoftDeleteEntity result = service.detail(entity.getId());
|
||||||
|
|
||||||
|
assertThat(result).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("query with conditions filters deleted entities")
|
||||||
|
void queryWithConditions_filtersDeletedEntities() {
|
||||||
|
var e1 = repository.save(new TestSoftDeleteEntity("张三", 25));
|
||||||
|
repository.save(new TestSoftDeleteEntity("李四", 30));
|
||||||
|
|
||||||
|
entityManager.createNativeQuery("UPDATE test_soft_delete_entity SET deleted = true WHERE id = ?")
|
||||||
|
.setParameter(1, e1.getId())
|
||||||
|
.executeUpdate();
|
||||||
|
entityManager.clear();
|
||||||
|
|
||||||
|
var query = new QueryBuilder().equal(Map.of("name", "张三")).build();
|
||||||
|
|
||||||
|
var result = service.list(query);
|
||||||
|
|
||||||
|
assertThat(result.items()).isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Restore Behavior")
|
||||||
|
class RestoreTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("restore deleted entity makes it visible again")
|
||||||
|
void restore_makesEntityVisible() {
|
||||||
|
var entity = repository.save(new TestSoftDeleteEntity("张三", 25));
|
||||||
|
|
||||||
|
entityManager.createNativeQuery("UPDATE test_soft_delete_entity SET deleted = true WHERE id = ?")
|
||||||
|
.setParameter(1, entity.getId())
|
||||||
|
.executeUpdate();
|
||||||
|
entityManager.clear();
|
||||||
|
|
||||||
|
assertThat(service.list()).isEmpty();
|
||||||
|
|
||||||
|
entityManager.createNativeQuery("UPDATE test_soft_delete_entity SET deleted = false WHERE id = ?")
|
||||||
|
.setParameter(1, entity.getId())
|
||||||
|
.executeUpdate();
|
||||||
|
entityManager.clear();
|
||||||
|
|
||||||
|
List<TestSoftDeleteEntity> result = service.list();
|
||||||
|
assertThat(result).hasSize(1);
|
||||||
|
assertThat(result.get(0).getName()).isEqualTo("张三");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.lanyuanxiaoyao.service.template.database.integration.controller;
|
||||||
|
|
||||||
|
import com.lanyuanxiaoyao.service.template.database.integration.entity.TestEntity;
|
||||||
|
import com.lanyuanxiaoyao.service.template.database.integration.entity.TestStatus;
|
||||||
|
import com.lanyuanxiaoyao.service.template.database.integration.service.TestService;
|
||||||
|
import com.lanyuanxiaoyao.service.template.database.controller.SimpleControllerSupport;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/test")
|
||||||
|
public class TestController extends SimpleControllerSupport<TestEntity, TestController.SaveItem, TestController.ListItem, TestController.DetailItem> {
|
||||||
|
public TestController(TestService service) {
|
||||||
|
super(service);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Function<SaveItem, TestEntity> saveItemMapper() {
|
||||||
|
return item -> new TestEntity(item.name, item.age, item.status, item.salary, item.tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Function<TestEntity, ListItem> listItemMapper() {
|
||||||
|
return entity -> new ListItem(entity.getId(), entity.getName(), entity.getAge(), entity.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Function<TestEntity, DetailItem> detailItemMapper() {
|
||||||
|
return entity -> new DetailItem(entity.getId(), entity.getName(), entity.getAge(), entity.getStatus(), entity.getSalary(), entity.getTags());
|
||||||
|
}
|
||||||
|
|
||||||
|
public record SaveItem(String name, Integer age, TestStatus status, Double salary, String tags) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ListItem(Long id, String name, Integer age, TestStatus status) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record DetailItem(Long id, String name, Integer age, TestStatus status, Double salary, String tags) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.lanyuanxiaoyao.service.template.database.integration.controller;
|
||||||
|
|
||||||
|
import com.lanyuanxiaoyao.service.template.database.integration.entity.TestSoftDeleteEntity;
|
||||||
|
import com.lanyuanxiaoyao.service.template.database.integration.service.TestSoftDeleteService;
|
||||||
|
import com.lanyuanxiaoyao.service.template.database.controller.SimpleControllerSupport;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/test-soft-delete")
|
||||||
|
public class TestSoftDeleteController extends SimpleControllerSupport<TestSoftDeleteEntity, TestSoftDeleteController.SaveItem, TestSoftDeleteController.ListItem, TestSoftDeleteController.DetailItem> {
|
||||||
|
public TestSoftDeleteController(TestSoftDeleteService service) {
|
||||||
|
super(service);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Function<SaveItem, TestSoftDeleteEntity> saveItemMapper() {
|
||||||
|
return item -> new TestSoftDeleteEntity(item.name, item.age);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Function<TestSoftDeleteEntity, ListItem> listItemMapper() {
|
||||||
|
return entity -> new ListItem(entity.getId(), entity.getName(), entity.getAge());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Function<TestSoftDeleteEntity, DetailItem> detailItemMapper() {
|
||||||
|
return entity -> new DetailItem(entity.getId(), entity.getName(), entity.getAge());
|
||||||
|
}
|
||||||
|
|
||||||
|
public record SaveItem(String name, Integer age) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ListItem(Long id, String name, Integer age) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record DetailItem(Long id, String name, Integer age) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.lanyuanxiaoyao.service.template.database.integration.entity;
|
||||||
|
|
||||||
|
import com.lanyuanxiaoyao.service.template.database.entity.SimpleEntity;
|
||||||
|
import com.lanyuanxiaoyao.service.template.database.entity.SnowflakeId;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.EnumType;
|
||||||
|
import jakarta.persistence.Enumerated;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.ToString;
|
||||||
|
import lombok.experimental.FieldNameConstants;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "test_entity")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@ToString
|
||||||
|
@FieldNameConstants
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class TestEntity extends SimpleEntity {
|
||||||
|
@SnowflakeId
|
||||||
|
@Column(comment = "名称")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Column(comment = "年龄")
|
||||||
|
private Integer age;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(comment = "状态")
|
||||||
|
private TestStatus status;
|
||||||
|
|
||||||
|
@Column(comment = "薪资")
|
||||||
|
private Double salary;
|
||||||
|
|
||||||
|
@Column(comment = "标签")
|
||||||
|
private String tags;
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.lanyuanxiaoyao.service.template.database.integration.entity;
|
||||||
|
|
||||||
|
import com.lanyuanxiaoyao.service.template.database.entity.SimpleEntity;
|
||||||
|
import com.lanyuanxiaoyao.service.template.database.entity.SnowflakeId;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.ToString;
|
||||||
|
import lombok.experimental.FieldNameConstants;
|
||||||
|
import org.hibernate.annotations.SoftDelete;
|
||||||
|
import org.hibernate.annotations.SoftDeleteType;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "test_soft_delete_entity")
|
||||||
|
@SoftDelete(strategy = SoftDeleteType.DELETED)
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@ToString
|
||||||
|
@FieldNameConstants
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class TestSoftDeleteEntity extends SimpleEntity {
|
||||||
|
@SnowflakeId
|
||||||
|
@Column(comment = "名称")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Column(comment = "年龄")
|
||||||
|
private Integer age;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user