使用 Datanucleus 从 MongoDB 中检索通过继承 类 建模的项目时出现异常

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

我和我的团队正在对我们公司的系统进行升级,该系统有点被遗忘了,而且它使用的所有东西都是 运行 旧版本;因此,开发更新的功能正在成为使用更新的和不受支持的技术的痛苦。

到目前为止,我们已经成功地生成了一个几乎完全可用的系统版本;但我们陷入了涉及 Datanucleus-JDO、MongoDB 和继承的功能。

我们有一些非常相似的模型(从代码的角度来看)。在当前的 in-production 版本中,对其应用更改通常涉及在所有 class 中重写同一段代码,因此我们认为继承会使工作变得更容易和更好。所以我们在最高层级有两个接口(据我们所知,Datanuclues 和 MongoDB 根本不关心它们);像这样:

public interface Entity extends Serializable {

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

}

public interface HourEntity extends Entity {

    String getHour();

}

我们使用应用程序定义的键,我们使用这个独特的 class 来构建不同类型的键。我们只希望此 class 的 toString 表示形式在 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();
    }

}

此应用程序定义的身份在所有其他不涉及继承的模型上工作正常。

这是我们打算存储在数据存储中的实际模型之一:

@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 + "]";
    }

}

这是它的子class(涉及这个问题的所有模型只涉及一个超级class,每个超级class只涉及一个children):

@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 + "]";
    }

}

最后,persisntance.xml是这样的

<?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>

和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>

因此,当尝试使用超级 class 或子 class 执行任何读取或写入操作时,问题就来了。这在几个(据我们所知都是可能的)场景中发生了相同的结果,但我们正在研究的测试场景是从这个调用开始的:

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

密钥是通过标准程序生成的,其他模型使用该程序成功存储和读取;当然,它是之前显示的密钥 class.

除此之外,我们已经调试了 datanucleus 任务。我们发现正如预期的那样:

  1. 元数据显示它是其他人的超级 class。
  2. 它使用应用程序管理的密钥。

但是当尝试获取 class 名称以确定哪个是要查询的正确 Mongo 集合时,datanucleus-mongodb 尝试同时查询 classes(Ticket 和小时票);但随后它向 mongo 驱动程序处理 key object perse,然后是 CodecConfigurationException 被抛出,因为 mongo 不知道如何使用键 class (在构建查询时,datanucleus-mongo 创建一个具有结构的 BasicDBObject {_id:key},因为有key条目,所以没有编解码器无法构造。这发生在MongoDBUtils class处datanucleus-mongodb 项目 v5.1.0;class MongoDBUtils,方法 getClassNameForIdentity(Object,AbstractClassMetaData,ExecutionContext,ClassLoaderResolver))。

因此,我们假设缺少一些配置来告诉 datanucleus 它应该使用 toString() 形式的键;因为 Monogo 驱动程序可以很好地处理字符串(datanuclues 文档实际上指出,当使用自定义 classes 作为数据存储键时,它将使用键的 toString() 形式;所以我不确定这是否是一个错误) .

我们已尝试使用 KeyTraslator 插件并使键 class 成为 DatastoreId 并包装在 StringId 中但没有成功:同样的异常被触发,除了将键 class 包装在一个 StringId:mongo 讲座是成功的,但是当试图构建模型 object 时,会抛出一个 ClassCastException,因为 String 不能转换成 Key,并且重构代码以使用 String key 会严重中断数据库中已有数据;因为它有一个特殊的格式,密钥 class 可以读取和生成。

我们在使用 datanucleus JDO 继承时是否遗漏了什么 w/mongoDB?

我没有太注意 objectIdClass 元数据的设置;因为从文档中我了解到它们仅适用于组合键。结果是,如果您定义一个只有一个属性的 objectId class;然后它表现为自定义 SingleFieldId;这就是我们想要的。

我发现 "funny" 未注释(或 objectIdClass 的未声明元数据)classes 可以正常工作并且使用的自定义密钥会受到威胁;但是一旦你将它们中的任何一个设为超级 class,那么你就有义务添加 objectIdClass 元数据。

除了使用 objectIdClass 注释 Ticket class(以及所有其他超级 classes),我们:

  • 从 Key class 中删除了 toString 和 hashCode 属性(@NotPersistent 和 transient 关键字不会让 Datanucleus 忽略它们;所以我猜 toString() 和 hashCode() 方法没有性能改进现在在自定义键上)。
  • 从键 class 属性中删除了所有 final 限定符(Datanucleus 文档没有说自定义键字段不能是最终的;但猜猜看,它们可以不会)
  • 更改了 Key key class 来自所有 superclass 的 String id 成员(如键 class)。我们还必须更改 id 成员的 getter 和 setter 的实现;在调用方法时使用键 class 所需的字符串构造函数来构建键。当然在package-mongo.orm中声明的"key"字段在superclasses.
  • 中改成了id

就是这样!通过这些小改动,我们的系统运行良好;其他持久性 classes 和 DAO 都不需要其他更改。