Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Data, Mongo, and @TypeAlias: reading not working

The issue

Awhile back I started using MongoDB and Spring Data. 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.

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. 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.
  • Configure and use a different TypeInformationMapper for Spring Data to use when converting my documents back into their Java entities; it needs to know, for example, that a type-alias of "widget.foo" refers to com.myapp.document.FooWidget.

I determined that I should use a TypeInformationMapper of type org.springframework.data.convert.MappingContextTypeInformationMapper. Supposedly a MappingContextTypeInformationMapper will scan my entities/documents to find @TypeAlias'ed documents and store an alias->to->class mapping. But I can't pass that to my MappingMongoConverter; I have to pass a subtype of MongoTypeMapper. So I am configuring a DefaultMongoTypeMapper, and passing a List of one MappingContextTypeInformationMapper as its "mappers" constructor arg.

Code

Here's the relevant part of my spring XML config:

<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. So in reality you won't really find a populated "Widget" collection in my MongoDB instance. Instead, a higher-level "Page" class can contain multiple "widgets" like so:

@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. 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.

Is there anything I might be missing?

like image 403
Dave Taubler Avatar asked Nov 08 '14 23:11

Dave Taubler


Video Answer


1 Answers

In the default setting, the MappingMongoConverter creates a DefaultMongoTypeMapper which in turn creates a MappingContextTypeInformationMapper.

That last class is the one responsible for maintaining the typeMap cache between TypeInformation and aliases.

That cache is populated in two places:

  1. In the constructor, for each 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().

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.
  • if you're using spring boot, you can use @EntityScan to declare which packages to scan for entities
  • in any case, you can always configure it with a custom set (from a static list or a custom scan) using mongoMappingContext.setInitialEntitySet()

One side note, for an entity to be discovered by a scan, it has to be annotated with either @Document or @Persitent.

More informations can be found in the spring-data-commons Developer Guide

like image 106
Christophe S Avatar answered Nov 15 '22 22:11

Christophe S