简体   繁体   English

Spring 数据、Mongo 和@TypeAlias:读取不工作

[英]Spring Data, Mongo, and @TypeAlias: reading not working

The issue问题

Awhile back I started using MongoDB and Spring Data.不久前,我开始使用 MongoDB 和 Spring 数据。 I'd left most of the default functionality in place, and so all of my documents were stored in MongoDB with a _class field pointing to the entity's fully-qualified class name.我保留了大部分默认功能,因此我的所有文档都存储在 MongoDB 中,其中_class字段指向实体的完全限定名称 class。

Right away that didn't "smell" right to me, but I left it alone.马上就没有“闻到”我的味道,但我没有理会它。 Until recently, when I refactored a bunch of code, and suddenly none of my documents could be read back from MongoDB and converted into their (refactored/renamed) Java entities.直到最近,当我重构一堆代码时,突然间我的所有文档都无法从 MongoDB 中读回并转换为它们(重构/重命名)的 Java 实体。 I quickly realized that it was because there was now a fully-qualified-classname mismatch.我很快意识到这是因为现在存在完全限定的类名不匹配。 I also quickly realized that--given that I might refactor again sometime in the future--if I didn't want all of my data to become unusable I'd need to figure something else out.我也很快意识到——考虑到我可能会在未来的某个时候再次重构——如果我不想让我的所有数据变得不可用,我需要想出别的办法。

What I've tried我试过的

So that's what I'm doing, but I've hit a wall.这就是我正在做的,但我碰壁了。 I think that I need to do the following:认为我需要执行以下操作:

  • Annotate each entity with @TypeAlias("ta") where "ta" is a unique, stable string.使用@TypeAlias("ta")注释每个实体,其中“ta”是唯一的、稳定的字符串。
  • Configure and use a different TypeInformationMapper for Spring Data to use when converting my documents back into their Java entities;为 Spring 数据配置和使用不同的TypeInformationMapper ,以便在将我的文档转换回其 Java 实体时使用; it needs to know, for example, that a type-alias of "widget.foo" refers to com.myapp.document.FooWidget .例如,它需要知道“widget.foo”的类型别名指的是com.myapp.document.FooWidget

I determined that I should use a TypeInformationMapper of type org.springframework.data.convert.MappingContextTypeInformationMapper .我确定我应该使用org.springframework.data.convert.MappingContextTypeInformationMapper类型的TypeInformationMapper Supposedly a MappingContextTypeInformationMapper will scan my entities/documents to find @TypeAlias'ed documents and store an alias->to->class mapping.据说 MappingContextTypeInformationMapper 将扫描我的实体/文档以查找 @TypeAlias'ed 文档并存储别名->到->类映射。 But I can't pass that to my MappingMongoConverter;但我无法将其传递给我的 MappingMongoConverter; I have to pass a subtype of MongoTypeMapper.我必须传递 MongoTypeMapper 的子类型。 So I am configuring a DefaultMongoTypeMapper , and passing a List of one MappingContextTypeInformationMapper as its "mappers" constructor arg.因此,我正在配置一个DefaultMongoTypeMapper ,并将一个MappingContextTypeInformationMapper的列表作为其“映射器”构造函数 arg 传递。

Code代码

Here's the relevant part of my spring XML config:这是我的 spring XML 配置的相关部分:

<bean id="mongoTypeMapper" class="org.springframework.data.mongodb.core.convert.DefaultMongoTypeMapper">
    <constructor-arg name="typeKey" value="_class"></constructor-arg>
    <constructor-arg name="mappers">
        <list>
            <ref bean="mappingContextTypeMapper" />
        </list>
    </constructor-arg> 
</bean>

<bean id="mappingContextTypeMapper" class="org.springframework.data.convert.MappingContextTypeInformationMapper">
    <constructor-arg ref="mappingContext" />
</bean>

<bean id="mappingMongoConverter"
    class="org.springframework.data.mongodb.core.convert.MappingMongoConverter">
    <constructor-arg ref="mongoDbFactory" />
    <constructor-arg ref="mappingContext" />
    <property name="mapKeyDotReplacement" value="__dot__" />
    <property name="typeMapper" ref="mongoTypeMapper"/>
 </bean>

 <bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate">
    <constructor-arg ref="mongoDbFactory" />
    <constructor-arg ref="mappingMongoConverter" />
 </bean>

Here's a sample entity/document:这是一个示例实体/文档:

@Document(collection="widget")
@TypeAlias("widget.foo")
public class FooWidget extends Widget {

    // ...

}

One important note is that any such "Widget" entity is stored as a nested document in Mongo.一个重要的注意事项是任何此类“Widget”实体都作为嵌套文档存储在 Mongo 中。 So in reality you won't really find a populated "Widget" collection in my MongoDB instance.所以实际上你不会在我的 MongoDB 实例中找到一个填充的“Widget”集合。 Instead, a higher-level "Page" class can contain multiple "widgets" like so:相反,更高级别的“页面”class 可以包含多个“小部件”,如下所示:

@Document(collection="page")
@TypeAlias("page")
public class Page extends BaseDocument {

    // ...

    private List<Widget> widgets = new ArrayList<Widget>();

}

The error I'm stuck on我坚持的错误

What happens is that I can save a Page along with a number of nested Widgets in Mongo.发生的事情是我可以在 Mongo 中保存一个页面以及许多嵌套的小部件。 But when I try to read said Page back out, I get something like the following:但是,当我尝试重新读取所述页面时,我得到如下内容:

org.springframework.beans.BeanInstantiationException: Could not instantiate bean class [com.myapp.document.Widget]: Is it an abstract class?

I can indeed see pages in Mongo containing "_class": "page" , with nested widgets also containing "_class": "widget.foo" It just appears like the mapping is not being applied in the reverse.我确实可以在 Mongo 中看到包含"_class": "page"的页面,嵌套的小部件也包含"_class": "widget.foo"看起来映射没有被反向应用。

Is there anything I might be missing?有什么我可能会遗漏的吗?

I spent a bunch of time with my debugger and the Spring Data source code, and I learned that Spring Data isn't as good as it probably should be with polymorphism as it should be, especially given the schema-less nature of NoSQL solutions like MongoDB. 我花了很多时间使用我的调试器和Spring Data源代码,并且我了解到Spring Data并不像它应该具有的多态性一样好,特别是考虑到NoSQL解决方案的无模式特性, MongoDB的。 But ultimately what I did was to write my own type mapper, and that wasn't too tough. 但最终我所做的就是编写自己的类型映射器,这并不太难。

The main problem was that, when reading in my Page document, the default mappers used by Spring Data would see a collection called widgets , then consult the Page class to determine that widgets pointed to a List, then consult the Widget class to look for @TypeAlias information. 主要问题是,在我的Page文档中读取时,Spring Data使用的默认映射器会看到一个名为widgets的集合,然后查阅Page类以确定指向List的小部件,然后查阅Widget类以查找@ TypeAlias信息。 What I needed instead was a mapper that scanned my persistent entities up front and stored an alias-to-class mapping for later use. 我需要的是一个映射器,它可以预先扫描我的持久性实体,并存储一个别名到类的映射供以后使用。 That's what my custom type mapper does. 这就是我的自定义类型映射器所做的。

I wrote a blog post discussing the details . 我写了一篇讨论细节博客文章

In the default setting, the MappingMongoConverter creates a DefaultMongoTypeMapper which in turn creates a MappingContextTypeInformationMapper . 在默认设置中, MappingMongoConverter创建DefaultMongoTypeMapper ,后者又创建MappingContextTypeInformationMapper

That last class is the one responsible for maintaining the typeMap cache between TypeInformation and aliases. 最后一个类是负责维护TypeInformation和别名之间的typeMap缓存的类。

That cache is populated in two places: 该缓存填充在两个地方:

  1. In the constructor, for each mappingContext.getPersistentEntities() 在构造函数中,为每个mappingContext.getPersistentEntities()
  2. When writing an object of an aliased type. 编写别名类型的对象时。

So if you want to make sure the alias is recognized in any context, you need to make sure that all your aliased entities are part of mappingContext.getPersistentEntities() . 因此,如果要确保在任何上下文中都能识别别名,则需要确保所有别名实体都是mappingContext.getPersistentEntities()一部分。

How you do that depends on your configuration. 如何执行此操作取决于您的配置。 For instance: 例如:

  • if you're using AbstractMongoConfiguration , you can overwrite its getMappingBasePackage() to return the name of a package containing all of your entities. 如果您正在使用AbstractMongoConfiguration ,则可以覆盖其getMappingBasePackage()以返回包含所有实体的包的名称。
  • if you're using spring boot, you can use @EntityScan to declare which packages to scan for entities 如果您使用的是Spring引导,则可以使用@EntityScan来声明要扫描实体的软件包
  • in any case, you can always configure it with a custom set (from a static list or a custom scan) using mongoMappingContext.setInitialEntitySet() 在任何情况下,您始终可以使用mongoMappingContext.setInitialEntitySet()使用自定义集(从静态列表或自定义扫描)配置它

One side note, for an entity to be discovered by a scan, it has to be annotated with either @Document or @Persitent . 另一方面,对于要通过扫描发现的实体,必须使用@Document@Persitent进行注释。

More informations can be found in the spring-data-commons Developer Guide 可以在spring-data-commons开发人员指南中找到更多信息

If you extend AbstractMongoConfiguration , you can override method getMappingBasePackage to specify the base package for your documents. 如果扩展AbstractMongoConfiguration ,则可以覆盖方法getMappingBasePackage以指定文档的基本包。

@Configuration
class RepositoryConfig extends AbstractMongoConfiguration {

    @Override
    protected String getMappingBasePackage() {
        return "com.example";
    }

} }

Update: In spring-data-mongodb 2+ you should use: 更新:在spring-data-mongodb 2+中你应该使用:

    @Configuration
    class RepositoryConfig extends AbstractMongoConfiguration {
            @Override
            protected Collection<String> getMappingBasePackages(){
                return Arrays.asList("com.example");
            }
        }

because getMappingBasePackage() is no deprecated and won't work. 因为getMappingBasePackage()没有被弃用,所以不起作用。

Today I ran into the exact same issue. 今天我遇到了完全相同的问题。 After more research I found out that my subclass was missing a repository. 经过更多研究后,我发现我的子类缺少一个存储库。 It appears that Spring Data is using the repositories to determine which concrete subclass to create and when it is missing, it falls back to the superclass which in this case is abstract. 看起来Spring Data正在使用存储库来确定要创建哪个具体子类以及何时缺少它,它会回退到超类,在这种情况下它是抽象的。

So please try to add a FooWidgetRepository and map it to FooWidget with correct ID type. 因此,请尝试添加FooWidgetRepository并将其映射到具有正确ID类型的FooWidget。 It might work in your case as well. 它也可能适用于您的情况。

If you use spring boot with auto-configuration, declaring the following bean can help:如果您使用 spring 引导自动配置,声明以下 bean 会有所帮助:

@Bean
MongoMappingContext mongoMappingContext(ApplicationContext applicationContext, MongoCustomConversions conversions) throws ClassNotFoundException {
    MongoMappingContext context = new MongoMappingContext();
    context.setInitialEntitySet(new EntityScanner(applicationContext).scan(Persistent.class));
    context.setSimpleTypeHolder(conversions.getSimpleTypeHolder());
    return context;
}

what does the trick is the following line:诀窍是以下行:

new EntityScanner(applicationContext).scan(Persistent.class)

Instead of scanning for Document s it will scan for both Document and TypeAlias , since both of these annotations are Persistent它不会扫描Document ,而是同时扫描DocumentTypeAlias ,因为这两个注释都是Persistent

Andreas Svensson is right, this can be done much simpler than described by Dave Taubler . Andreas Svensson是对的,这可以比Dave Taubler描述的要简单得多。

I posted a slightly more elaborate answer than Andreas' (including sample code) in this post . 我在这篇文章中发布了一个比Andreas(包括示例代码)更精细的答案。 Excerpt: 摘抄:

So all you need to do is to declare an "unused" Repository-Interface for your sub-classes, just like you proposed as "unsafe" in your OP: 因此,您需要做的就是为您的子类声明一个“未使用的”存储库接口,就像您在OP中提出的“不安全”一样:

public interface NodeRepository extends MongoRepository<Node, String> { 
  // all of your repo methods go here
  Node findById(String id);
  Node findFirst100ByNodeType(String nodeType);
  ... etc.
}
public interface LeafType1Repository extends MongoRepository<LeafType1, String> {
  // leave empty
}
public interface LeafType2Repository extends MongoRepository<LeafType2, String> { 
  // leave empty
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM