Search code examples
javamongodbhibernatehibernate-ogm

Hibernate OGM ~ Discriminating Polymorphic Objects (Abstract Superclasses)


I have a data model for MongoDB in Java that uses abstract superclasses to discriminate between polymorphic objects like this:

[Object that gets saved in the database]
└[List<AbstractSuperclass>]
 ├[SubclassA extends AbstractSuperclass]
 ├[SubclassB extends AbstractSuperclass]
 ├[SubclassA extends AbstractSuperclass]
 ├[SubclassC extends AbstractSuperclass]
 ├[SubclassC extends AbstractSuperclass]
 └[SubclassD extends AbstractSuperclass]

When I'm just using Java MongoDB Driver, that works just fine if I use the @BsonDiscriminator annotation.

However, I have not yet been able to get this to work with Hibernate. I have tried various annotations, all of which have produced various degrees of fail. Among the things I've tried are:

  • @AbstractSuperclass
  • @DiscriminatorColumn and @DiscriminatorValue
  • @OneToMany

I may have done it wrong though, so feel free to suggest any of the above if you think it will solve the problem.

To easily reproduce the situation, I have created a simple sample project that replicates the data structure in a simplified way and manages to reproduce the issue:

Let the GameCharacter class be the object I want to store with all its children. As I understand it, that means that the GameCharacter should be the only @Entity here, since all other objects exist only in regards to the GameCharacter.

@Entity
public class GameCharacter {
    @Id
    public String _id;
    public String name;
    public Weapon weapon;
    public Armor armor;
    @OneToMany
    public List<Item> inventory;
    
    public GameCharacter() {
        
    }
    
    public GameCharacter(String name, Weapon weapon, Armor armor, String _id, List<Item> inventory) {
        this._id = _id;
        this.name = name;
        this.weapon = weapon;
        this.armor = armor;
        this.inventory = inventory;
    }
}

Let Item be an abstract superclass of inventory items.

@BsonDiscriminator
@Entity
@DiscriminatorColumn(name="ITEM_TYPE")
public abstract class Item {
    @Id
    public String name;
    public int amount;
    
    public Item() {
        
    }
    
    public Item(String name, int amount) {
        this.name = name;
        this.amount = amount;
    }
}

Let Potion be one of several potential classes filling up the inventory and inheriting from the abstract Item class.

@Embeddable
@DiscriminatorValue("POTION")
public class Potion extends Item {
    
    public Potion() {
    } 
    
    public Potion(String name, int amount) {
        super(name, amount);
    }
}

The above code samples are the closest I've come to a working solution. That way, at least Hibernate OGM can save and read from the database. However, it does not save them correctly, for upon inspecting the data in the MongoDB, it turns out to be this:

_id:"sylv01"
weapon:Object
    [...]
name:Sylvia the Hero
armor:Object
    [...]
inventory:Array
    0:Hi-Potion
    1:Mega Ether

...wheras it should be this (saved by Java MongoDB Driver):

_id:"sylv01"
weapon:Object
    [...]
name:Sylvia the Hero
armor:Object
    [...]
inventory:Array
    0:Object
        _t:"dataObjects.Potion"
        amount:3
        name:"Hi-Potion"
    1:Object
        _t:"dataObjects.Ether"
        amount:5
        name:"Mega Ether"

I assume that this might be due to me having to annotate Item as an @Entity, even though in my data model it really is more of an @Embeddable or an @AbstractSuperclass. However, if I do that, and maybe switch out the @OneToMany annotation in the GameCharacter for @ElementCollection, which I feel would be more fitting, I get errors like:

  • javax.persistence.PersistenceException: org.hibernate.InstantiationException: Cannot instantiate abstract class or interface: : dataObjects.Item
  • org.hibernate.MappingException: Could not determine type for: dataObjects.Item, at table: GameCharacter_inventory, for columns: [org.hibernate.mapping.Column(inventory)]

Anyway, here's a test case that currently fails with the above setup:

    @Test
    void writeAndReadShouldSaveInventoryCorrectly() throws SecurityException, IllegalStateException, NotSupportedException, SystemException, HeuristicMixedException, HeuristicRollbackException, RollbackException {
        
        GameCharacter sylvia = GameCharacters.sylvia();
        
        OgmAccessor.write(sylvia, entityManagerFactory);
        
        transactionManager.begin();
        GameCharacter loadedGameCharacter
            = entityManager.find(GameCharacter.class, sylvia._id);
        transactionManager.commit();
        
        int actualInventorySize = loadedGameCharacter.inventory.size();
        assertEquals(2, actualInventorySize);
    }

This test currently crashes with the following error:

    javax.persistence.EntityNotFoundException: Unable to find dataObjects.Item with id Hi-Potion

...which I assume is because I had to make the Inventory Items an @Entity to get this far.

However, what I actually want is a solution where everything related to the GameCharacter gets saved when I commit the GameCharacter, as the children-objects it contains have no meaning outside the GameCharacter and it thus does not make sense for them to exist as separate entities.

I get the feeling that I'm close to the solution, but I can't figure out what I'm doing wrong here and where. As such, any input would be greatly appreciated.


EDIT:

The dependencies I am using in my project are:

plugins {
    id 'java-library'
}

repositories {

    jcenter()
}

dependencies {
    // Use JUnit test framework
    testCompile("org.junit.jupiter:junit-jupiter:5.6.0")
    testCompile("org.junit.platform:junit-platform-runner:1.6.0")
    
    //MongoDB Driver
    compile 'org.mongodb:mongodb-driver:3.6.0'
    compile 'commons-logging:commons-logging:1.2'
    
    //Logging
    compile 'log4j:log4j:1.2.17'
    compile 'org.slf4j:slf4j-log4j12:1.7.25'
    compile 'commons-logging:commons-logging:1.2'
    
    //Versioning
    compile group: 'org.hibernate.ogm', name: 'hibernate-ogm-mongodb', version: '5.4.0.Final'
    compile group: 'org.jboss.narayana.jta', name: 'narayana-jta', version: '5.9.2.Final'
    compile group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.0'
}


EDIT:

I added this to a public github repository, so anyone who is interested can try and see if he finds a working version. The test project also contains a configuration for a MongoDB Docker Container that can be started using docker-compose up in the folder /mongo_db.

https://github.com/Kira-Cesonia/MongoDBVersioningTest


Solution

  • UPDATED: JPA doesn't support hierarchies for embeddables. Plus, you are using @OneToMany with an embeddable and not an entity.

    A mapping that will work is the following:

    @Entity
    public class GameCharacter {
        ...
    
        @ElementCollection
        public List<Item> inventory;
    
        ...
    }
    
    @BsonDiscriminator
    @Embeddable
    public class Item {
        public String type;
        public String name;
        public int amount;
    
        public Item() {
        }
    
        public Item(String name, int amount, String type) {
            this.name = name;
            this.amount = amount;
            this.type = type;
        }
    }