Exception when retrieving items modeled by inheritance classes from MongoDB using Datanucleus


Manuel Castillo <contact@...>
 

me and my team are working on an upgrade of our company's system which as getting kind of forgotten and was running old versions of everything it uses; so developing newer features was becoming a pain with newer and unsupported technologies.

So far we have managed to produce an almost fully working version of the system; but we got stuck at a feature which involves Datanucleus-JDO, MongoDB and inheritance.

We have some models which are tremendously simmilar (from the code's prespective). In the current in-production version, to apply a change to it usually involves to rewrite the same piece of code in all classes, so we thought that inheritance would make the job easier and better. So we have two interfaces at the top hirarchy level (which as far we know, Datanuclues nor MongoDB doesn't care about them at all); which go like this:

public interface Entity extends Serializable {

    String getDate();
    double getQty();
    void setQty(double qty);
    void setDate(String date);
    void setKey(Key key);

}

And

public interface HourEntity extends Entity {

    String getHour();

}

We use application defined keys, we use this unique class to build different kind of keys. We only want the toString representation of this class to sotre and retrieve data in Mongo.

public final class Key implements Serializable {
    static final long serialVersionUID = -448150158203091507L;
    public final String targetClassName;
    public final String id;
    public final String toString;
    public final int hashCode;

    public Key() {
        targetClassName = null;
        id = null;
        toString = null;
        hashCode = -1;
    }

    public Key(String str) {
        String[] parts = str.split("\\(");
        parts[1] = parts[1].replaceAll("\\)", " ");
        parts[1] = parts[1].replace("\"", " ");
        parts[1] = parts[1].trim();
        this.targetClassName = parts[0];
        this.id = parts[1];
        toString = this.toString();
        hashCode = this.hashCode();
    }

    public Key(String classCollectionName, String id) {
        if (StringUtils.isEmpty(classCollectionName)) {
            throw new IllegalArgumentException("No collection/class name specified.");
        }
        if (id == null) {
            throw new IllegalArgumentException("ID cannot be null");
        }
        targetClassName = classCollectionName;
        this.id = id;
        toString = this.toString();
        hashCode = this.hashCode();
    }

    public String getTargetClassName() {
        return targetClassName;
    }

    public int hashCode() {
        if(hashCode != -1) return hashCode; 
        int prime = 31;
        int result = 1;
        result = prime * result + (id != null ? id.hashCode() : 0);
        result = prime * result + (targetClassName != null ? targetClassName.hashCode() : 0);
        return result;
    }

    public boolean equals(Object object) {
    if (object instanceof Key) {
        Key key = (Key) object;
        if (this == key)
            return true;
        return targetClassName.equals(key.targetClassName) && Objects.equals(id, key.id);
    } else {
        return false;
    }
}

    public String toString() {
        if(toString != null) return toString;
        StringBuilder buffer = new StringBuilder();
        buffer.append(targetClassName);
         buffer.append("(");
        if (id != null) {
            buffer.append((new StringBuilder()).append("\"").append(id)
                    .append("\"").toString());
        } else {
            buffer.append("no-id-yet");
        }
        buffer.append(")");
        return buffer.toString();
    }

}

This apllication defined identiy is working fine on all other models which does not involve iheritance.

This is one of the actual models that we intend to store in our datastore:

@PersistenceCapable(detachable="true")
@Inheritance(strategy=InheritanceStrategy.COMPLETE_TABLE)
public class Ticket implements Entity {

    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.UNSPECIFIED, column="_id")
    protected Key key;

    protected String date;
    protected int qty;

    public Ticket() {
        this.qty = 0;
    }

    public Key getKey() {
        return key;
    }

    @Override
    public void setKey(Key key) {
        this.key = key;
    }

    public double getQty() {
        return qty;
    }

    public void setQty(double qty) {
        this.qty = (int) qty;
    }

    public String getDate() {
        return date;
    }

    public void setDate(String date) {
        this.date = date;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((key == null) ? 0 : key.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Ticket other = (Ticket) obj;
        if (key == null) {
            if (other.key != null)
                return false;
        } else if (!key.equals(other.key))
            return false;
        return true;
    }

    @Override
    public String toString() {
        return "Ticket [key=" + key + ", date=" + date + ", qty="
                + qty + "]";
    }

}

And this is its subclass (all models which involve this problem just involve one super class and only one children per every super class):

@PersistenceCapable(detachable="true")
@Inheritance(strategy=InheritanceStrategy.COMPLETE_TABLE)
public class HourTicket extends Ticket implements HourEntity {

    private String hour;

    public HourTicket() {
        super();
    }

    public Key getKey() {
        return key;
    }

    @Override
    public void setKey(Key key) {
        this.key = key;
    }

    public String getHour() {
        return hour;
    }

    public void setHour(String hour) {
        this.hour = hour;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((key == null) ? 0 : key.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        HourTicket other = (HourTicket) obj;
        if (key == null) {
            if (other.key != null)
                return false;
        } else if (!key.equals(other.key))
            return false;
        return true;
    }

    @Override
    public String toString() {
        return "HourTicket [key=" + key + ", date=" + date
                + ", hour=" + hour + ", qty=" + qty + "]";
    }

}

Finally, the persisntance.xml is like this

<?xml version="1.0" encoding="UTF-8" ?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
        http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0">

    <!-- JOSAdmin "unit" -->
    <persistence-unit name="ourdatastore">
        <class>mx.ourdomain.Ticket</class>
        <class>mx.ourdomain.HourTicket</class>
        <exclude-unlisted-classes/>

    </persistence-unit>
</persistence>

And package-mongo.orm

<?xml version="1.0"?>
<!DOCTYPE orm SYSTEM "file:/javax/jdo/orm.dtd">
<orm>
    <package name="mx.ourdomain" >
        <class name="Ticket" table="Ticket">
            <field name="key" primary-key="true" >
                <column name="_id" length="100" />
            </field >
        </class>

        <class name="HourTicket" table="HourTicket">
            <primary-key >
                <column name="_id" target="_id" />
            </primary-key>
        </class>
     </package>
</orm>

So, the problems comes when trying to perform any read or write opperations using either the super class or the subclass. This has happned with the same exact results in several (all posible as far we know) scenarios, but the test scenario we are study begins with this call:

Ticket ticket = persistenceManager.getObjectById(Ticket.class, key);

The key is generated with an standard procedure which is used by other models which do store and read successfully; and of course, it is of the previously shown key class.

We've tried to redifine the Key class as a DatastoreId and using KeyTraslator with no success; the further we got was to perform a successful read, but when mapping the values into the model object; the String returned by the traslator was being casted to our Key class; which of course resulted on a ClassCastException.

We tried to use a String from the traslator since the spectrum of data types that the mongo driver can natively support is very narrow, and it includes String, the String representation of our key is the actual desired value to use within mongo (which is done successful on any other classes which does not involve inheritance) and the Datanuclues docs states that (http://www.datanucleus.org/products/datanucleus/jdo/mapping.html#application_identity --> Application Identity : Accessing objects by Identity):

If you are using your own PK class then the mykey value is the toString() form of the identity of your PK class. 

If this means that we have to explicitly call the toString method of our Key class, I think is a bit unclear since the method accepts Object and works fine with the Key object itself in all classes without inheritance.

So, I have the feeling that this might be a bug in datanucleus-mongodb (or just some missconfiguration in our project), because in datanucleus-mongodb v5.1.0-release, class MongoDBUtils, method  getClassNameForIdentity(Object, AbstractClassMetaData, ExecutionContext, ClassLoaderResolver)) there is the following code:

...
BasicDBObject query = new BasicDBObject();
if (rootCmd.getIdentityType() == IdentityType.DATASTORE)
{
    ...
} else if (rootCmd.getIdentityType() == IdentityType.APPLICATION) { if (IdentityUtils.isSingleFieldIdentity(id)) { Object key = IdentityUtils.getTargetKeyForSingleFieldIdentity(id); /// <-- HERE int[] pkNums = rootCmd.getPKMemberPositions(); AbstractMemberMetaData pkMmd = rootCmd.getMetaDataForManagedMemberAtAbsolutePosition(pkNums[0]); String pkPropName = table.getMemberColumnMappingForMember(pkMmd).getColumn(0).getName(); query.put(pkPropName, key); /// <--- AND MAYBE ALSO HERE } ...

The Mongo Java Driver has very few data types which it supports; which of course excludes our Key class, so I think that datanuclues should put into the query the toString() form of the key object above to prevent a  CodecConfigurationException; which is the one broking our code.

So, should I refactor most queries to explicity use the toString of the key, are we missing some configurations, and/or should we contribute with a methot to perform this data type map (calling toString when the key class is not in this list: http://mongodb.github.io/mongo-java-driver/3.7/bson/documents/)

Thanks!

Join main@datanucleus.groups.io to automatically receive all group messages.