diff --git a/README.md b/README.md index fe7fce7..9b0729d 100644 --- a/README.md +++ b/README.md @@ -31,83 +31,11 @@ com.lanyuanxiaoyao.service.template.{module}/ ## database 模块 -单表 CRUD → REST 接口的快速实现框架。基于 JPA + Fenix + QueryDSL + MapStruct。 +单表 CRUD → REST 接口快速实现框架。基于 JPA + Fenix + QueryDSL + MapStruct。 -### 实体继承 - -``` -IdOnlyEntity (id: Long, @SnowflakeId) ← @MappedSuperclass - ↑ -SimpleEntity (+ createdTime: LocalDateTime, ← @MappedSuperclass - modifiedTime: LocalDateTime) - ↑ -业务实体 (具体 @Entity) -``` - -- ID: Long,雪花算法,字段标 `@SnowflakeId`(自定义注解) -- 时间字段: `@CreatedDate`/`@LastModifiedDate`,JPA `AuditingEntityListener` 自动填充 -- 列注释: `@Column(comment = "...")` -- 物理命名: `PhysicalNamingStrategySnakeCaseImpl`(camelCase→snake_case) -- 保存: Fenix `saveOrUpdateByNotNullProperties()`,自动 INSERT/UPDATE,仅更新非 null 字段 - -### Repository - -```java -@NoRepositoryBean -public interface SimpleRepository extends - FenixJpaRepository, - FenixJpaSpecificationExecutor, - ListQueryByExampleExecutor, - ListQuerydslPredicateExecutor {} -``` - -业务 Repository 直接继承: `interface EmployeeRepository extends SimpleRepository {}` - -无 XML Mapper,无自定义 SQL,查询通过 JPA Criteria / Fenix / QueryDSL 动态构建。 - -### 接口组合模式 - -```java -SimpleService extends SaveService, QueryService, RemoveService -SimpleController extends SaveController, QueryController, RemoveController -``` - -抽象基类: `SimpleServiceSupport`(Service) / `SimpleControllerSupport`(Controller),用 `*Support` 后缀(非 `*Impl`)。 - -Controller 子类必须提供三个 mapper 函数: `saveItemMapper()`, `listItemMapper()`, `detailItemMapper()` - -### CRUD 五步 - -1. 创建实体 — 继承 `SimpleEntity` -2. 创建 Repository — 继承 `SimpleRepository` -3. 创建 Service — 继承 `SimpleServiceSupport` -4. 创建 DTO — 在 Controller 内定义 `SaveItem`/`ListItem`/`DetailItem` record -5. 创建 Controller — 继承 `SimpleControllerSupport`,实现 3 个 mapper - -也可用 `DatabaseHelper.generateBasicFiles()` 自动生成上述脚手架,`@RequestMapping` 路径由实体名 camelCase→snake_case 推导。`DatabaseHelper.generateDDL()` 可从实体类生成 DDL SQL。 - -### API 端点 - -| 操作 | 方法 | 路径 | -| --------- | ---- | ---------------- | -| 新增/更新 | POST | `/save` | -| 全部列表 | GET | `/list` | -| 条件列表 | POST | `/list` | -| 详情 | GET | `/detail/{id}` | -| 删除 | GET | `/remove/{id}` | - -统一响应: `GlobalResponse(status:Integer, message:String, data:T)`,成功 status=0,失败 status=500。 - -- 列表: `data = ListItem(items:Iterable, total:Long)` -- 详情: `data = DetailItem(item:T)` - -### 查询条件 - -查询对象: `Query(Queryable query, List sort, Pageable page)` - -支持: equal/notEqual/like/notLike/contain/notContain/startWith/endWith/great/less/greatEqual/lessEqual/between/notBetween/inside/notInside/nullEqual/notNullEqual/empty/notEmpty - -分页: `Pageable(index, size)`,index 从 1 开始,默认 `(1, 10)`,无排序默认 `createdTime DESC` +**文档**: +- [开发指南](docs/database-development.md) - 模块架构、核心设计、技术实现、扩展指南 +- [使用指南](docs/database-usage.md) - 快速开始、API 接口、查询条件、高级用法 ## 开发规范 diff --git a/docs/database-development.md b/docs/database-development.md new file mode 100644 index 0000000..c880380 --- /dev/null +++ b/docs/database-development.md @@ -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 extends + FenixJpaRepository, // CRUD + Fenix + FenixJpaSpecificationExecutor, // Specification + ListQueryByExampleExecutor, // Example + ListQuerydslPredicateExecutor {} // QueryDSL +``` + +**核心能力**: +- `saveOrUpdateByNotNullProperties()` - 部分字段更新 +- Specification - 动态条件查询 +- QueryDSL - 类型安全查询 + +### Service 接口组合 + +```java +SaveService // save(entity), save(entities) +QueryService // detail(id), list(), list(query), count() +RemoveService // remove(id), remove(ids) + +SimpleService extends SaveService, QueryService, RemoveService +``` + +**SimpleServiceSupport 实现**: +- 保存:Fenix `saveOrUpdateByNotNullProperties()` +- 查询:JPA Criteria + Specification +- 删除:`deleteBatchByIds()` +- 扩展点:重写 `commonPredicates()` 添加全局过滤条件 + +### Controller 接口组合 + +```java +SaveController // POST /save +QueryController // GET/POST /list, GET /detail/{id} +RemoveController // GET /remove/{id} + +SimpleController +``` + +**SimpleControllerSupport 实现**: +- 调用 Service 方法 +- 通过 Mapper 转换 DTO ↔ Entity +- 封装 GlobalResponse +- 扩展点:实现 `saveItemMapper()`, `listItemMapper()`, `detailItemMapper()` + +### 查询条件 + +**Query 结构**:`Query(query: Queryable, sort: List, 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(status, message, data) +// status: 0 成功, 500 失败 +// 列表: data = ListItem(items, total) +// 详情: data = DetailItem(item) +``` + +## 技术细节 + +### 雪花算法 + +`SnowflakeHelper`:64 位 Long,1 位符号 + 41 位时间戳 + 10 位机器 ID + 12 位序列号 + +`SnowflakeIdGenerator`:实现 `IdentifierGenerator`,持久化时调用 `SnowflakeHelper.next()` + +### 部分更新 + +Fenix `saveOrUpdateByNotNullProperties()`: +- 自动判断 INSERT/UPDATE +- 仅更新非 null 字段 + +### 命名策略 + +`PhysicalNamingStrategySnakeCaseImpl`:camelCase → snake_case + +### 注解处理器 + +执行顺序:lombok → hibernate-jpamodelgen → querydsl-apt → mapstruct-processor + +生成:getter/setter、JPA 元模型(_Entity)、QueryDSL Q 类(QEntity)、MapStruct Mapper + +## 工具类 + +### DatabaseHelper + +```java +// 生成 DDL +generateDDL(entityPackages, ddlFilePath, dialect, jdbc, username, password, driver) + +// 生成脚手架 +generateBasicFiles(entityPackages, projectRootPackage, projectRootPath, override) +``` + +### SnowflakeHelper + +```java +Long id = SnowflakeHelper.next(); +``` + +## 扩展指南 + +### 软删除 + +1. 创建 `SoftDeleteEntity extends SimpleEntity`,添加 `deleted` 字段 +2. 重写 `commonPredicates()` 返回 `deleted = false` 条件 +3. 覆盖 `remove()` 改为更新 `deleted` 字段 + +### 多租户 + +1. 创建 `TenantEntity extends SimpleEntity`,添加 `tenantId` 字段 +2. 重写 `commonPredicates()` 添加租户过滤 +3. ThreadLocal 或 Spring Security 存储当前租户 + +### 审计字段 + +在 `SimpleEntity` 添加 `createdBy`, `modifiedBy`,配合 Spring Security 获取当前用户 + +### 自定义查询操作符 + +1. `Query.Queryable` 添加字段 +2. `QueryParser` 添加抽象方法 +3. `JpaQueryParser` 实现转换为 Predicate + +## 测试 + +- H2 内存数据库 +- `@DataJpaTest` 测试 Repository +- `@WebMvcTest` 测试 Controller +- 测试用例:`src/test/java/.../integration/` + +## 依赖 + +核心:spring-boot-starter-data-jpa, fenix-spring-boot-starter:4.0.0, querydsl-jpa:7.1, mapstruct:1.6.3 + +传递:spring-boot-service-template-common + +## 注意事项 + +- 事务:写操作 `@Transactional(rollbackFor = Throwable.class)`,读操作 `@Transactional(readOnly = true)` +- 异常:不使用全局处理器,直接抛出 +- 性能:批量操作使用批量方法 +- 线程安全:GlobalResponse 用 record 保证不可变,SnowflakeHelper 用原子变量 \ No newline at end of file diff --git a/docs/database-usage.md b/docs/database-usage.md new file mode 100644 index 0000000..eb8d3a9 --- /dev/null +++ b/docs/database-usage.md @@ -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 list = service.list(); + } +} +``` + +适用:批处理、定时任务、数据迁移、命令行应用、后台服务。 + +## 快速开始 + +### 1. 添加依赖 + +```xml + + com.lanyuanxiaoyao + spring-boot-service-template-database + +``` + +### 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 {} +``` + +继承能力:CRUD、分页、Specification、QueryDSL、Example。 + +### 4. 创建 Service + +```java +@Slf4j +@Service +public class EmployeeService extends SimpleServiceSupport { + public EmployeeService(EmployeeRepository repository) { + super(repository); + } +} +``` + +自动获得完整 CRUD 能力。 + +### 5. 创建 Controller(Web 应用) + +```java +@Slf4j +@RestController +@RequestMapping("employee") +public class EmployeeController + extends SimpleControllerSupport { + + private final EmployeeService service; + + public EmployeeController(EmployeeService service) { + super(service); + this.service = service; + } + + @Override + protected Function 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 listItemMapper() { + return entity -> new ListItem( + entity.getId(), entity.getName(), entity.getDepartmentId(), + entity.getEmail(), entity.getCreatedTime() + ); + } + + @Override + protected Function 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, // 排序 + 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 { + private final EmployeeRepository repository; + + public EmployeeService(EmployeeRepository repository) { + super(repository); + this.repository = repository; + } + + // 自定义方法 + public List findByDepartmentId(Long departmentId) { + return repository.findAll( + (root, query, builder) -> builder.equal(root.get("departmentId"), departmentId) + ); + } + + // 全局过滤条件 + @Override + protected Predicate commonPredicates(Root root, CriteriaQuery query, CriteriaBuilder builder) { + return builder.equal(root.get("deleted"), false); // 软删除过滤 + } +} +``` + +### QueryDSL 查询 + +```java +public List 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 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 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 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/` \ No newline at end of file