使用 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 任务。我们发现正如预期的那样:
- 元数据显示它是其他人的超级 class。
- 它使用应用程序管理的密钥。
但是当尝试获取 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 都不需要其他更改。
我和我的团队正在对我们公司的系统进行升级,该系统有点被遗忘了,而且它使用的所有东西都是 运行 旧版本;因此,开发更新的功能正在成为使用更新的和不受支持的技术的痛苦。
到目前为止,我们已经成功地生成了一个几乎完全可用的系统版本;但我们陷入了涉及 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 任务。我们发现正如预期的那样:
- 元数据显示它是其他人的超级 class。
- 它使用应用程序管理的密钥。
但是当尝试获取 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 都不需要其他更改。