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 会同时返回 DogCat。当 Spring 尝试将 Cat 的数据反序列化为 Dog 对象时,可能会因为字段不兼容(如找不到 tailLength)导致报错或数据错乱。
  • 加入 ​_class​ 限制(现状):
    查询条件变为 { "color": "black", "_class": "com.test.Dog" }。这样就精准排除了 Cat,保证查回来的只能是 Dog

MongoDB查询源码

  1. Repository 接口层
  • org.springframework.data.repository.query.QueryByExampleExecutor#findOne
  • 接收用户构建的Example对象(包含 Probe 样本)。
  • Probe (样本): 它是你实际创建的一个 Java 对象(Entity)。你需要在这个对象中,把你想要查询的字段填上值,不查的字段留空(Null)。
  1. 默认实现层
  • 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()));
}
  1. 核心模板层(MongoTemplate)
  • org.springframework.data.mongodb.core.MongoTemplate#findOne
  • org.springframework.data.mongodb.core.MongoTemplate#doFindOne
  • 负责准备数据库连接、集合名称,并启动查询构建流程。
  1. 查询映射层(核心逻辑)
  • org.springframework.data.mongodb.core.QueryOperations.QueryContext#getMappedQuery
  • org.springframework.data.mongodb.core.convert.QueryMapper#getMappedObject
  • SpringData 将Java的查询对象转换为Mongodb原生的BSON文档
  1. 类型转换层(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 中存储的历史数据(旧包名)不匹配,从而引发查询失败。