Q:使用 Example 作为查询条件时,为什么会对比该对象的类型?
原因:
这是 Spring Data 的查询机制,目的是控制多态的应用,防止父子类存在相同的字段,导致查询到错误的表上。
场景举例
假设有一个集合 animals,里面存储了两种不同类型的文档,它们都有 color 属性:
- 文档 A (Dog):
{ "_class": "com.test.Dog", "color": "black", "tailLength": 10 }
- 文档 B (Cat):
{ "_class": "com.test.Cat", "color": "black", "whiskerLength": 5 }
如果我们构建一个 Dog 对象作为样本进行查询:
Dog probe = new Dog();
probe.setColor("black");
repository.findOne(Example.of(probe));
结果对比
- 如果不对比
_class:
查询条件仅为{ "color": "black" }。MongoDB 会同时返回Dog和Cat。当 Spring 尝试将Cat的数据反序列化为Dog对象时,可能会因为字段不兼容(如找不到tailLength)导致报错或数据错乱。 - 加入
_class 限制(现状):
查询条件变为{ "color": "black", "_class": "com.test.Dog" }。这样就精准排除了Cat,保证查回来的只能是Dog。
MongoDB查询源码
- Repository 接口层
- org.springframework.data.repository.query.QueryByExampleExecutor#findOne
- 接收用户构建的Example对象(包含 Probe 样本)。
- Probe (样本): 它是你实际创建的一个 Java 对象(Entity)。你需要在这个对象中,把你想要查询的字段填上值,不查的字段留空(Null)。
- 默认实现层
- org.springframework.data.mongodb.repository.support.SimpleMongoRepository#findOne
- 将Example对象转换成内部的Query对象
@Override
public <S extends T> Optional<S> findOne(Example<S> example) {
Assert.notNull(example, "Sample must not be null");
Query query = new Query(new Criteria().alike(example)) //
.collation(entityInformation.getCollation());
getReadPreference().ifPresent(query::withReadPreference);
return Optional
.ofNullable(mongoOperations.findOne(query, example.getProbeType(), entityInformation.getCollectionName()));
}
- 核心模板层(MongoTemplate)
- org.springframework.data.mongodb.core.MongoTemplate#findOne
- org.springframework.data.mongodb.core.MongoTemplate#doFindOne
- 负责准备数据库连接、集合名称,并启动查询构建流程。
- 查询映射层(核心逻辑)
- org.springframework.data.mongodb.core.QueryOperations.QueryContext#getMappedQuery
- org.springframework.data.mongodb.core.convert.QueryMapper#getMappedObject
- SpringData 将Java的查询对象转换为Mongodb原生的BSON文档
- 类型转换层(TypeMapper)
- org.springframework.data.mongodb.core.convert.DefaultMongoTypeMapper#writeTypeRestrictions
- 负责计算并写入类型限制条件(
_class)
@Override
public void writeTypeRestrictions(Document result, @Nullable Set<Class<?>> restrictedTypes) {
// 1. 如果没有限制类型(比如 findById),直接返回,不做坏事
if (ObjectUtils.isEmpty(restrictedTypes)) {
return;
}
// 2. 准备一个列表,用来放允许的类名(对应 MongoDB 的 $in 操作符)
BasicDBList restrictedMappedTypes = new BasicDBList();
// 3. 遍历你传入的 Entity 类(比如 Dog.class)
for (Class<?> restrictedType : restrictedTypes) {
// 【关键点 A】 获取类的别名。
// 如果你没有加 @TypeAlias 注解,这里获取到的就是 "全类名" (包名+类名)!
// 你的情况:com.test.Dog
Alias typeAlias = getAliasFor(TypeInformation.of(restrictedType));
if (!ObjectUtils.nullSafeEquals(Alias.NONE, typeAlias) && typeAlias.isPresent()) {
// 把全类名加到列表里
restrictedMappedTypes.add(typeAlias.getValue());
}
}
// 【关键点 B】 强制写入查询条件!
// accessor.writeTypeTo 会生成键名为 "_class" 的字段
// 最终生成的 BSON 就是: { "_class": { "$in": [ "你的新全类名" ] } }
accessor.writeTypeTo(result, new Document("$in", restrictedMappedTypes));
}
流程图示
结论
在使用Example进行查询时,Spring Data MongoDB遵循 严格类型匹配原则,由于Example查询的语义是基于样本对象进行查找,框架默认为对象的类型也行查询条件的一部分
- DefaultMongoTypeMapper: 在检测到实体类未配置
@TypeAlias注解时,默认会使用当前类的 全限定名(包名+类名) 来生成_class查询条件。因此,若代码中对包名进行了重构(变更),会导致生成的查询条件(新包名)与 MongoDB 中存储的历史数据(旧包名)不匹配,从而引发查询失败。
评论