Spring-Data MongoDB与接口类型字段的问题

16

我正在使用Spring-Data来操作MongoDB:

版本信息 - org.mongodb.mongo-java-driver版本为2.10.1,org.springframework.data.spring-data-mongodb版本为1.2.1.RELEASE。

我有一个类似于这里定义的情况(抱歉格式有点乱...):

我刚开始使用Java和spring-data-mongodb开发某个应用程序时遇到了一些问题,但我一直没有解决:

我有几个文档bean,如下所示:

@Document(collection="myBeanBar")
public class BarImpl implements Bar {
    String id;
    Foo foo;
    // More fields and methods ... 
}

@Document
public class FooImpl implements Foo {
    String id;
    String someField;
    // some more fields and methods ...
} 

我有一个仓库类,其中有一个方法只是简单地调用了类似于这样的查找方法:
public List<? extends Bar> findByFooField(final String fieldValue) {
    Query query = Query.query(Criteria.where("foo.someField").is(fieldValue));
    return getMongoOperations().find(query, BarImpl.class);
} 

保存 Bar 对象没有问题,它会将其存储在 mongo 中,并且会为 Foo 和 Bar 都添加 "_class" 属性。然而,通过 Foo 的某个属性查找时会抛出如下异常:
Exception in thread "main" java.lang.IllegalArgumentException: No
property someField found on test.Foo!
    at org.springframework.data.mapping.context.AbstractMappingContext.getPersistentPropertyPath(AbstractMappingContext.java:225)
    at org.springframework.data.mongodb.core.convert.QueryMapper.getPath(QueryMapper.java:202)
    at org.springframework.data.mongodb.core.convert.QueryMapper.getTargetProperty(QueryMapper.java:190)
    at org.springframework.data.mongodb.core.convert.QueryMapper.getMappedObject(QueryMapper.java:86)
    at org.springframework.data.mongodb.core.MongoTemplate.doFind(MongoTemplate.java:1336)
    at org.springframework.data.mongodb.core.MongoTemplate.doFind(MongoTemplate.java:1322)
    at org.springframework.data.mongodb.core.MongoTemplate.find(MongoTemplate.java:495)
    at org.springframework.data.mongodb.core.MongoTemplate.find(MongoTemplate.java:486)

给出的解决方案是在抽象类上使用@TypeAlias注释,这告诉框架使用特定的实现(在这种情况下为FooImpl)。
在我的情况下,我有接口成员而不是抽象成员:
@Document(collection="myBeanBar")
public class BarImpl implements Bar {
    String id;
    IFoo foo;
    // More fields and methods ...
}

我非常不愿意在接口IFoo上放置一个注释,以提供默认实现。相反,我想告诉框架,在实现BarImpl类的上下文中,这个字段的默认实现是什么,类似于@JsonTypeInfo:

@Document(collection="myBeanBar") 
public class BarImpl implements Bar {
    String id;    

    @JsonTypeInfo(use = Id.CLASS, defaultImpl = FooImpl.class)
    IFoo foo; 

    // More fields and methods ... 
}

我找到了这个答案,它或多或少地说要避免使用接口。但如果没有更好的选择,我很乐意知道。

有什么想法吗?

谢谢!


你使用的Spring Data MongoDB版本是哪个? - Oliver Drotbohm
好的,我添加了版本信息 - org.mongodb.mongo-java-driver 版本 2.10.1,org.springframework.data.spring-data-mongodb 版本 1.2.1.RELEASE。 - Ido Cohn
2
感觉你可能遇到了 https://jira.springsource.org/browse/DATACMNS-311 这个问题。请问你的类路径中是否有 Spring Data Commons 1.5.1?这个版本已经修复了这个 bug。 - Oliver Drotbohm
嘿@OliverGierke,是的 - 那是我路径中的版本。 感谢链接,看起来我无法在仍使用接口的情况下修复此问题,因此我不得不使用实现类。希望他们尽快解决这个问题。 - Ido Cohn
我有同样的问题,最终采用了一个包装类来封装我想要的那个类。例如,Thing类有Foo和Bar(但不是两者都有),然后使用Thing来持久化。在返回时获取thing.isFoo();和thing.getFoo()。 - Alexandre Santos
你能否开始在调试中添加你的包org.springframework.data?这样你就可以看到生成的查询并将其与你的MongoDB模式进行比较。 - Patouche
5个回答

10

我的问题与该问题类似,但抛出的异常略有不同:

Could not instantiate bean class [class name]: Specified class is an interface

当我的DB类字段之一声明为接口时,就会出现这种情况。保存该字段没有问题,但从MongoDB读取它时会抛出异常。最后我找到了解决方案,利用org.springframework.core.convert.converter.Converter

只需要两个步骤:1. 构建一个实现Converter的类; 2. 在servlet上下文中注册转换器。是的,您不必修改任何现有代码,例如添加注释。

以下是我的模型类,其中字段Data是一个接口:

@Document(collection="record")
public class Record {
    @Id
    private String id;

    // Data is an interface
    private Data data;

    // And some other fields and setter/getter methods of them
}

转换器:
@ReadingConverter
public class DataReadConverter implements Converter<DBObject, Data> {
    @Override
    public Data convert(DBObject source) {
        // Your implementation to parse the DBObject,
        // this object can be BasicDBObject or BasicDBList,
        // and return an object instance that implements Data.

        return null;
    }
}

最后要做的事情就是注册转换器,我的配置文件是XML格式:

<mongo:mongo id="mongo" />

<mongo:db-factory mongo-ref="mongo" dbname="example" />

<mongo:mapping-converter>
    <mongo:custom-converters>
        <mongo:converter>
            <beans:bean class="com.example.DataReadConverter" />
        </mongo:converter>
    </mongo:custom-converters>
</mongo:mapping-converter>

<beans:bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate">
    <beans:constructor-arg name="mongoDbFactory" ref="mongoDbFactory" />
    <beans:constructor-arg name="mongoConverter" ref="mappingConverter" />
</beans:bean>

部署应用程序并重试。它应该可以正确解析来自MongoDB的DBObject中的interface字段。

我的Spring MongoDB应用程序版本为:spring-*-4.1.0和spring-data-mongodb-1.6.0。


1

虽然其他答案提供的转换器解决方案可以工作,但它需要您为每个多态字段编写一个转换器。这很繁琐且容易出错,因为每次添加新的多态字段或更改现有模型时都需要记住这些转换器。

您可以配置类型映射而无需编写转换器。此外,您可以使其自动化运行。以下是如何实现:

  1. 首先,我们需要 反射库。在这里选择适用于您的构建工具的最新版本 here

    例如使用 Gradle:

    implementation("org.reflections:reflections:0.10.2")
    
  2. 现在,我们需要对 DefaultMongoTypeMapper 进行小型扩展,以便轻松配置和实例化。以下是在 Kotlin 中如何实现:

    class ReflectiveMongoTypeMapper(
        private val reflections: Reflections = Reflections("com.example")
    ) : DefaultMongoTypeMapper(
        DEFAULT_TYPE_KEY,
        listOf(
            ConfigurableTypeInformationMapper(
                reflections.getTypesAnnotatedWith(TypeAlias::class.java).associateWith { clazz ->
                    getAnnotation(clazz, TypeAlias::class.java)!!.value
                }
            ),
            SimpleTypeInformationMapper(),
        )
    )
    

    其中 com.example 是您的基本包或带有 MongoDB 模型的包。

    通过这种方式,我们将找到所有使用 @TypeAlias 注释的类并注册别名到类型映射。

  3. 接下来,我们需要稍微调整应用程序的 Mongo 配置。该配置必须扩展 AbstractMongoClientConfiguration,我们需要覆盖方法 mappingMongoConverter,以利用我们之前创建的映射器。它应该如下所示:

    override fun mappingMongoConverter(
        databaseFactory: MongoDatabaseFactory,
        customConversions: MongoCustomConversions,
        mappingContext: MongoMappingContext,
    ) = super.mappingMongoConverter(databaseFactory, customConversions, mappingContext).apply {
        setTypeMapper(ReflectiveMongoTypeMapper())
    }
    
  4. 完成!

现在所有别名到类型映射将在上下文启动时自动注册,所有你的多态字段都能正常工作。无需编写和维护转换器。
你可以在GitHub上查看完整的代码示例
此外,这里有一篇博客文章,你可以阅读有关此问题的根本原因以及其他解决方法(如果你不想依赖反射):https://blog.monosoul.dev/2022/09/16/spring-data-mongodb-polymorphic-fields/

谢谢@andrei-nevedomskii!您使用反射库上的ConfigurableTypeInformationMapper解决了多态字段的问题。Spring Data默认只支持文档的TypeAlias,但是我必须将字段保留为默认类名,因为DefaultMongoTypeMapper无法看到我们用于字段的类别名。最终,我可以从数据库中摆脱完整的类名。 - vbg

1
我遇到了与@victor-wong相同的错误信息。
以下代码解决了使用Spring Boot 2.3.2和spring-data-mongodb 3.0.2时出现的问题:
转换器:
import org.bson.Document;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.ReadingConverter;

@ReadingConverter
public class DataReadConverter implements Converter<Document, Data> {

    @Override
    public Data convert(Document source) {

        return new DataImpl(source.get("key"));
    }
}

最后一件事是注册转换器。
    @Bean
    public MongoCustomConversions customConversions() {
        return new MongoCustomConversions(
                List.of(
                        new DataReadConverter()
                )
        );
    }

更多信息请查看这里:https://jira.spring.io/browse/DATAMONGO-2391


0

我在多态反序列化方面遇到了同样的问题。基本上,我从两个来源(REST API和ETL)导入数据。使用Spring Data进行的REST API导入很好,因为它们有“_class”字段,所以反序列化不是问题。但是,ETL导入的数据没有“_class”字段,因此在将其反序列化为接口时,它不知道要使用哪个具体类。

我尝试扩展DefaultMongoTypeMapper,但这并不起作用,因为我试图映射一个在Mongo文档中不存在的字段。

因此,对我有效的解决方案是在文档到达应用程序但反序列化开始之前拦截反序列化。此时,我可以简单地验证“_class”字段是否存在,如果不存在,则识别文档类型并添加相应的具体类的“_class”字段。

这是通过覆盖AbstractMongoEventListener的onAfterLoad()方法来完成的。

@Slf4j
@Component
public class WrapperRepositoryListener extends 
AbstractMongoEventListener<Data> {
    @Override
    public void onAfterLoad(AfterLoadEvent<Data> event) {
         Document field = Document)event.getDocument().get(ROOT_FIELD);
         final String type = String.valueOf(field.get("type")).toLowerCase();

         switch (type) {
            case TYPE -> field.put("_class", CLASSNAME);
            default -> log.debug("Type is not supported: " + type);
         }
}

}

我希望这篇文章能够帮助到所有来到这个页面的人。


-2

在数据对象中定义接口是一个非常糟糕的想法。

接口意味着某个对象能够做某些事情,但并不提供有关字段的任何信息。你真的需要使用接口吗?你能避免这种情况吗?即使使用抽象类定义也会是更好的选择。

P.S. 当然,在任何情况下,我的答案都不能被标记为正确答案。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接