写了 20 年 Java,我受够了 MyBatis 的 4 个瞬间

写 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:

  1. 有事务:一次请求涉及多次数据库写操作
  2. 有业务校验:插入前要检查数据合法性、权限校验
  3. 跨模块复用:同一个查询逻辑被多个 Controller 调用
  4. 需要组合多个 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.JdbcDB.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.javaDbLogUtil.javaDBDynamic.javaDynamicAndTxTest.java

  • 项目:dlz-db
  • Maven:top.dlzio:dlz-db-spring-boot-starter or top.dlzio:dlz-db-solon-plugin
  • GitHub:https://github.com/dingkui/dlz-db
  • Gitee:https://gitee.com/dlzio/dlz-db
相关推荐
斯特凡今天也很帅11 小时前
新建数据源报错No bean named ‘SqlSessionFactorykf‘ available
java·数据库·spring boot·mybatis
未若君雅裁1 天前
MyBatis 一级缓存、二级缓存与清理机制
java·缓存·mybatis
Simon523141 天前
MyBatis三大核心文件:Entity、DAO、Mapper
mybatis
MandalaO_O1 天前
MyBatis:核心概念 + 环境搭建 + CRUD
java·tomcat·mybatis
XS0301061 天前
MyBatis基础实战笔记一
笔记·mybatis
噢,我明白了1 天前
MyBatis-Plus的引入和配置
java·tomcat·mybatis
霸道流氓气质2 天前
Spring Boot + MyBatis-Plus 实现异常隔离的 Upsert 数据落库(含远程调用数据补全)
spring boot·后端·mybatis
Devin~Y2 天前
大厂Java面试实战:Spring Boot微服务、Redis缓存、Kafka消息队列与Spring AI RAG
java·spring boot·redis·kafka·mybatis·spring mvc·hikaricp
Don.TIk2 天前
ChapterOne-搭建项目骨架
java·spring·spring cloud·mybatis