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:
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
.
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;
}
}