Application identity ("custom keys") might be breaking queries for inheritance classes


Manuel Castillo <contact@...>
 

Hello, I'm having a trouble when performing any kind of queries into collections mapped to classes with inheritance.

I'm using datanucleus JDO with MongoDB. The key class I'm are using is like this:
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 identity model is working fine on all other classes which don't involve inheritance. Examples of the models which are giving trouble are; the supper class:
@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 the subclass:
@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 + "]";
    }

}
Our persistence.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 is:
<?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>
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);
I followed the debbuger to see if there was some obvious misconfugration going on. I already tried to use KeyTraslators and DatastoreIds, but every time we got the same result.

I have the feeling that this could be a bug, since there is this piece of code in the datanuclues-mongodb 5.1.0-release in the method getClassNameForIdentity(Object, AbstractClassMetaData, ExecutionContext, ClassLoaderResolver)):
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 HERE
}
....

The type of key is Object; but the spectrum of data types that Mongo BasicDBObject is very narrow and ofcourse it excludes the Key type we've defined; hence, it need a Coded and throws a CodecConfigurationException which is the one breaking the code. Shouldn't the key use the toString of the object when its class its not supported by the mongo driver? (http://mongodb.github.io/mongo-java-driver/3.1/bson/documents/) It is stated in the Datanuclues docs that when using application managed keys the toString form of it will be used so... are I wrong or did I found a bug?

Thanks

I 've already checked that


Andy
 
Edited

Hi,

Take a step backwards. How do you think DataNucleus knows how to persist a field of type `Key`? It doesn't, unless you tell it. You don't define that as an "objectIdClass" for that persistable class, and you don't define an `@AttributeConverter` for that `Key` type, so it knows not. Defining an `@AttributeConverter` to convert `Key` to String would be the normal way of handling that. Inheritance has nothing to do with that.

I also don't see any reference to the LOG which tells you what MongoDB query it performs for your `getObjectById` call, or indeed what MongoDB call is made to do the INSERT of the object in the first place. They would likely reveal what it is trying to do with it.


Manuel Castillo <contact@...>
 

thank you Andy, using objectIdClass metadata on super classes solved the problem. I'm not sure if inheritance is not involved at all; performing some minor changes to the Key class (removing the hashCode and toString attributes and the final qualifiers) and to the super classes (replacing the Key key attribute for String id as in the Key class; with some minor modifications to the Key getters and setters) did the trick.

No modifications were necessary on any other classes which does not involve inheritance, DAOs or anywhere else.

I'm sorry for not adding any LOG reference, it seemed to me pretty straightfoward that adding a non-recognized data type on a Mongo's BasicDBOject will result in an exception; the code that follows the snippet I copied is:
DBObject foundObj = dbColl.findOne(query);

Which will result in a CodecConfigurationException when the key object class is not recognized by Mongo.

But after all, there is a way to get this to work, which is really good; but I'm not sure if the code should go all the way there with an invalid key (for mongo) or not wrapping the exception and telling that a objectId and/or attribute converter should be used.