不用手写查询语句:Spring Data Mongo与MyBatis选型对比和避坑

设备台账这类文档型业务落到 MongoDB 上,Java 侧最常见的两个持久层选择是 Spring Data MongoDB Repository 和 MyBatis。前者靠方法名自动生成查询,后者靠手写 SQL 精细控制。这篇笔记用同一个设备集合(Device)把两边拆开对比,顺带记录一个真实踩过的并发坑。框架没有绝对优劣,分界线在于:你要的是「少写代码」还是「控制语句」。

Repository:方法名就是查询语句

定义一个接口,继承 MongoRepository,查询方法按命名规则声明即可:

@Repository
public interface DeviceRepository extends MongoRepository<Device, String> {
    Device findByDeviceId(String deviceId);
    List<Device> findByDeviceType(String deviceType);
    List<Device> findByStatus(String status);
}

findByDeviceId 会被框架解析成 { "deviceId" : 参数 } 的 MongoDB 查询,启动时动态代理生成实现类,XML 映射文件完全不存在。基础增删改查由父接口自带:save()、findById()、findAll()、delete()。

实体类用注解完成映射:

@Document(collection = "device")
public class Device {
    @Id
    private String id; // MongoDB内置主键 _id
    @Indexed(unique = true)
    private String deviceId; // 业务设备编号(业务唯一,≠_id)
    private String deviceType;
    private String status;
    @Field("stationCode")
    private String station;
}

几个注解的分工要分清:@Document 指定集合名;@Id 映射内置主键 _id;@Indexed(unique=true) 给业务编号建唯一索引;@Field 处理文档字段名与 Java 驼峰不一致的情况。

多条件复杂查询走 @Query 写原生 JSON:

@Query("{ 'status': 'online', 'station': ?0 }")
List<Device> findOnlineByStation(String station);

方法名的解析规则值得花两分钟记住:findByXxx 生成等值查询,findByXxxAndYyy 拼条件,findByXxxIgnoreCase 附加修饰符。凡是方法名能表达的单字段、简单组合查询,优先走命名推导;一旦涉及嵌套文档、数组条件或聚合,别硬凑方法名,直接上 @Query——可读性和可维护性都更好。业务层的典型用法也保持极简:查出来、改字段、save 回去三步。

一个真实踩过的坑:save 的并发覆盖

Kafka 消息并发消费场景下,多条消息同时读到同一份文档,各自改完字段再调 save(),后保存的会把前面的修改整个覆盖掉,中间状态直接丢失。这是「读-改-全量写」模式的固有缺陷,save() 的更新依据又是内置 _id 而不是业务字段,碰撞概率比直觉高。

修复思路是把全量覆盖改成原子局部更新:

@Query("{ 'deviceId': ?0 }")
Device findByDeviceId(String deviceId);
// 原子更新指定字段,避免读改写窗口
mongoTemplate.updateFirst(
    Query.query(Criteria.where("deviceId").is(deviceId)),
    Update.update("status", "online"), Device.class);

另一个前置防线是唯一索引。deviceId 在业务上必须唯一,索引建好之后重复插入会直接报 MongoDB 的 E11000 错误——这本身就是一次有效的写入侧校验,可以用 db.device.createIndex({deviceId:1},{unique:true}) 在集合上先行建立,再由 @Indexed 注解在应用层声明。

还有个版本相关的注意点:多文档事务需要 MongoDB 4.0 以上的副本集部署,单机版跑不起事务,Spring Data 侧的 @Transactional 在单机上会静默失效。测试环境和生产拓扑保持一致,别在单机上验证事务逻辑。

顺着唯一索引再延伸一步:业务键和内置主键的分离要在团队里形成共识。所有按 deviceId 的关联查询、跨服务引用一律走业务键,_id 只留给框架内部使用——不然哪天换库或做数据迁移,靠 _id 关联的数据会全部悬空。插入侧的错误处理也要预设好:唯一索引冲突抛出的异常要转成业务层的「设备已存在」响应,而不是让 500 直接漏到调用方。

MyBatis 侧的对照

同样的查询,MyBatis 要写接口加 XML 两份:

public interface DeviceMapper {
    Device findByDeviceId(String deviceId);
    List<Device> findByDeviceType(String deviceType);
    int updateStatus(Device device);
}

<select id="findByDeviceId" resultType="Device">
    select id,device_id,device_type,status from device where device_id=#{deviceId}
</select>

MyBatis 的定位是关系型数据库:SQL 全部手写(XML 或注解),没有自带 CRUD,字段映射靠 resultMap 配置,动态代理只是执行预定义的 SQL。换来的是对语句的完全控制力,复杂关联查询、数据库方言优化都是它的主场。

选型对照

对比项 Spring Data Mongo MyBatis
数据库 MongoDB 文档库 MySQL 等关系库
映射文件 不需要 XML Mapper.xml 或注解 SQL
简单查询 方法名自动解析 全部手写
CRUD 父接口自带 自己写
实体映射 @Document/@Field 注解 resultMap 配置
主键 内置 _id 与业务键分离 表主键自定义

一句话收束:MongoDB 上做以 CRUD 为主的快速开发,Repository 的方法名驱动能把样板代码压到最低;落到关系库、需要 SQL 级控制时再选 MyBatis。真正决定成败的往往不是框架,而是 _id 与业务键的区分、唯一索引的落位、并发写入的原子性这几个细节——它们在两种框架里都存在,只是表现形式不同。

0

评论0

请先
显示验证码
没有账号?注册  忘记密码?