2.5 完整 CRUD 示例
在一个已经由 Spring Boot Parent 或 BOM 管理版本的 Web 工程中,搭一个可直接运行的 CRUD 应用。 核心:一个 Controller 直接用
DB.xx,不需要 Mapper / DAO。Service 层可选(按业务需要)。开发原则:首选
DB.pojo(实体操作),DB.jdbc(原生 SQL)尽量少用或不用。 绝大多数 CRUD 用DB.pojo链式即可;只有极复杂 SQL(多表 JOIN、窗口函数、动态列)才考虑DB.jdbc兜底。
一、目录结构
src/main/
├── java/com/example/demo/
│ ├── DemoApplication.java # 启动类
│ ├── entity/
│ │ └── User.java # 实体类(自动建表依据)
│ ├── service/
│ │ └── UserService.java # 直接 DB.xx(可选,推荐)
│ └── controller/
│ └── UserController.java # 直接用 DB.xx 写业务(可选,不推荐)
└── resources/
└── application.yml # spring.datasource + dlz.db 配置
二、Maven 依赖
版本兼容:
dlz-db-spring-boot-starter:8.0.0支持本仓库验证的 Spring Boot 2/3 集成方式。Boot 2 使用 Java 8+;Boot 3 使用 Java 17+。下面只列<dependencies>,Spring Boot 版本应由项目 Parent 或 BOM 统一管理。
<!-- Spring Boot Web(构建 Web 应用必须) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 必须显式引入:DLZ-DB Starter 中该依赖是 provided,不会传递给应用 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- DLZ-DB Spring Boot Starter -->
<dependency>
<groupId>top.dlzio</groupId>
<artifactId>dlz-db-spring-boot-starter</artifactId>
<version>8.0.0</version>
</dependency>
<!-- 数据库驱动(按需引入:sqlite / mysql / pg) -->
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.45.1.0</version>
</dependency>
<!-- Lombok(简化实体类代码,可选但推荐) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
注意:
- 如果是纯后台服务(不需要 Web),可以不加
spring-boot-starter-web,但仍需提供 Spring Boot 自动配置环境和spring-boot-starter-jdbc。- Lombok 是可选的,不加的话实体类需要手动写 getter/setter。
- MySQL / PostgreSQL 驱动需另外引入,见下方"切换数据库"部分。
三、配置 application.yml
数据源就是 Spring 的 spring.datasource 配置。切换数据库只改这里。
默认使用 Spring Boot 自带的 HikariCP 连接池,可通过 spring.datasource.hikari.* 调整连接池参数。
# SQLite(默认,零配置即可运行)
spring:
datasource:
driver-class-name: org.sqlite.JDBC
url: jdbc:sqlite:./data/demo.sqlite3
# 连接池配置(可选,HikariCP 参数)
hikari:
maximum-pool-size: 10 # 最大连接数
minimum-idle: 2 # 最小空闲连接
connection-timeout: 30000 # 连接超时(毫秒)
# MySQL(切库只改这段)
# spring:
# datasource:
# driver-class-name: com.mysql.cj.jdbc.Driver
# url: jdbc:mysql://localhost:3306/demo?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8
# username: root
# password: 123456
# PostgreSQL(切库只改这段)
# spring:
# datasource:
# driver-class-name: org.postgresql.Driver
# url: jdbc:postgresql://localhost:5432/demo
# username: postgres
# password: postgres
dlz:
db:
# 逻辑删除字段(目标数据表含对应列时自动启用)
logic-delete-field: deleted
helper:
# 扫描哪些包下的实体,自动建表/同步字段
package-name: com.example.demo.entity
# 是否自动建表/加字段。开发环境 true;生产建议 false(避免误改表结构)
auto-update: true
log:
show-run-sql: true # 打印执行的 SQL
show-caller: true # 将 SQL 调用位置写入 MDC;需由日志 pattern 显示
show-result: false # 打印查询结果(调试时开启,数据量大时建议关闭,避免日志爆炸)
# 是否从数据库加载预设 SQL(配合 DB.sql 使用,详见"预设SQL"章节)
use-db-sql: false
# 预设 SQL 的扫描路径(classpath 下的 sql 目录)
sqllist:
- app/*
- demo/*
四、启动类
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
五、实体类
package com.example.demo.entity;
import com.dlz.db.core.anno.IdType;
import com.dlz.db.core.anno.TableField;
import com.dlz.db.core.anno.TableId;
import com.dlz.db.core.anno.TableName;
import lombok.Data;
import java.util.Date;
@Data
@TableName("user") // 指定表名(不写则驼峰转下划线 user)
public class User {
@TableId(type = IdType.AUTO) // 数据库自增主键
private Long id;
private String name;
@TableField(comment = "年龄")
private Integer age;
private String email;
private Integer deleted; // 映射逻辑删除列,也用于 Pojo 批量插入时回填默认值
private Date createTime;
}
实体字段类型会映射为数据库列类型(Long→BIGINT、String→VARCHAR、Integer→INT、Date→DATETIME 等)。@TableField(comment = "...") 只会在支持列注释且对应方言实现了该能力时生成 DDL COMMENT;本例默认使用的 SQLite 不支持列注释。
主键生成策略
@TableId 标记主键字段,策略由 type 明确决定:
IdType | 说明 |
|---|---|
SEQ | 默认值,使用 DLZ-DB 内部号段生成器 |
AUTO | 使用数据库自增主键并回填 |
INPUT | 由应用提供 ID,为 null 时报错 |
ASSIGN_ID | 框架分配雪花 ID |
ASSIGN_UUID | 框架分配无连字符 UUID |
注意:
DB.pojo.insert(entity)会将框架分配或数据库生成的主键回填到 entity 对象中。数据库自增示例必须显式写IdType.AUTO。
逻辑删除字段
目标数据库表中存在名为 deleted(或通过 logic-delete-field 自定义的列名)的列时,DLZ-DB 自动启用逻辑删除:
- 查询:自动加
WHERE deleted = 0条件 - 删除:自动改为
UPDATE SET deleted = 1,不是真的 DELETE - 物理删除:Pojo Delete Wrapper 使用
.physical();按单个主键直接删除可调用deleteById(..., DeleteOption.PHYSICAL)
单条插入会根据目标表补入
deleted = 0,无需业务代码手动设置。Pojo 批量插入若要把默认值同步回填到对象,还需要 Bean 中存在对应映射字段。
时间字段
createTime、updateTime 等时间字段不会自动填充,需要在业务代码中手动 set:
user.setCreateTime(new Date());
DB.pojo.insert(user);
六、自动建表 & 同步数据库(重点)
只需定义实体 + 配置扫描路径,启动时自动建表;表已存在但缺字段时自动加列。
原理:
- 启动时
DlzDbAdapter读取dlz.db.helper配置。 HelperScan.scan(package-name)扫描该包下所有带@TableName的类。- 表不存在 →
createTable(按实体字段建表)。 - 表存在但缺某字段 →
createColumn(自动加列)。
dlz:
db:
helper:
package-name: com.example.demo.entity # 扫这里
auto-update: true # 开自动建表/加列
实体
User定义在com.example.demo.entity下,配好上面后无需手动执行建表 SQL,启动即自动创建user表;后续给实体加字段,重启自动ALTER TABLE加列。 注意:auto-update: true只适合开发/测试;生产建议关闭,避免误改线上表结构。
七、Service 层(可选)
简单的 CRUD 可以直接在 Controller 里写 DB.xx,不需要 Service 层。复杂业务按需加 Service:
package com.example.demo.service.impl;
import com.dlz.db.DB;
import com.example.demo.entity.User;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
public User getById(Long id) {
return DB.pojo.selectWrapper(User.class).eq(User::getId, id).queryBean();
}
public List<User> listByName(String name) {
return DB.pojo.selectWrapper(User.class)
.like(name != null && !name.isEmpty(), User::getName, name)
.queryBeanList();
}
public User create(User user) {
DB.pojo.insert(user); // 自动回填主键
return user;
}
public int update(User user) {
return DB.pojo.updateWrapper(user).eq(User::getId, user.getId()).execute();
}
public int delete(Long id) {
return DB.pojo.deleteWrapper(User.class).eq(User::getId, id).execute();
}
}
Service 里同样直接
DB.xx,不写 Mapper / DAO。事务方法加@Transactional即可。
八、Controller —— 业务简单无复用事务需求情况直接用 DB.xx,不需要 DAO
package com.example.demo.controller;
import com.dlz.db.DB;
import com.dlz.db.model.Page;
import com.example.demo.entity.User;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
import java.util.List;
@RestController
@RequestMapping("/user")
public class UserController {
// 单条查询
@GetMapping("/{id}")
public User get(@PathVariable Long id) {
return DB.pojo.selectById(User.class, id);
}
// 列表查询(条件可选)
@GetMapping
public List<User> list(@RequestParam(required = false) String name) {
return DB.pojo.selectWrapper(User.class)
.like(name != null && !name.isEmpty(), User::getName, name)
.orderByDesc(User::getCreateTime)
.queryBeanList();
}
// 分页
@GetMapping("/page")
public Page<User> page(@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "10") int pageSize) {
return DB.pojo.selectWrapper(User.class)
.page(Page.build(pageNum, pageSize))
.queryBeanPage();
}
// 新增(自动回填主键)
@PostMapping
public User create(@RequestBody User user) {
user.setCreateTime(new Date());
DB.pojo.insert(user);
return user;
}
// 更新
@PutMapping("/{id}")
public int update(@PathVariable Long id, @RequestBody User user) {
user.setId(id);
return DB.pojo.updateById(user);
}
// 删除
@DeleteMapping("/{id}")
public int delete(@PathVariable Long id) {
return DB.pojo.deleteById(User.class, id);
}
}
分页返回结构
Page<T> 包含以下字段:
| 字段 | 类型 | 说明 |
|---|---|---|
current | long | 当前页码(从 1 开始) |
size | long | 每页大小 |
total | long | 总记录数 |
pages | long | 总页数 |
records | List<T> | 当前页数据列表 |
返回 JSON 示例:
{
"current": 1,
"size": 10,
"total": 100,
"pages": 10,
"records": [{"id": 1, "name": "张三"}]
}
更新时 null 字段的处理
DB.pojo.updateWrapper(entity) 默认会将实体中所有非 null 字段拼入 UPDATE 的 SET 子句。null 值的字段不会被更新(保持数据库原值)。
User user = new User();
user.setId(1L);
user.setName("新名字");
user.setAge(null); // age 为 null,不会被更新
// 实际执行:UPDATE user SET name = ? WHERE id = ?
// age 字段保持不变
DB.pojo.updateWrapper(user).eq(User::getId, 1L).execute();
如果只需要把某个字段置为 null,可使用
updateWrapper(User.class).set(User::getAge, null);按实体单条更新可调用DB.pojo.updateById(user, UpdateOption.INCLUDE_NULL)。
九、原生 SQL(DB.jdbc)—— 兜底用,少用
DB.pojo 能解决的都优先用 DB.pojo。 只有遇到 DB.pojo 难以表达的复杂 SQL(多表 JOIN、窗口函数、动态列、复杂聚合)才用 DB.jdbc 兜底:
// 复杂查询返回 ResultMap(Map 结构)
List<ResultMap> rows = DB.jdbc.selectWrapper(
"SELECT u.name, COUNT(o.id) AS cnt FROM user u " +
"LEFT JOIN orders o ON o.user_id = u.id GROUP BY u.id").queryList();
// JDBC 写操作直接调用 execute(...),调用时立即执行
DB.jdbc.execute("UPDATE user SET age = ? WHERE id = ?", 26, 1);
// 统计
long count = DB.jdbc.count("SELECT * FROM user WHERE deleted = 0");
提醒:能用
DB.pojo表达的单表 CRUD、分页、条件查询,不要用DB.jdbc(实体操作有类型安全、逻辑删除自动过滤、驼峰映射等优势)。
十、事务
// 编程式事务
DB.tx.run(() -> {
DB.pojo.insert(order);
DB.pojo.insert(orderItem);
});
// 声明式:Spring Boot 直接用 @Transactional
@Transactional
public void createOrder(Order order, List<OrderItem> items) {
DB.pojo.insert(order);
DB.batch.insert(items);
}
十一、扩展成"完整应用"
启动类 + Controller 就是一个完整可运行的 Spring Boot 应用。在这个基础上:
- 加 REST API / 认证(Spring Security / 简单拦截器)
- 加 UI(
src/main/resources/static/放前端页面) - 加 WebSocket(实时推送)
即可扩展成 console 控制台,数据访问始终 DB.xx,无需 DAO / Service / Mapper。
十二、快速验证
启动成功后,可以用下面的命令快速验证接口是否正常:
# 1. 新增一个用户
curl -X POST http://localhost:8080/user \
-H "Content-Type: application/json" \
-d '{"name":"张三","age":25,"email":"zhangsan@example.com","deleted":0}'
# 2. 查询用户列表
curl http://localhost:8080/user
# 3. 分页查询
curl "http://localhost:8080/user/page?pageNum=1&pageSize=10"
# 4. 根据 ID 查询(把 1 换成上一步返回的真实 ID)
curl http://localhost:8080/user/1
# 5. 更新用户
curl -X PUT http://localhost:8080/user/1 \
-H "Content-Type: application/json" \
-d '{"name":"张三丰","age":26}'
# 6. 删除用户(逻辑删除)
curl -X DELETE http://localhost:8080/user/1
同时观察控制台日志:show-run-sql: true 会输出执行 SQL;show-caller: true 会把调用位置写入 MDC,日志 pattern 包含相应 MDC 字段时才会显示。
十三、常见问题 FAQ
Q1:启动时报 NoClassDefFoundError 或 ClassNotFoundException
检查依赖是否完整:
- 确认
dlz-db-spring-boot-starter版本正确 - 确认已显式引入
spring-boot-starter-jdbc;DLZ-DB Starter 将它声明为provided,不会传递给应用 - 确认数据库驱动已引入(SQLite/MySQL/PostgreSQL 至少一个)
- 用
mvn dependency:tree检查依赖冲突
Q2:表没有自动创建
排查步骤:
- 确认
dlz.db.helper.auto-update: true - 确认
package-name路径正确,是实体类所在的包 - 确认实体类有
@TableName注解 - 查看启动日志中是否有建表 SQL 输出
- 确认数据库连接正常,用户有 CREATE TABLE 权限
Q3:查询不到数据(表里明明有数据)
大概率是逻辑删除的问题:
- 目标数据表有
deleted列时,查询自动加WHERE deleted = 0 - 检查数据的
deleted字段值是否为 0 - 如果是历史表没有
deleted字段,应补齐表结构,或为该项目配置正确的logic-delete-field;查询 Wrapper 不提供ignoreLogicDelete(true)
Q4:插入数据后主键没有回填
- 确认主键字段加了
@TableId(type = IdType.AUTO);@TableId的默认策略是SEQ,不是数据库自增 - 确认数据库支持自增(MySQL 的 AUTO_INCREMENT、SQLite 的 INTEGER PRIMARY KEY、PostgreSQL 的 SERIAL)
- SQLite 注意:主键类型必须是
INTEGER(对应 JavaLong/Integer)
Q5:DB.pojo.insert(user) 报错 "table not found"
- 检查
dlz.db.helper.package-name是否扫描到了该实体类 - 检查实体类是否有
@TableName注解 - 确认
auto-update: true是否开启 - 如果是手动建的表,检查表名和实体名是否匹配(驼峰转下划线)
Q6:更新时想把字段设为 null 怎么办
updateById(entity) 默认跳过实体中的 null 字段。只更新一个字段时,可在 Wrapper 上显式设置 null:
DB.pojo.updateWrapper(User.class)
.set(User::getEmail, null)
.eq(User::getId, 1L)
.execute();
DB.batch.update(...) 不接收 DbOption,而是始终按全字段更新,null 会覆盖数据库值,且不会自动追加逻辑删除过滤。它只适合带真实主键的完整行快照;部分字段批量修改应改用 Wrapper 或显式批量 JDBC,详见 3.6 批量操作。
Q7:@ConfigurationPropertiesScan 可以不加吗
可以不加。dlz.db.* 配置由 starter 自动装配类里的 @Bean @ConfigurationProperties(prefix = "dlz.db") 完成绑定(详见 SpringDlzDbAutoConfiguration),应用启动类只需要 @SpringBootApplication,无需额外注解。
注意:如果没引入 starter 而自己手写配置类,才需要自行处理配置绑定。
继续阅读
- 常规查询和写入:3.1 基础 CRUD
- 动态查询:3.2 条件构造器
- 分页和结果类型:3.3 分页排序与结果映射
- 逻辑删除:3.5 逻辑删除
- Spring 配置和事务:4.1 Spring Boot 完整集成