写了 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.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-starter or top.dlzio:dlz-db-solon-plugin
  • GitHub:https://github.com/dingkui/dlz-db
  • Gitee:https://gitee.com/dlzio/dlz-db
相关推荐
郭小李19 小时前
mybatis 动态 sql
java·sql·mybatis
知守观19 小时前
技术考古:iBATIS 2.3.4 时代的持久层,是如何用 Map 玩转 307 个存储过程的?
java·后端·mybatis
captain37620 小时前
MyBatis Plus(1)
java·服务器·mybatis
代码不停1 天前
MyBatis进阶
spring boot·mybatis
captain3761 天前
MyBatis(入门5)其他查询操作
java·数据库·spring boot·mybatis
captain3762 天前
MyBatis(进阶1)
mybatis
郭小李2 天前
mybatis的 Mapper.xml语法
xml·java·mybatis
m0_639906942 天前
MyBatis 实现分页查询|逻辑分页、物理分页、PageHelper 原理
mybatis
captain3763 天前
MyBatis操作数据库(入门2)
java·数据库·spring boot·后端·spring·mybatis
正儿八经的少年4 天前
MyBatis 一对一、一对多 SQL
数据库·sql·mybatis