写 Java 20 年,MyBatis 用了不下 10 年。它是个好框架,但有些痛点是真磨人。
瞬间一:SQL 日志搜半小时找不到谁执行的
生产上一条慢 SQL:
vbnet
SELECT * FROM sys_menu WHERE ... AND ... ORDER BY ...
全局搜关键字,一堆类似的 SQL,哪个是罪魁祸首?MyBatis 的日志只告诉你"有一条 SQL 跑了",不告诉你谁跑的。
DLZ-DB 开了 show-caller: true 后:
sql
caller:(MenuController.java:42) 1250ms sql:SELECT * FROM sys_menu t where status = 1 and ...
你在 IDE 里直接点 MenuController.java:42 就能跳过去,日志中的sql 参数已经自动填充好,拷贝就可以直接执行。生产排查时,这一条就值回票价。
源码实际就是通过 DbLogUtil.getTraceCaller(level) 遍历调用栈,跳过框架层,找到业务代码位置。
瞬间二:为一次简单 CRUD,建 6 个文件 200 行代码
新增一个 sys_config 表,有 id、name、value 三个字段。按 MyBatis-Plus 的标准流程:
- 建 Entity 类、Mapper 接口(继承 BaseMapper)、Service 接口、ServiceImpl 实现、Controller、还可能来个 DTO/VO...
- 写完感觉像做了个微型项目。
DLZ-DB 怎么做?
java
@Data
@TableName("sys_config")
public class SysConfig {
private Long id;
private String name;
private String value;
}
// Controller 里一行搞定
@GetMapping("/config/{id}")
public SysConfig get(@PathVariable Long id) {
return DB.Pojo.selectById(SysConfig.class, id);
}
没 Mapper、没 Service、没 XML。DB.Pojo 六个入口之一,选它就行。
瞬间三:@DS("slave") 写死了,新租户动态接入无解
SaaS 系统要给新租户动态配数据源。注解 @DS("tenant_xxx") 是编译期硬编码的,怎么搞?
SpEL 表达式、AOP 切面、ThreadLocal push/pop......每一步都在和框架较劲。
DLZ-DB 就是字符串:
java
// 运行时注册新数据源
DataSourceProperty prop = new DataSourceProperty();
prop.setName("tenant_001");
prop.setUrl("jdbc:mysql://10.0.0.5:3306/db_tenant001");
prop.setUsername("root");
prop.setPassword("pwd123");
prop.setDriverClassName("com.mysql.cj.jdbc.Driver");
DB.Dynamic.setDataSource(prop);
// 任意逻辑决定走哪个库
String dsName = routeByTenant(tenantId);
List<Order> orders = DB.Dynamic.use(dsName, () ->
DB.Pojo.select(Order.class).eq(Order::getStatus, 1).queryBeanList()
);
瞬间四:JSON 字段要自己 parseObject 一层层剥
数据库有个 profile 字段存 JSON:
json
{"address":{"city":"杭州","street":"西湖区"}}
MyBatis 查出来是 String,你得:
java
JSONObject profile = JSON.parseObject(user.getProfile());
JSONObject address = profile.getJSONObject("address");
String city = address.getString("city");
DLZ-DB 的 ResultMap 继承自 JSONMap,路径取值是原生能力:
java
ResultMap result = DB.Table.select("user").eq("id", 1).queryOne();
result.getStr("profile.address.city", "未知"); // → "杭州"
不需要任何 JSON 解析代码。
7000 行代码,靠什么保证质量?
功能少不代表质量随便。DLZ-DB 有两个数字能说明问题:
100% 测试通过率。 38 个测试文件覆盖了核心模块的全部功能------条件构造器的 20+ 种操作符、AND/OR 嵌套的各种组合、数据源切换与事务回滚的集成测试、批量操作的边界情况。每个 PR 跑 CI,全绿才合并。
45% JaCoCo 行覆盖率。 在开源项目里不算高,但这里有一个背景:框架里大量的 catch 分支、异常处理路径、数据库交互层在单元测试中天然难以覆盖(需要真实数据库连接)。实际跑集成测试时,核心路径的覆盖率远高于这个数字。
换句话说:能测的都测了,测不到的不是没测,是留给集成环境了。 对于一个 7000 行的轻量框架来说,这个质量底子足够硬。
没有 Service 层,代码会不会失控?
有人说:"Controller 里直接写查询,不就成了大泥球?"
这是对的------如果滥用的话。关键不是"要不要 Service",而是什么时候要。
直接写 Controller 的场景
简单的、一次性的、无业务逻辑的查询,直接写在 Controller 里反而更清晰:
java
@RestController
public class DictController {
// 字典查询,纯查,无任何业务逻辑
@GetMapping("/dicts")
public List<Dict> list(@RequestParam String type) {
return DB.Pojo.select(Dict.class)
.eq(Dict::getType, type)
.orderByAsc(Dict::getSort)
.queryBeanList();
}
}
这种场景强行套一层 Service + ServiceImpl,只会凭空多两个文件,每个文件里就一行调用。不写 Service 不是因为懒,是因为没必要。
需要 Service 的场景
当出现以下任一情况时,抽 Service:
- 有事务:一次请求涉及多次数据库写操作
- 有业务校验:插入前要检查数据合法性、权限校验
- 跨模块复用:同一个查询逻辑被多个 Controller 调用
- 需要组合多个 DB 操作:查 A 表 → 算个值 → 写 B 表
java
@Service
public class OrderService {
@Transactional
public void createOrder(Order order, List<OrderItem> items) {
// 1. 校验库存
checkStock(items);
// 2. 写订单
DB.Pojo.insert(order);
// 3. 写订单明细
DB.Batch.insert(items);
// 4. 扣库存
items.forEach(this::deductStock);
}
// 这个查询被多个地方复用,抽成 Service 方法
public List<Order> findUserOrders(Long userId) {
return DB.Pojo.select(Order.class)
.eq(Order::getUserId, userId)
.orderByDesc(Order::getCreateTime)
.queryBeanList();
}
}
一句话原则
有业务逻辑 → Service;纯查纯写 → Controller 直接来。
DLZ-DB 不强制你写 Service,但它也不阻止你写。把选择权还给开发者------让简单的事情简单做,复杂的事情规范做。
坦诚聊聊 DLZ-DB 的不足
没有完美的框架,DLZ-DB 也有它不适合的场景:
1. 复杂关联查询能力弱
多表 JOIN、子查询、GROUP BY + HAVING 这些场景,DLZ-DB 的 Lambda 条件构造器不直接支持。虽然你可以用 .sql() 注入原生 SQL 片段:
java
DB.Pojo.select(User.class)
.sql("EXISTS (SELECT 1 FROM vip WHERE user_id=t.id AND level>=#{lv})",new JSONMap("lv", 3))
.queryBeanList();
但复杂到一定程度,还是得回到 DB.Jdbc 或 DB.Sql 手写 SQL。如果你的项目 80% 都是复杂报表和连表查询,MyBatis 的 XML 管理可能更合适。
2. 没有 ORM 级联操作
DLZ-DB 不是 ORM,它不管理实体间关系。没有 @OneToMany、@ManyToOne、级联保存、懒加载这些 JPA 概念。你得自己查两次:
java
// 查订单
Order order = DB.Pojo.selectById(Order.class, orderId);
// 自己查明细
List<OrderItem> items = DB.Pojo.select(OrderItem.class)
.eq(OrderItem::getOrderId, orderId)
.queryBeanList();
这对于习惯了 Hibernate 自动级联的开发者来说,会觉得"退步"了。但对不想要 ORM 黑魔法的人来说,这正是他们要的------显式 > 魔法。
3. 生态和社区尚小
MyBatis-Plus 有 16k+ Star、丰富的插件生态(代码生成器、分页插件、防全表更新插件......)。DLZ-DB 刚开源不到一年,社区规模、第三方教程、Stack Overflow 问答都还是起步阶段。遇到问题,你可能需要自己跟源码。
反过来看,这也和它的设计一致:核心不到 7000 行,你确实能自己看懂。
4. 不适合"注解信仰者"
如果你习惯了 @Select、@Modifying、@Query 这种声明式风格,或者你的团队规范要求 Mapper + Service 的分层架构,DLZ-DB 的链式 API 风格需要一个适应过程。它不是 Spring Data JPA 那种"写个接口方法名就自动生成 SQL"的魔幻体验。
5. 预设 SQL 的 IDE 支持有限
DB.Sql.select("key.user.find") 这句里的 "key.user.find" 是字符串,重构时 IDE 不会自动帮你改名。XML 里定义的 SQL key 和代码里的引用是分离的。虽然配置文件路径有约定,但不如 MyBatis 的 Mapper 接口 + XML 一一对应那么直观。
总结
我写 DLZ-DB 不是为了再造轮子,而是因为"框架总长在你不想要它长的地方"。
它不是银弹。适合的场景 :中小项目、快速迭代、SaaS 多租户、AI 辅助编程、想减少样板代码的团队。不适合的场景:复杂报表系统、重度 ORM 依赖、团队规范强制 Mapper/Service 分层架构。
后面我还会继续写一些 DLZ-DB 设计背后的思考,包括:
- 《为什么 AI 更容易写对 DLZ-DB,而不是 MyBatis》
- 《Java 为什么天然讨厌数据库中的 JSON 字段?》
- 《很多 Java CRUD,其实是在给框架打工》
- 《AI 正在倒逼 Java 框架重新设计》
- 《生产慢 SQL,为什么总定位不到是谁执行的?》
如果这些问题你也踩过坑,欢迎先点个收藏,后面慢慢聊。
代码示例均来自 DLZ-DB 源码和测试用例:DbPojoTest.java、DbLogUtil.java、DBDynamic.java、DynamicAndTxTest.java。
- 项目:
dlz-db - Maven:
top.dlzio:dlz-db-spring-boot-starterortop.dlzio:dlz-db-solon-plugin - GitHub:
https://github.com/dingkui/dlz-db - Gitee:
https://gitee.com/dlzio/dlz-db