Spring Data MongoDB — My take on inheritance support

Mladen Maravić
4 min readFeb 3, 2018

--

Spring Data MongoDB project provides MongoDB support for any Spring-based app using the usual Spring Data abstractions: entities and repositories. While the project covers a lot of functionality out-of-the-box, there is one glaring omission: support for inheritance in your entity classes.

The reason is simple: it is a hard thing to do right. Some support for inheritance arrived when a “hidden” field was added to all entity classes as they were stored in the database:_class. By default, Spring will add this field and put the full class name of the entity type when storing data into the database. Unfortunately, this is where any inheritance support more or less stops.

The problem

For example, imagine a simple hierarchy of classes (get/set methods omitted):

@Document(collection = "things")
public abstract class Thing {

@Id
private String id;
}
@Document(collection = "things")
public class Car extends Thing {

}
@Document(collection = "things")
public class Boat extends Thing {

}

So basically, we have a superclass named “Thing” and two concrete subclasses named “Car” and “Boat”. We store all of those in the same collection - a collection of “things”. Lets now define three standard Spring Data repositories:

public interface ThingRepository extends 
CrudRepository<Thing, String> { }
public interface CarRepository extends
CrudRepository<Car, String> { }
public interface BoatRepository extends
CrudRepository<Boat, String> { }

Finally, let’s give this a try:

Car honda = new Car();
carRepository.save(honda);

Boat enterprise = new Boat();
boatRepository.save(enterprise);

// We should have 2 things in the collection
assertThat(thingRepository.count()).isEqualTo(2); // TRUE

// But only one of each specific types of things
assertThat(carRepository.count()).isEqualTo(1); // FALSE, gives 2
assertThat(boatRepository.count()).isEqualTo(1); // FALSE, gives 2

Not exactly what we hoped for, right? Turns out, when doing a count() operation, the _class field is not used at all in queries generated by any of the three repositories. This is to be expected — just take a look at the source code for the class that implements the basic set of operations.

My use-case

At the time of writing this, I was working on a project that really needed a basic inheritance support to be available. The system had to deal with data that could be very easily modeled using basic OOP tools: abstraction and inheritance. We didn’t even have to support long inheritance chains — the max was 2 levels deep. So I set to implement my own inheritance support that would solve all of my (simple) needs:

  • Superclass repositories (ThingRepository in example above) must operate on all data in the collection, regardless of actual type.
  • Subclass repositories (CarRepository and BoatRepository in example above) should operate, by default and without any additional code, only on data that is of the subclass type.

One additional thing I need to mention is that I use the @TypeAlias annotation to distinguish between types. The reason is simple: we were just starting the project and there was still time for a lot of refactoring. By default, Spring Data MongoDB fills the _class field with the fully qualified class name of the entity class. This means that if we do any refactoring involving class names or package names, we will have to do search & replace all over the database. Hence we decided to use type aliases which give us stable data in database in exchange for a little bit more work.

Please note: the solution presented here was created with our use-case in mind! If you have a different use case, this implementation would have to be modified to suit your needs!

The solution

While designing a proof of concept of a solution, we decided to follow a couple of simple rules:

  • All entity classes for which a repository interface is to be declared must have a @Document annotation with a collection attribute.
  • All entity classes that share a common superclass will be stored in the same collection.
  • To support inheritance, all subclasses (concrete classes) will use the @TypeAlias annotation.

Following the stated rules, here are the revised entity classes (get/set method omitted):

@Document(collection = "things")
public abstract class Thing {

@Id
private String id;
}
@Document(collection = "things")
@TypeAlias("car")
public class Car extends Thing {
}@Document(collection = "things")
@TypeAlias("boat")
public class Boat extends Thing {
}

All three classes are annotated as documents, stored in the same MongoDB collection. The specific subclasses use the type alias annotation. The repositories remain the same — that was the goal we wanted to achieve!

The first thing we had to fix was the basic implementation of the repository. Namely the implementations of the count() and findAll(). We wanted those methods to take the entity type into consideration, but only for classes that actually have the type alias annotation (because only those are type-specific, according to our rules and our use-case). To implement this, we created a subclass of SimpleMongoRepository:

The other thing we have to take care of are the queries created automatically by Spring for specially name methods in repositories:

We didn’t bother with repository queries that used the @Query annotation — for those we could easily add the appropriate criteria inside the query statement manually.

All that is left, is some bits of glue code. The most important part is our extension of the standard MongoRepositoryFactory. This class is important because we need to be able to tell Spring Data MongoDB to use our version of the PartTreeMongoQuery shown above:

Update 2018–11–27: If you are using Spring Data ≥ 2.1.0 please substitute class “EvaluationContextProvider” with “QueryMethodEvaluationContextProvider”.

Next comes the basic glue that ties it all together:

And finally, we need to make sure Spring Data MongoDB is bootstrapped properly to use our classes:

@Configuration
@EnableMongoRepositories(
repositoryBaseClass = InheritanceAwareSimpleMongoRepository.class,
repositoryFactoryBeanClass = InheritanceAwareMongoRepositoryFactoryBean.class)
public class MongoConfigurer {

}

And that is it.

A demo app and all of the code is available on github: https://github.com/beb4ch/spring-data-mongodb-inheritance-test

--

--