Search code examples
javajpainheritancetreeeclipselink

JPA mapped tree structure with inheritance


I'm trying to implement a tree structure in JPA, that I want mapped to an H2 database using EclipseLink. The nodes of the tree are possibly subclasses of the base node class. What is happening is that EL is creating a brain-dead link table for the children as follows:

[EL Fine]: sql: 2015-04-10 13:26:08.266--ServerSession(667346055)--Connection(873610597)--CREATE TABLE ORGANIZATIONNODE_ORGANIZATIONNODE (OrganizationNode_IDSTRING VARCHAR NOT NULL, children_IDSTRING VARCHAR NOT NULL, Device_IDSTRING VARCHAR NOT NULL, monitors_IDSTRING VARCHAR NOT NULL, PRIMARY KEY (OrganizationNode_IDSTRING, children_IDSTRING, Device_IDSTRING, monitors_IDSTRING))

OrganizationNode is the proper superclass of Device. Both of these are @Entity, OrganizationNode extends AbstractEntity, which is a @MappedSuperclass where the @Id is defined (it is a string). Even stranger, while there is a Monitor class that is not in the tree structure, the only place "monitors" plural occurs is as a field of Device... what??

Now, it's fine to use a table like that to implement a tree structure, but I don't expect a compound primary key with separate instances of the Id field for each subclass! That's got to break - because some children are not Device, and therefore do not have a "Device_IDSTRING", and sure enough:

Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.5.0.v20130507-3faac2b): org.eclipse.persistence.exceptions.DatabaseException|Internal Exception: org.h2.jdbc.JdbcSQLException: NULL not allowed for column "DEVICE_IDSTRING"; SQL statement:|INSERT INTO ORGANIZATIONNODE_ORGANIZATIONNODE (children_IDSTRING, OrganizationNode_IDSTRING) VALUES (?, ?) [23502-186]|Error Code: 23502|Call: INSERT INTO ORGANIZATIONNODE_ORGANIZATIONNODE (children_IDSTRING, OrganizationNode_IDSTRING) VALUES (?, ?)|?bind => [2 parameters bound]|Query: DataModifyQuery(name="children" sql="INSERT INTO ORGANIZATIONNODE_ORGANIZATIONNODE (children_IDSTRING, OrganizationNode_IDSTRING) VALUES (?, ?)")

This seems like truly bizarre behavior. I've tried every combination of mapping annotations I could possibly think of to fix it. Any ideas?

Classes follow.

AbstractEntity.java:

@MappedSuperclass
public abstract class AbstractEntity {
// @Converter(name="uuidConverter",converterClass=UUIDConverter.class)
transient UUID id = null;
@Id String idString;

static long sequence = 1;

static long GREGORIAN_EPOCH_OFFSET = 12219292800000L;

public AbstractEntity() {
    ThreadContext tctx = ThreadContext.getThreadContext();
    long msb = tctx.getNodeID();
    long lsb = (System.currentTimeMillis()+GREGORIAN_EPOCH_OFFSET) * 1000 + ((sequence++) % 1000);
    lsb = (lsb & 0xCFFFFFFFFFFFFFFFL) | (0x8000000000000000L);  
    msb = (msb & 0xFFFFFFFFFFFF0FFFL) | (0x0000000000001000L);
    id = new UUID(msb,lsb);
    idString = id.toString();
}

@Id
public UUID getUUID() {
    return id;
}

public String getIdString() {
    return idString;
}

public void setIdString(String idString) {
    this.idString = idString;
    this.id = UUID.fromString(idString);
}

void setUUID(UUID id) {
    this.id = id;
    this.idString = id.toString();
}

@Override
public String toString() {
    return "["+this.getClass().getCanonicalName()+" "+this.getUUID()+"]";
}
}

OrganizationNode.java:

@Entity
public class OrganizationNode extends AbstractEntity {

@ManyToOne(cascade=CascadeType.ALL)
NodeType nodeType;

@Column(nullable=true)
String name;

@OneToMany(cascade=CascadeType.ALL)
Set<OrganizationNode> children;

public OrganizationNode() {}

public OrganizationNode(NodeType nt, String name) {
    this.nodeType = nt;
    this.name = name;
    children = new HashSet<>();
}

public void setNodeType(NodeType nt) {
    nodeType = nt;
}

public NodeType getNodeType() {
   return nodeType;
}

public String getName() { 
    if ((name == null) || (name.equals(""))) return null;
    return name;
} 

public void setName(String name) { 
    this.name = name; 
}

public Set<OrganizationNode> getChildren() {
    return children;
}

public void setChildren(Set<OrganizationNode> children) {
    this.children = children;
} 

public void addNode(OrganizationNode node) {
    children.add(node);
}

public void removeNode(OrganizationNode node) {
    children.remove(node);
}
}

Device.java:

@Entity
public class Device extends OrganizationNode {

Set<Monitor> monitors;

public Device() {
   super();
}

public Device(NodeType nt, String name) {
    super(nt, name);
    monitors = new HashSet<>();
}

public Set<Monitor> getMonitors() {
    return monitors;
}

public void setMonitors(Set<Monitor> monitors) {
    this.monitors = monitors;
}

public void addMonitor(Monitor monitor) {
    monitors.add(monitor);
}

}

Solution

  • You need to decide what inheritance startegy you want to use. The default one is typically the "Single Table Inheritance" so all the subclasses are represented in one table with merged columns.

    @Inheritance
    @Entity
    public class OrganizationNode extends AbstractEntity {
    ...
    }
    

    and you saw it the sql.

    You can have Joined, Multiple Table Inheritance where each subclass has its own table and are joined with parent table:

    @Inheritance(strategy=InheritanceType.JOINED)
    

    Finally, the last option is Table Per Class Inheritance, where there is no "inheritance" tree reflected in the tables structure, and each object has its full table with all the columns from the class and supperclasses.

    @Inheritance(strategy=InheritanceType.TABLE_PER_CLASS)
    

    The last one is the least efficient.

    • You can have only one strategy, which you define on the top of the inheritance (OrganizationNode), it cannot be changed in subclasses.

    • The default single table inheritance is typically the most efficient unless there are really a lot of columns which are not shared between the classes

    • You should probably explicitly declare column which will be used to deteriment the actual class type: @DiscriminatorColumn(name="NODE_TYPE") and for each Entity define the value: @DiscriminatorValue("TYPE1")