1
0

Compare commits

...

16 Commits

Author SHA1 Message Date
6bf9a3295a docs: 将 database 模块文档拆分为独立文件 2026-04-01 17:31:25 +08:00
0a7e38f931 test: 添加全面的单元测试和集成测试
- 添加 common 模块单元测试 (ObjectHelper, SnowflakeHelper)
- 添加 database 模块集成测试 (SimpleServiceSupport, @SoftDelete)
- 添加 Controller REST API 契约测试
- 配置 H2 数据库和 p6spy 用于测试
- 更新 openspec 配置,添加并行任务和提问工具规则
2026-04-01 16:15:15 +08:00
fc9cb14daf chore: 将 .vscode 从 git 跟踪中移除并加入 gitignore 2026-04-01 11:06:42 +08:00
4abb65129b docs: 精简 README 和 openspec 配置,添加 AGENTS.md 2026-04-01 11:03:28 +08:00
8ef18a8e85 refactor(database): 合并数据库模块,简化包结构 2026-03-31 23:55:12 +08:00
22811e4adb chore: 添加 gitignore 规则和 openspec 配置 2026-03-31 11:19:10 +08:00
28bcc1a118 docs: 重构 README 文档,精简至 220 行,提取通用规范 2026-03-31 11:18:34 +08:00
e9b0e79d48 feat: 优化日志显示 2026-01-27 10:06:57 +08:00
18cd1dbed8 docs: 更新文档 2026-01-27 10:05:31 +08:00
2db84152b5 feat(jpa): 补充方法查询的测试用例 2026-01-22 23:55:26 +08:00
28baf5600b fix(jpa): 优化测试用例 2026-01-22 22:51:21 +08:00
015016a2da fix(jpa): 优化测试用例 2026-01-22 22:36:57 +08:00
7b555492ee feat(jpa): 增加默认 JpaQueryFactory 的注入 2026-01-22 22:36:38 +08:00
e7fa23a365 fix(jpa): 优化注释 2026-01-22 22:18:37 +08:00
2adf4951f7 fix(database): 补充事务注解 2026-01-22 22:18:37 +08:00
3692657b64 docs: 补充README 2026-01-22 22:18:37 +08:00
106 changed files with 2656 additions and 4256 deletions

11
.gitignore vendored
View File

@@ -54,12 +54,7 @@ buildNumber.properties
*.rar
hs_err_pid*
replay_pid*
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
.vscode/
.history/
*.vsix
.metadata
@@ -132,3 +127,7 @@ Icon
Network Trash Folder
Temporary Items
.apdisk
.opencode
.claude
.kilocode
openspec/changes/archive

1
AGENTS.md Normal file
View File

@@ -0,0 +1 @@
严格遵守openspec/config.yaml中context声明的项目规范

506
README.md
View File

@@ -1,469 +1,109 @@
# 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 整体架构图
```
┌─────────────────────────────────────┐
│ Controller Layer │
│ (处理HTTP请求数据转换与响应) │
├─────────────────────────────────────┤
│ Service Layer │
│ (业务逻辑处理,事务管理) │
├─────────────────────────────────────┤
│ Repository Layer │
│ (数据访问,数据库交互) │
├─────────────────────────────────────┤
│ Entity Layer │
│ (数据模型定义,实体映射) │
└─────────────────────────────────────┘
spring-boot-service-template/ (根 POM, packaging=pom)
├── spring-boot-service-template-common/ (jar — 通用工具)
└── spring-boot-service-template-database/ (jar — JPA 数据库能力)
```
#### 2.2.2 模块划分
- controller: 控制层,处理 HTTP 请求,包括接口定义和支持类
- entity: 实体层,定义数据模型和数据库映射
- service: 服务层,处理业务逻辑和事务管理
- repository: 仓储层,处理数据访问和数据库交互
- helper: 辅助类模块,提供工具类和通用方法
模块命名规则: `spring-boot-service-template-{capability}[-{impl}]`
#### 2.2.3 设计模式
- 模板方法模式:通过抽象类定义通用操作流程
- 策略模式:通过函数式接口实现数据转换策略
- 仓储模式:封装数据访问逻辑,提供统一的数据操作接口
## 包结构
## 3. 项目结构
### 3.1 目录结构说明
```
src/
├── main/
│ └── java/
│ └── com/lanyuanxiaoyao/service/template/
│ ├── controller/
├── entity/
├── helper/
├── repository/
│ └── service/
└── test/
└── java/
└── com/lanyuanxiaoyao/service/template/
├── controller/
├── entity/
├── repository/
└── service/
com.lanyuanxiaoyao.service.template.{module}/
configuration/ # @Configuration用 configuration 不用 config
controller/ # REST 控制器 + 接口
entity/ # JPA 实体、Query、GlobalResponse 等数据对象
exception/ # 自定义异常(用 exception 不用 exceptions
helper/ # 工具类(用 helper 不用 util/utils
repository/ # Spring Data 仓库
service/ # 业务服务
```
### 3.2 核心模块介绍
无独立 DTO/VO 包DTO 作为 Controller 内部 record。
#### 3.2.1 controller模块
提供基础的 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
## database 模块
#### 3.2.2 entity模块
定义基础实体类,包含审计字段。主要包含:
- [IdOnlyEntity](src/main/java/com/lanyuanxiaoyao/service/template/entity/IdOnlyEntity.java):仅包含 ID 的基础实体
- [SimpleEntity](src/main/java/com/lanyuanxiaoyao/service/template/entity/SimpleEntity.java):包含基础字段的实体类,继承自 IdOnlyEntity
单表 CRUD → REST 接口快速实现框架。基于 JPA + Fenix + QueryDSL + MapStruct。
#### 3.2.3 service模块
提供基础服务接口和支持类。主要包含:
- [SimpleService](src/main/java/com/lanyuanxiaoyao/service/template/service/SimpleService.java):定义基础服务接口
- [SimpleServiceSupport](src/main/java/com/lanyuanxiaoyao/service/template/service/SimpleServiceSupport.java):实现基础服务功能
**文档**
- [开发指南](docs/database-development.md) - 模块架构、核心设计、技术实现、扩展指南
- [使用指南](docs/database-usage.md) - 快速开始、API 接口、查询条件、高级用法
#### 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)
支持通过 POST 请求创建实体对象。通过 save 接口实现,接收 SAVE_ITEM 类型的参数,通过 Mapper 转换为实体对象后保存。
- 2 空格缩进K&R 大括号,不用 Tab
- 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)
支持通过 GET 请求删除实体对象。通过 remove 接口实现,根据 ID 删除指定实体。
- 实体: `@Getter` `@Setter` `@ToString` `@FieldNameConstants`,子类加 `@ToString(callSuper = true)`
- 日志: `@Slf4j`
- 注入: `@RequiredArgsConstructor(access = AccessLevel.PROTECTED)`(构造器注入)
- 事务: 写 `@Transactional(rollbackFor = Throwable.class)`,读 `@Transactional(readOnly = true)`
- 实体基类: `@MappedSuperclass` `@EntityListeners(AuditingEntityListener.class)`
### 4.2 查询功能详解
不用:
#### 4.2.1 简单查询
支持基于 ID 的简单查询,通过 detail 接口实现。
- 不用 `@Valid`/`@NotNull` 等校验注解
- 不用 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 分页查询
支持分页查询功能,通过 page 配置实现分页参数设置。
继承 `RuntimeException`,构造器用 `String.formatted()`。查无数据用 `Optional.orElseThrow(() -> new IdNotFoundException(id))`。无全局 `@ControllerAdvice`
#### 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 审计字段
包含创建时间(createdTime)和修改时间(modifiedTime)等审计字段,通过 Spring Data JPA 的审计功能自动维护。
- 文档/注释/commit 用中文,代码标识符用英文
- 不引入新依赖前先复用已有组件,优先 JDK 和 Spring 内置能力
- 构造器注入,不使用 `@Autowired` 字段注入
#### 4.3.3 实体关系
支持常见的实体关系映射,如一对一、一对多、多对多等。
### 构建
## 5. 使用指南
### 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>
```bash
mvn clean package
```
#### 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 功能。

View 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 位 Long1 位符号 + 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
View 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. 创建 ControllerWeb 应用)
```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
View 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
View File

@@ -11,11 +11,7 @@
<modules>
<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/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>
<module>spring-boot-service-template-database</module>
</modules>
<properties>
@@ -31,8 +27,6 @@
<mapstruct.version>1.6.3</mapstruct.version>
<mapstruct-plus.version>1.5.0</mapstruct-plus.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>
</properties>
@@ -57,12 +51,7 @@
</dependency>
<dependency>
<groupId>com.lanyuanxiaoyao</groupId>
<artifactId>spring-boot-service-template-database-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.lanyuanxiaoyao</groupId>
<artifactId>spring-boot-service-template-database-common-test</artifactId>
<artifactId>spring-boot-service-template-database</artifactId>
<version>${project.version}</version>
</dependency>
@@ -117,22 +106,6 @@
<version>${hibernate.version}</version>
</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>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>

View File

@@ -11,6 +11,14 @@
<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>
<plugins>
<plugin>

View File

@@ -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
}
}

View File

@@ -9,12 +9,12 @@
<version>1.1.0-SNAPSHOT</version>
</parent>
<artifactId>spring-boot-service-template-database-jpa</artifactId>
<artifactId>spring-boot-service-template-database</artifactId>
<dependencies>
<dependency>
<groupId>com.lanyuanxiaoyao</groupId>
<artifactId>spring-boot-service-template-database-common</artifactId>
<artifactId>spring-boot-service-template-common</artifactId>
</dependency>
<dependency>
@@ -35,15 +35,37 @@
<groupId>io.github.openfeign.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-ant</artifactId>
</dependency>
<dependency>
<groupId>com.lanyuanxiaoyao</groupId>
<artifactId>spring-boot-service-template-database-common-test</artifactId>
<groupId>org.mapstruct</groupId>
<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>
</dependency>
</dependencies>
@@ -87,6 +109,11 @@
<artifactId>jakarta.persistence-api</artifactId>
<version>3.2.0</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<arg>-Aquerydsl.entityAccessors=true</arg>
@@ -94,6 +121,10 @@
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -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>

View File

@@ -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(), "请求失败");
}
}

View File

@@ -1,12 +0,0 @@
server:
port: 2490
decorator:
datasource:
p6spy:
multiline: false
exclude-categories:
- commit
- result
- resultset
- rollback
log-format: "%(category)|%(executionTime)|%(sqlSingleLine)"

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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());
});
}
}
}

View File

@@ -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
);

View File

@@ -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();
}
}

View File

@@ -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
) {
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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");
// 查找年龄大于30Senior或年龄在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() {
}
}

View File

@@ -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
) {
}
}

View File

@@ -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
) {
}
}

View File

@@ -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
) {
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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,
}
}

View File

@@ -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
}
}

View File

@@ -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;
}

View File

@@ -1,9 +0,0 @@
package com.lanyuanxiaoyao.service.template.database.jpa.entity.vo;
public record EmployeeWithCompanyName(
String name,
String companyName,
Integer age,
String role
) {
}

View File

@@ -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> {
}

View File

@@ -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);
}

View File

@@ -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> {
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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>

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -1,6 +0,0 @@
package com.lanyuanxiaoyao.service.template.database.xbatis.mapper;
import cn.xbatis.core.mybatis.mapper.BasicMapper;
public interface MybatisBasicMapper extends BasicMapper {
}

View File

@@ -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()));
}
}
}

View File

@@ -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
);

View File

@@ -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, "查询数量错误");
}
}

View File

@@ -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
) {
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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.common.entity.Query;
import com.lanyuanxiaoyao.service.template.database.entity.GlobalResponse;
import com.lanyuanxiaoyao.service.template.database.entity.Query;
/**
* 查询控制器接口用于定义统一的查询实体详情和列表的接口规范

View File

@@ -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;
/**
* 删除控制器接口用于定义统一的删除实体对象的接口规范

View File

@@ -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;
/**
* 保存控制器接口用于定义统一的保存实体对象的接口规范

View File

@@ -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 {
}

View File

@@ -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.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.jpa.entity.SimpleEntity;
import com.lanyuanxiaoyao.service.template.database.jpa.service.SimpleServiceSupport;
import com.lanyuanxiaoyao.service.template.database.entity.GlobalResponse;
import com.lanyuanxiaoyao.service.template.database.entity.Query;
import com.lanyuanxiaoyao.service.template.database.entity.SimpleEntity;
import com.lanyuanxiaoyao.service.template.database.service.SimpleServiceSupport;
import java.util.function.Function;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;

View File

@@ -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.Map;

View File

@@ -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.EntityListeners;

View File

@@ -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.util.List;

View File

@@ -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.EntityListeners;

View File

@@ -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.Retention;

View File

@@ -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 lombok.extern.slf4j.Slf4j;
import org.hibernate.engine.spi.SharedSessionContractImplementor;

View File

@@ -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 IdNotFoundException(Long id) {

View File

@@ -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 NotCollectionException(String variable) {

View File

@@ -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 NotComparableException(String variable) {

View File

@@ -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 NotStringException(String variable) {

View File

@@ -1,4 +1,4 @@
package com.lanyuanxiaoyao.service.template.database.jpa.helper;
package com.lanyuanxiaoyao.service.template.database.helper;
import jakarta.persistence.Entity;
import java.io.IOException;
@@ -115,7 +115,7 @@ public class DatabaseHelper {
package %s.repository;
import %s;
import com.lanyuanxiaoyao.service.template.repository.SimpleRepository;
import com.lanyuanxiaoyao.service.template.database.repository.SimpleRepository;
import org.springframework.stereotype.Repository;
@Repository
@@ -134,7 +134,7 @@ public class DatabaseHelper {
import %s;
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 org.springframework.stereotype.Service;
@@ -158,7 +158,7 @@ public class DatabaseHelper {
import %s;
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 lombok.extern.slf4j.Slf4j;
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) {
throw new RuntimeException("Failed to load entity class: " + className, e);

View File

@@ -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.specification.FenixJpaSpecificationExecutor;

View File

@@ -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.database.common.entity.Query;
import com.lanyuanxiaoyao.service.template.database.entity.Query;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;

View File

@@ -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.common.entity.Query;
import com.lanyuanxiaoyao.service.template.database.entity.Page;
import com.lanyuanxiaoyao.service.template.database.entity.Query;
import java.util.List;
import java.util.Set;

View File

@@ -1,4 +1,4 @@
package com.lanyuanxiaoyao.service.template.database.common.service;
package com.lanyuanxiaoyao.service.template.database.service;
/**
* 保存服务接口用于定义统一的保存实体对象的服务规范

View File

@@ -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> {
}

View File

@@ -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.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.exception.NotCollectionException;
import com.lanyuanxiaoyao.service.template.database.common.exception.NotComparableException;
import com.lanyuanxiaoyao.service.template.database.common.exception.NotStringException;
import com.lanyuanxiaoyao.service.template.database.common.service.QueryParser;
import com.lanyuanxiaoyao.service.template.database.common.service.SimpleService;
import com.lanyuanxiaoyao.service.template.database.jpa.entity.IdOnlyEntity;
import com.lanyuanxiaoyao.service.template.database.jpa.entity.SimpleEntity;
import com.lanyuanxiaoyao.service.template.database.jpa.repository.SimpleRepository;
import com.lanyuanxiaoyao.service.template.database.entity.Page;
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.entity.IdOnlyEntity;
import com.lanyuanxiaoyao.service.template.database.entity.SimpleEntity;
import com.lanyuanxiaoyao.service.template.database.repository.SimpleRepository;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Path;
@@ -77,16 +75,6 @@ public abstract class SimpleServiceSupport<ENTITY extends SimpleEntity> implemen
this.repository = repository;
}
/**
* 保存实体对象
* <p>
* 使用saveOrUpdateByNotNullProperties方法保存实体只更新非空字段
* 该方法具有事务性遇到任何异常都会回滚
* </p>
*
* @param entity 需要保存的实体对象
* @return 返回保存后的实体ID
*/
@Transactional(rollbackFor = Throwable.class)
@Override
public Long save(ENTITY entity) {
@@ -94,56 +82,25 @@ public abstract class SimpleServiceSupport<ENTITY extends SimpleEntity> implemen
return entity.getId();
}
/**
* 批量保存实体对象集合
* <p>
* 使用saveOrUpdateAllByNotNullProperties方法只更新非空字段
* 该方法具有事务性遇到任何异常都会回滚
* </p>
*
* @param entities 需要保存的实体对象集合
*/
@Transactional(rollbackFor = Throwable.class)
@Override
public void save(Iterable<ENTITY> entities) {
repository.saveOrUpdateAllByNotNullProperties(entities);
}
/**
* 统计符合条件的实体数量
* <p>
* 根据listPredicate方法构建的条件统计实体数量
* </p>
*
* @return 返回符合条件的实体数量
*/
@Transactional(readOnly = true)
@Override
public Long count() {
return repository.count(this::commonPredicates);
}
/**
* 获取所有符合条件的实体列表
* <p>
* 根据listPredicate方法构建的条件查询所有实体
* </p>
*
* @return 返回符合条件的实体列表
*/
@Transactional(readOnly = true)
@Override
public List<ENTITY> list() {
return repository.findAll(this::commonPredicates);
}
/**
* 根据ID集合获取实体列表
* <p>
* 根据提供的ID集合查询对应的实体列表并结合listPredicate方法构建的条件
* </p>
*
* @param ids ID集合
* @return 返回ID集合对应的实体列表
*/
@Transactional(readOnly = true)
@Override
public List<ENTITY> list(Set<Long> ids) {
if (ObjectHelper.isEmpty(ids)) {
@@ -164,16 +121,7 @@ public abstract class SimpleServiceSupport<ENTITY extends SimpleEntity> implemen
return null;
}
/**
* 根据查询条件分页获取实体列表
* <p>
* 支持复杂的查询条件和分页功能
* 默认分页参数第1页每页10条记录按创建时间降序排列
* </p>
*
* @param listQuery 查询条件对象
* @return 返回分页查询结果
*/
@Transactional(readOnly = true)
@Override
public Page<ENTITY> list(Query listQuery) {
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")
@Transactional(readOnly = true)
@Override
public ENTITY detail(Long id) {
return detailOptional(id).orElse(null);
}
/**
* 根据ID获取实体详情不存在时抛出异常
* <p>
* 如果实体不存在则抛出IdNotFoundException异常
* </p>
*
* @param id 实体ID
* @return 返回实体详情
* @throws IdNotFoundException 当实体不存在时抛出
*/
@Named("detailOrThrow")
@Transactional(readOnly = true)
@Override
public ENTITY detailOrThrow(Long id) {
return detailOptional(id).orElseThrow(() -> new IdNotFoundException(id));
}
/**
* 根据ID删除实体
* <p>
* 具有事务性遇到任何异常都会回滚
* 如果ID为空则不执行任何操作
* </p>
*
* @param id 实体主键ID
*/
@Transactional(rollbackFor = Throwable.class)
@Override
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)
@Override
public void remove(Set<Long> ids) {

View File

@@ -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);
}
}
}

View File

@@ -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 {
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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("张三");
}
}
}

View File

@@ -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) {
}
}

View File

@@ -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) {
}
}

View File

@@ -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;
}

View File

@@ -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