1
0

Compare commits

...

5 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
106 changed files with 2624 additions and 4654 deletions

9
.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
@@ -133,4 +128,6 @@ 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声明的项目规范

295
README.md
View File

@@ -1,260 +1,109 @@
# Spring Boot Service Template
微服务快速开发能力模板,提供标准化的能力模块,帮助开发者快速构建 Spring Boot 微服务应用
微服务快速开发能力模板,v1.1.0-SNAPSHOT。Java 17, Spring Boot 4.0.0, Spring Cloud 2025.1.0
**当前版本**: 1.1.0-SNAPSHOT
GroupId: `com.lanyuanxiaoyao`,根包: `com.lanyuanxiaoyao.service.template`
## 1. 项目概述
Spring Boot Service Template 是一个能力模块化的微服务开发模板。每个能力模块封装特定领域的通用功能,开发者只需引入依赖、实现少量代码即可获得完整的业务能力。
**核心价值**
- 能力模块化:每个模块独立可用,按需引入
- 零样板代码:通用逻辑已封装,专注业务实现
- 多实现支持:同一能力支持多种技术实现,灵活选择
## 2. 架构
## 模块
```
┌─────────────────────────────────────────────────────────────────────┐
│ Spring Boot Service Template
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 能力模块 (Capability) │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ database │ │ user │ │ payment │ ... │ │
│ │ │ ✅ 已实现 │ │ 🔜 规划中 │ │ 🔜 规划中 │ │ │
│ │ └──────┬──────┘ └─────────────┘ └─────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ jpa │ eq │ xbatis (实现) │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 通用基础 (Common) │ │
│ │ │ │
│ │ ObjectHelper (对象工具) │ SnowflakeHelper (ID生成) │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
spring-boot-service-template/ (根 POM, packaging=pom)
├── spring-boot-service-template-common/ (jar — 通用工具)
└── spring-boot-service-template-database/ (jar — JPA 数据库能力)
```
**模块依赖关系**
模块命名规则: `spring-boot-service-template-{capability}[-{impl}]`
## 包结构
```
┌─────────────┐
应用 │
└──────┬──────┘
┌─────────────────┐ ┌───────────────────┐
│ capability-common│────▶│ capability-impl │
(接口定义) │ (jpa/eq/xbatis) │
└───────┬─────────┘ └───────────────────┘
┌─────────────┐
│ common │
│ (通用工具) │
└─────────────┘
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. 通用规范
无独立 DTO/VO 包DTO 作为 Controller 内部 record。
### 3.1 模块命名
## database 模块
| 模块类型 | 命名格式 | 示例 |
|---------|---------|------|
| 能力模块 | `spring-boot-service-template-{capability}` | database |
| 通用定义 | `spring-boot-service-template-{capability}-common` | database-common |
| 具体实现 | `spring-boot-service-template-{capability}-{impl}` | database-jpa |
| 通用基础 | `spring-boot-service-template-common` | common |
单表 CRUD → REST 接口快速实现框架。基于 JPA + Fenix + QueryDSL + MapStruct。
### 3.2 依赖原则
**文档**
- [开发指南](docs/database-development.md) - 模块架构、核心设计、技术实现、扩展指南
- [使用指南](docs/database-usage.md) - 快速开始、API 接口、查询条件、高级用法
- **common 模块**:与业务无关的工具类,不依赖任何能力模块
- **capability-common**:定义接口和通用实体,不依赖具体实现技术
- **最小化第三方依赖**:优先使用 JDK 和 Spring 内置能力
## 开发规范
### 3.3 响应格式
### 命名
所有 API 使用统一的 `GlobalResponse<T>` 格式:
| 类别 | 模式 | 示例 |
| --------------- | ---------------------------------------- | ---------------------------------- |
| 实体基类 | `*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 内部 |
```json
{
"status": 0,
"message": "OK",
"data": { ... }
}
```
- 接口不加 `I` 前缀
- 方法: 动词开头 `save()`/`list()`/`detail()`/`remove()`;布尔用 `is`/`has` 前缀
- 常量 UPPER_SNAKE_CASE字段常量用 `@FieldNameConstants` 生成 `{Class}.Fields.{field}`
- 泛型: 简单用 `T`/`E`,领域用 `ENTITY`/`SAVE_ITEM`/`LIST_ITEM`/`DETAIL_ITEM`
| 字段 | 说明 |
|-----|------|
| status | 状态码0=成功500=错误 |
| message | 描述信息 |
| data | 业务数据 |
### 代码风格
列表数据封装:
- 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>`
- 不写 `//` 行内注释,除非必要
```json
{
"status": 0,
"message": "OK",
"data": {
"items": [...],
"total": 100
}
}
```
### 注解
### 3.4 代码风格
用:
- **接口与实现分离**:接口定义在 `*-common`,实现在具体模块
- **空值检查**:使用 `ObjectHelper.isNull/isNotNull/isEmpty/isNotEmpty`
- **异常定义**:继承 `RuntimeException`构造器接收业务参数
- **注解使用**`@Getter` / `@Setter` / `@FieldNameConstants` / `@NoArgsConstructor`
- 实体: `@Getter` `@Setter` `@ToString` `@FieldNameConstants`,子类加 `@ToString(callSuper = true)`
- 日志: `@Slf4j`
- 注入: `@RequiredArgsConstructor(access = AccessLevel.PROTECTED)`构造器注入)
- 事务: 写 `@Transactional(rollbackFor = Throwable.class)`,读 `@Transactional(readOnly = true)`
- 实体基类: `@MappedSuperclass` `@EntityListeners(AuditingEntityListener.class)`
## 4. 能力模块
不用:
### 4.1 database 模块
- 不用 `@Valid`/`@NotNull` 等校验注解
- 不用 Swagger/OpenAPI 注解
- 不用 `@Autowired` 字段注入
- 不写代码注释(`//`),除非必要
单表 CRUD → REST 接口的快速实现,支持三种数据库访问方式。
### 异常
#### 模块概述
继承 `RuntimeException`,构造器用 `String.formatted()`。查无数据用 `Optional.orElseThrow(() -> new IdNotFoundException(id))`。无全局 `@ControllerAdvice`
| 功能 | 说明 |
|-----|------|
| 保存 | 单条/批量保存,自动判断新增/更新 |
| 查询 | 详情查询、列表查询、分页查询、条件查询 |
| 删除 | 单条/批量删除 |
### 依赖管理
#### 实现方式对比
版本集中在根 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
| 特性 | JPA | Easy Query | Xbatis |
|-----|-----|------------|--------|
| 类型安全查询 | ✅ QueryDSL | ✅ Proxy API | ❌ |
| 自动审计 | ✅ | ❌ | ❌ |
| 逻辑删除 | ❌ | ✅ | ✅ |
| 复杂 SQL | 中等 | 中等 | 强 |
| 适用场景 | 标准 JPA 项目 | 类型安全 + 逻辑删除 | 复杂查询场景 |
### 通用原则
#### 快速开始
- 文档/注释/commit 用中文,代码标识符用英文
- 不引入新依赖前先复用已有组件,优先 JDK 和 Spring 内置能力
- 构造器注入,不使用 `@Autowired` 字段注入
**1. 添加依赖**
### 构建
```xml
<!-- JPA 实现 -->
<dependency>
<groupId>com.lanyuanxiaoyao</groupId>
<artifactId>spring-boot-service-template-database-jpa</artifactId>
<version>1.1.0-SNAPSHOT</version>
</dependency>
```
**2. 实现 CRUD 的 5 个步骤**
参考测试代码:`spring-boot-service-template-database-jpa/src/test/java/.../`
| 步骤 | 说明 | 参考文件 |
|-----|------|---------|
| 1. 创建实体 | 继承 `SimpleEntity` | `entity/Employee.java` |
| 2. 创建 Repository | 继承 `SimpleRepository<ENTITY>` | `repository/EmployeeRepository.java` |
| 3. 创建 Service | 继承 `SimpleServiceSupport<ENTITY>` | `service/EmployeeService.java` |
| 4. 创建 DTO | 定义 SaveItem, ListItem, DetailItem | `controller/EmployeeController.java` |
| 5. 创建 Controller | 继承 `SimpleControllerSupport`,实现 3 个 Mapper | `controller/EmployeeController.java` |
**3. 查询条件语法**
```json
{
"query": {
"equal": { "name": "张三" },
"like": { "name": "%张%" },
"contain": { "name": "张" },
"startWith": { "name": "张" },
"endWith": { "name": "三" },
"great": { "age": 18 },
"less": { "age": 60 },
"greatEqual": { "age": 18 },
"lessEqual": { "age": 60 },
"between": { "age": { "start": 18, "end": 60 } },
"inside": { "id": [1, 2, 3] },
"notInside": { "id": [1, 2, 3] },
"nullEqual": ["deletedTime"],
"notNullEqual": ["name"]
},
"sort": [{ "column": "createdTime", "direction": "DESC" }],
"page": { "index": 1, "size": 10 }
}
```
#### 模块特定规范
**分层结构**
```
Controller (HTTP处理、DTO转换)
Service (业务逻辑、事务管理)
Repository/Mapper (数据访问)
Entity (数据模型)
```
**泛型参数**
- `ENTITY` - 实体类型
- `SAVE_ITEM` - 保存请求 DTO
- `LIST_ITEM` - 列表响应 DTO
- `DETAIL_ITEM` - 详情响应 DTO
**实体继承**
- JPA: `IdOnlyEntity``SimpleEntity` → 业务实体
- EQ/Xbatis: `IdOnlyEntity``SimpleEntity``LogicDeleteEntity`
### 4.2 user 模块
🚧 规划中...
### 4.3 payment 模块
🚧 规划中...
## 5. 技术栈
| 组件 | 版本 |
|-----|------|
| Java | 17 |
| Spring Boot | 4.0.0 |
| Spring Cloud | 2025.1.0 |
| Hibernate | 7.1.8.Final |
| QueryDSL | 7.1 |
| Easy Query | 3.1.68 |
| Xbatis | 1.9.7-spring-boot4 |
| Lombok | - |
| MapStruct | 1.6.3 |
## 6. 开发与测试
**构建项目**
```bash
mvn clean package
```
**运行测试**
```bash
mvn test
```
测试使用 H2 内存数据库,无需额外配置。
## 7. 文档索引
- 详细文档:`docs/` 目录
- 测试示例:各模块的 `src/test/java/` 目录

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/`

View File

@@ -1,12 +1,13 @@
schema: spec-driven
context: |
- maven管理的java项目遵循java代码最佳实践规范
- 交流、文档、注释、提交信息使用中文,代码命名使用英文
- 新增代码要遵循原有代码的设计风格和模式,优先考虑复用已有组件、工具、依赖库
- **优先阅读README.md**README.md文档是项目的开发文档记录代码结构和关键开发模式优先读取获取上下文
- 涉及页面/路由/组件/功能模块变更或技术栈调整时同步更新README.md
- Git提交: 仅中文; 格式为"类型: 简短描述",类型可选: feat(新功能)/fix(修复)/refactor(重构)/docs(文档)/style(格式)/test(测试)/chore(构建/工具); 多行描述空行后加详细说明; 禁创建git操作task
- **优先阅读README.md**获取项目结构与开发规范所有代码风格、命名、注解、依赖、API等规范以README为准
- 新增代码优先复用已有组件、工具、依赖库,不引入新依赖
- 涉及模块结构、API、实体等变更时同步更新README.md
- Git提交: 仅中文; 格式"类型: 简短描述", 类型: feat/fix/refactor/docs/style/test/chore; 多行描述空行后写详细说明
- 禁止创建git操作task
- 积极使用subagents精心设计并行任务节省上下文空间加速任务执行
- 优先使用提问工具对用户进行提问
rules:
proposal:

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>

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,286 +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();
}
}
@Transactional(readOnly = true)
@Override
public Long count() {
return entityQuery.queryable(target).count();
}
@Transactional(readOnly = true)
@Override
public List<ENTITY> list() {
return entityQuery.queryable(target).toList();
}
@Transactional(readOnly = true)
@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) {
}
@Transactional(readOnly = true)
@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")
@Transactional(readOnly = true)
@Override
public ENTITY detail(Long id) {
return detailOptional(id).orElse(null);
}
@Named("detailOrThrow")
@Transactional(readOnly = true)
@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,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,212 +0,0 @@
package com.lanyuanxiaoyao.service.template.database.jpa.repository;
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.Skill;
import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.stereotype.Repository;
@SuppressWarnings("NullableProblems")
@Repository
public interface EmployeeRepository extends SimpleRepository<Employee> {
@EntityGraph(attributePaths = {"company"})
@Override
Optional<Employee> findOne(Specification<Employee> specification);
// ==================== 1. 基本字段查询 ====================
// 单字段精确匹配
List<Employee> findByName(String name);
Optional<Employee> findByCode(String code);
List<Employee> findByRole(Employee.Role role);
// 布尔值查询
List<Employee> findByActiveTrue();
List<Employee> findByActiveFalse();
long countByActiveTrue();
long countByActiveFalse();
// ==================== 2. 比较运算符查询 ====================
List<Employee> findByAgeGreaterThan(Integer age);
List<Employee> findByAgeLessThan(Integer age);
List<Employee> findByAgeGreaterThanEqual(Integer age);
List<Employee> findByAgeLessThanEqual(Integer age);
List<Employee> findByAgeBetween(Integer startAge, Integer endAge);
List<Employee> findBySalaryGreaterThan(BigDecimal salary);
List<Employee> findBySalaryLessThan(BigDecimal salary);
List<Employee> findBySalaryGreaterThanEqual(BigDecimal salary);
List<Employee> findBySalaryLessThanEqual(BigDecimal salary);
List<Employee> findBySalaryBetween(BigDecimal minSalary, BigDecimal maxSalary);
// ==================== 3. 字符串匹配查询 ====================
// 精确匹配
List<Employee> findByNameContaining(String name);
List<Employee> findByNameStartingWith(String prefix);
List<Employee> findByNameEndingWith(String suffix);
List<Employee> findByNameLike(String pattern);
// 忽略大小写
List<Employee> findByNameContainingIgnoreCase(String name);
List<Employee> findByNameStartingWithIgnoreCase(String prefix);
List<Employee> findByNameEndingWithIgnoreCase(String suffix);
List<Employee> findByNameIgnoreCase(String name);
// ==================== 4. NULL值和空值查询 ====================
List<Employee> findByBonusIsNull();
List<Employee> findByBonusIsNotNull();
List<Employee> findByResumeIsNull();
List<Employee> findByResumeIsNotNull();
List<Employee> findByHobbiesEmpty();
List<Employee> findByHobbiesIsNotEmpty();
List<Employee> findBySkillsEmpty();
List<Employee> findBySkillsIsNotEmpty();
// ==================== 5. 集合成员查询 (IN/NOT IN) ====================
List<Employee> findByRoleIn(Set<Employee.Role> roles);
List<Employee> findByRoleNotIn(Set<Employee.Role> roles);
List<Employee> findByNameIn(List<String> names);
List<Employee> findByNameNotIn(List<String> names);
List<Employee> findByAgeIn(List<Integer> ages);
List<Employee> findByAgeNotIn(List<Integer> ages);
// ==================== 6. 逻辑运算查询 (AND/OR/NOT) ====================
List<Employee> findByNameAndRole(String name, Employee.Role role);
List<Employee> findByAgeAndActive(Integer age, Boolean active);
List<Employee> findByNameAndSalaryGreaterThan(String name, BigDecimal salary);
List<Employee> findByRoleAndActiveAndAgeGreaterThan(Employee.Role role, Boolean active, Integer age);
List<Employee> findByNameOrRole(String name, Employee.Role role);
List<Employee> findByAgeLessThanOrSalaryGreaterThan(Integer age, BigDecimal salary);
List<Employee> findByNameOrCode(String name, String code);
List<Employee> findByRoleNot(Employee.Role role);
List<Employee> findByNameNot(String name);
// ==================== 7. 排序查询 ====================
List<Employee> findByActiveTrueOrderByAgeAsc();
List<Employee> findByActiveTrueOrderByAgeDesc();
List<Employee> findByActiveTrueOrderBySalaryDesc();
List<Employee> findByRoleOrderByAgeDescNameAsc(Employee.Role role);
// ==================== 8. 分页查询 ====================
Page<Employee> findByActiveTrue(Pageable pageable);
Page<Employee> findByRole(Employee.Role role, Pageable pageable);
Page<Employee> findBySalaryGreaterThan(BigDecimal salary, Pageable pageable);
// ==================== 9. 关联查询 (JOIN) ====================
List<Employee> findByCompany(Company company);
List<Employee> findByCompanyName(String companyName);
List<Employee> findByCompanyNameContaining(String companyName);
List<Employee> findByCompanyMembersGreaterThan(Integer members);
List<Employee> findBySkillsContaining(Skill skill);
List<Employee> findBySkillsName(String skillName);
List<Employee> findBySkillsNameContaining(String skillName);
List<Employee> findBySkillsNameIn(List<String> skillNames);
// ==================== 10. 嵌入式对象查询 ====================
List<Employee> findByAddressCity(String city);
List<Employee> findByAddressCityContaining(String city);
List<Employee> findByAddressState(String state);
List<Employee> findByAddressCountry(String country);
List<Employee> findByAddressCityAndAddressState(String city, String state);
// ==================== 11. 复合复杂查询 ====================
List<Employee> findByNameAndRoleAndAgeGreaterThan(String name, Employee.Role role, Integer age);
List<Employee> findByRoleAndActiveTrueAndSalaryGreaterThan(Employee.Role role, BigDecimal salary);
List<Employee> findByNameContainingIgnoreCaseAndActiveTrueAndAgeBetween(String name, Integer minAge, Integer maxAge);
List<Employee> findByCompanyNameContainingAndActiveTrue(String companyName);
List<Employee> findBySkillsNameContainingAndAgeGreaterThan(String skillName, Integer age);
// ==================== 12. DISTINCT 查询 ====================
List<Employee> findDistinctByRole(Employee.Role role);
// ==================== 13. TOP/LIMIT 查询 ====================
List<Employee> findTop5BySalaryGreaterThan(BigDecimal salary);
Optional<Employee> findFirstByRoleOrderBySalaryDesc(Employee.Role role);
// ==================== 14. LIKE 模式查询 ====================
List<Employee> findByNameLikeIgnoreCase(String pattern);
List<Employee> findByNameStartingWithAndRole(String prefix, Employee.Role role);
List<Employee> findByNameEndingWithAndAgeLessThan(String suffix, Integer age);
}

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,258 +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);
}
@Transactional(readOnly = true)
@Override
public Long count() {
return (long) mapper.countAll(target);
}
@Transactional(readOnly = true)
@Override
public List<ENTITY> list() {
return mapper.listAll(target);
}
@Transactional(readOnly = true)
@Override
public List<ENTITY> list(Set<Long> ids) {
return mapper.listByIds(target, ids);
}
protected void commonPredicates(Where where) {
}
@Transactional(readOnly = true)
@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")
@Transactional(readOnly = true)
@Override
public ENTITY detail(Long id) {
return detailOptional(id).orElse(null);
}
@Named("detailOrThrow")
@Transactional(readOnly = true)
@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

@@ -1,4 +1,4 @@
package com.lanyuanxiaoyao.service.template.database.jpa.configuration;
package com.lanyuanxiaoyao.service.template.database.configuration;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
@@ -11,4 +11,4 @@ public class QueryDSLConfiguration {
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;
@@ -356,4 +356,4 @@ public record GlobalResponse<T>(Integer status, String message, T data) {
*/
public record DetailItem<T>(T item) {
}
}
}

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.util.List;
@@ -28,4 +28,4 @@ import java.util.List;
* @param total 总记录数用于计算总页数和显示分页信息
*/
public record Page<ENTITY>(List<ENTITY> items, long total) {
}
}

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;
@@ -10,4 +10,4 @@ import org.hibernate.annotations.IdGeneratorType;
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.METHOD })
public @interface SnowflakeId {
}
}

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;
@@ -17,4 +17,4 @@ public class SnowflakeIdGenerator implements IdentifierGenerator {
throw new RuntimeException(e);
}
}
}
}

View File

@@ -1,7 +1,7 @@
package com.lanyuanxiaoyao.service.template.database.common.exception;
package com.lanyuanxiaoyao.service.template.database.exception;
public class IdNotFoundException extends RuntimeException {
public IdNotFoundException(Long id) {
super("ID为 %d 的资源不存在".formatted(id));
}
}
}

View File

@@ -1,7 +1,7 @@
package com.lanyuanxiaoyao.service.template.database.common.exception;
package com.lanyuanxiaoyao.service.template.database.exception;
public class NotCollectionException extends RuntimeException {
public NotCollectionException(String variable) {
super("变量 %s 不是集合".formatted(variable));
}
}
}

View File

@@ -1,7 +1,7 @@
package com.lanyuanxiaoyao.service.template.database.common.exception;
package com.lanyuanxiaoyao.service.template.database.exception;
public class NotComparableException extends RuntimeException {
public NotComparableException(String variable) {
super("变量 %s 不能比较".formatted(variable));
}
}
}

View File

@@ -1,7 +1,7 @@
package com.lanyuanxiaoyao.service.template.database.common.exception;
package com.lanyuanxiaoyao.service.template.database.exception;
public class NotStringException extends RuntimeException {
public NotStringException(String variable) {
super("变量 %s 不是字符串".formatted(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.common.helper;
package com.lanyuanxiaoyao.service.template.database.helper;
import java.time.Instant;
@@ -65,4 +65,4 @@ public class SnowflakeHelper {
private static long nowTimestamp() {
return Instant.now().toEpochMilli();
}
}
}

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;
@@ -125,4 +125,4 @@ public abstract class QueryParser<O> {
notBetween(queryable, container);
}
}
}
}

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;

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