Added support for complex object references as specified in

"New Format for Object-Relational Mapping (Version 2)"
This commit is contained in:
hns 2003-04-25 16:08:40 +00:00
parent 20df514693
commit 0cbc25c04a
6 changed files with 281 additions and 60 deletions

View file

@ -237,7 +237,8 @@ public final class TypeManager {
if ((dbmap != null) && dbmap.needsUpdate()) {
dbmap.update();
if ((proto != hopobjectProto) && (proto != globalProto)) {
// this is now done in dbmap.update()!!!
/*if ((proto != hopobjectProto) && (proto != globalProto)) {
// set parent prototype, in case it has changed.
String parentName = dbmap.getExtends();
@ -246,7 +247,7 @@ public final class TypeManager {
} else if (!app.isJavaPrototype(proto.getName())) {
proto.setParentPrototype(hopobjectProto);
}
}
} */
}
}

View file

@ -17,6 +17,7 @@
package helma.objectmodel.db;
import helma.framework.core.Application;
import helma.framework.core.Prototype;
import helma.util.SystemProperties;
import helma.util.Updatable;
import java.sql.*;
@ -159,6 +160,7 @@ public final class DbMapping implements Updatable {
return props.lastModified() != lastTypeChange;
}
/**
* Read the mapping from the Properties. Return true if the properties were changed.
* The read is split in two, this method and the rewire method. The reason is that in order
@ -173,9 +175,6 @@ public final class DbMapping implements Updatable {
// can be stored in this table
prototypeField = props.getProperty("_prototypefield");
// see if this prototype extends (inherits from) any other prototype
extendsProto = props.getProperty("_extends");
dbSourceName = props.getProperty("_db");
if (dbSourceName != null) {
@ -220,17 +219,40 @@ public final class DbMapping implements Updatable {
lastTypeChange = props.lastModified();
// see if this prototype extends (inherits from) any other prototype
extendsProto = props.getProperty("_extends");
if (extendsProto != null) {
parentMapping = app.getDbMapping(extendsProto);
if (parentMapping != null && parentMapping.needsUpdate()) {
parentMapping.update();
}
} else {
parentMapping = null;
}
// set the parent prototype in the corresponding Prototype object!
// this was previously done by TypeManager, but we need to do it
// ourself because DbMapping.update() may be called by other code than
// the TypeManager.
if (typename != null &&
!"global".equalsIgnoreCase(typename) &&
!"hopobject".equalsIgnoreCase(typename)) {
Prototype proto = app.getPrototypeByName(typename);
if (proto != null) {
if (extendsProto != null) {
proto.setParentPrototype(app.getPrototypeByName(extendsProto));
} else if (!app.isJavaPrototype(typename)) {
proto.setParentPrototype(app.getPrototypeByName("hopobject"));
}
}
}
// null the cached columns and select string
columns = null;
columnMap.clear();
selectString = insertString = updateString = null;
if (extendsProto != null) {
parentMapping = app.getDbMapping(extendsProto);
}
// if (tableName != null && dbSource != null) {
// app.logEvent ("set data dbSource for "+typename+" to "+dbSource);
HashMap p2d = new HashMap();
HashMap d2p = new HashMap();
@ -259,7 +281,15 @@ public final class DbMapping implements Updatable {
if ((rel.columnName != null) &&
((rel.reftype == Relation.PRIMITIVE) ||
(rel.reftype == Relation.REFERENCE))) {
d2p.put(rel.columnName.toUpperCase(), rel);
Relation old = (Relation) d2p.put(rel.columnName.toUpperCase(), rel);
// check if we're overwriting another relation
// if so, primitive relations get precendence to references
if (old != null) {
app.logEvent("*** Duplicate mapping for "+typename+"."+rel.columnName);
if (old.reftype == Relation.PRIMITIVE) {
d2p.put(old.columnName.toUpperCase(), old);
}
}
}
// app.logEvent ("Mapping "+propName+" -> "+dbField);

View file

@ -0,0 +1,124 @@
/*
* Helma License Notice
*
* The contents of this file are subject to the Helma License
* Version 2.0 (the "License"). You may not use this file except in
* compliance with the License. A copy of the License is available at
* http://adele.helma.org/download/helma/license.txt
*
* Copyright 1998-2003 Helma Software. All Rights Reserved.
*
* $RCSfile$
* $Author$
* $Revision$
* $Date$
*/
package helma.objectmodel.db;
import java.io.Serializable;
import java.util.Map;
/**
* This is the internal representation of a database key with multiple
* columns. It is constructed from the logical table (type) name and the
* column name/column value pairs that identify the key's object
*
* NOTE: This class doesn't fully support the Key interface - getID always
* returns null since there is no unique key (at least we don't know about it).
*/
public final class MultiKey implements Key, Serializable {
// the name of the prototype which defines the storage of this object.
// this is the name of the object's prototype, or one of its ancestors.
// If null, the object is stored in the embedded db.
private final String storageName;
// the id that defines this key's object within the above storage space
private final Map parts;
// lazily initialized hashcode
private transient int hashcode = 0;
/**
* make a key for a persistent Object, describing its datasource and key parts.
*/
public MultiKey(DbMapping dbmap, Map parts) {
this.parts = parts;
this.storageName = (dbmap == null) ? null : dbmap.getStorageTypeName();
}
/**
*
*
* @param what the other key to be compared with this one
*
* @return true if both keys are identical
*/
public boolean equals(Object what) {
if (what == this) {
return true;
}
if (!(what instanceof MultiKey)) {
return false;
}
MultiKey k = (MultiKey) what;
// storageName is an interned string (by DbMapping, from where we got it)
// so we can compare by using == instead of the equals method.
return (storageName == k.storageName) &&
((parts == k.parts) || parts.equals(k.parts));
}
/**
*
*
* @return this key's hash code
*/
public int hashCode() {
if (hashcode == 0) {
hashcode = (storageName == null) ? (17 + (37 * parts.hashCode()))
: (17 + (37 * storageName.hashCode()) +
(+37 * parts.hashCode()));
}
return hashcode;
}
/**
*
*
* @return the key of this key's object's parent object
*/
public Key getParentKey() {
return null;
}
/**
*
*
* @return the unique storage name for this key's object
*/
public String getStorageName() {
return storageName;
}
/**
*
*
* @return this key's object's id
*/
public String getID() {
return null;
}
/**
*
*
* @return a string representation for this key
*/
public String toString() {
return (storageName == null) ? ("[" + parts + "]") : (storageName + "[" + parts + "]");
}
}

View file

@ -1821,7 +1821,7 @@ public final class Node implements INode, Serializable {
}
// so if we have a property relation and it does in fact link to another object...
if ((propRel != null) && propRel.isCollection()) {
if ((propRel != null) && (propRel.isCollection() || propRel.isComplexReference())) {
// in some cases we just want to create and set a generic node without consulting
// the NodeManager if it exists: When we get a collection (aka virtual node)
// from a transient node for the first time, or when we get a collection whose
@ -1835,9 +1835,9 @@ public final class Node implements INode, Serializable {
}
// if this is from relational database only fetch if this node
// is itself persistent.
else if ((state != TRANSIENT) && propRel.createPropertyOnDemand()) {
else if ((state != TRANSIENT) && propRel.createOnDemand()) {
// this may be a relational node stored by property name
try {
// try {
Node pn = nmgr.getNode(this, propname, propRel);
if (pn != null) {
@ -1850,9 +1850,9 @@ public final class Node implements INode, Serializable {
prop = new Property(propname, this, pn);
}
} catch (RuntimeException nonode) {
// } catch (RuntimeException nonode) {
// wasn't a node after all
}
// }
}
}
}
@ -2367,6 +2367,15 @@ public final class Node implements INode, Serializable {
String p2 = propname.toLowerCase();
Relation rel = (dbmap == null) ? null : dbmap.getPropertyRelation(propname);
if (rel != null && rel.isComplexReference()) {
rel.setConstraints(this, n);
Key key = new MultiKey(n.getDbMapping(), rel.getKeyParts(this));
nmgr.nmgr.registerNode(n, key);
return;
}
Property prop = (propMap == null) ? null : (Property) propMap.get(p2);
if (prop != null) {
@ -2389,8 +2398,6 @@ public final class Node implements INode, Serializable {
prop.setNodeValue(n);
Relation rel = (dbmap == null) ? null : dbmap.getPropertyRelation(propname);
if ((rel == null) || (rel.reftype == Relation.REFERENCE) || rel.virtual ||
(rel.otherType == null) || !rel.otherType.isRelational()) {
// the node must be stored as explicit property

View file

@ -270,7 +270,10 @@ public final class NodeManager {
Key key = null;
// check what kind of object we're looking for and make an apropriate key
if (rel.virtual || (rel.groupby != null) || !rel.usesPrimaryKey()) {
if (rel.isComplexReference()) {
// a key for a complex reference
key = new MultiKey(rel.otherType, rel.getKeyParts(home));
} else if (rel.virtual || (rel.groupby != null) || !rel.usesPrimaryKey()) {
// a key for a virtually defined object that's never actually stored in the db
// or a key for an object that represents subobjects grouped by some property, generated on the fly
key = new SyntheticKey(home.getKey(), kstr);
@ -395,6 +398,14 @@ public final class NodeManager {
cache.put(node.getKey(), node);
}
/**
* Register a node in the node cache using the key argument.
*/
protected void registerNode(Node node, Key key) {
cache.put(key, node);
}
/**
* Remove a node from the node cache. If at a later time it is accessed again,
* it will be refetched from the database.
@ -1523,7 +1534,7 @@ public final class NodeManager {
DbColumn[] columns = dbm.getColumns();
StringBuffer q = dbm.getSelect();
if (home.getSubnodeRelation() != null) {
if (home.getSubnodeRelation() != null && !rel.isComplexReference()) {
// combine our key with the constraints in the manually set subnode relation
q.append("WHERE ");
q.append(rel.accessName);

View file

@ -21,6 +21,8 @@ import helma.objectmodel.*;
import java.sql.SQLException;
import java.util.Properties;
import java.util.Vector;
import java.util.Map;
import java.util.HashMap;
/**
* This describes how a property of a persistent Object is stored in a
@ -41,6 +43,10 @@ public final class Relation {
// a 1-to-many relation, a field in another table points to objects of this type
public final static int COLLECTION = 2;
// a 1-to-1 reference with multiple or otherwise not-trivial constraints
// this is managed differently than REFERENCE, hence the separate type.
public final static int COMPLEX_REFERENCE = 3;
// direct mapping is a very powerful feature: objects of some types can be directly accessed
// by one of their properties/db fields.
// public final static int DIRECT = 3;
@ -141,6 +147,12 @@ public final class Relation {
throw new RuntimeException("DbMapping for " + proto +
" not found from " + ownType.typename);
}
// make sure the type we're referring to is up to date!
if (otherType != null && otherType.needsUpdate()) {
otherType.update();
}
} else {
virtual = false;
columnName = desc;
@ -148,13 +160,7 @@ public final class Relation {
}
}
String rdonly = props.getProperty(propName + ".readonly");
if ((rdonly != null) && "true".equalsIgnoreCase(rdonly)) {
readonly = true;
} else {
readonly = false;
}
readonly = "true".equalsIgnoreCase(props.getProperty(propName + ".readonly"));
isPrivate = "true".equalsIgnoreCase(props.getProperty(propName + ".private"));
@ -167,6 +173,15 @@ public final class Relation {
constraints = new Constraint[newConstraints.size()];
newConstraints.copyInto(constraints);
// check if this is a non-trivial reference
if (reftype == REFERENCE) {
if (constraints.length > 1 || !usesPrimaryKey()) {
reftype = COMPLEX_REFERENCE;
}
}
// if DbMapping for virtual nodes has already been created,
// update its subnode relation.
// FIXME: needs to be synchronized?
@ -248,21 +263,18 @@ public final class Relation {
String foreign = props.getProperty(propName + ".foreign");
if ((local != null) && (foreign != null)) {
cnst.addElement(new Constraint(local, otherType.getTableName(), foreign, false));
cnst.addElement(new Constraint(local, foreign, false));
columnName = local;
}
// parse additional contstraints
// parse additional contstraints from *.1 to *.9
for (int i=1; i<10; i++) {
local = props.getProperty(propName + ".local."+i);
foreign = props.getProperty(propName + ".foreign."+i);
if ((local != null) && (foreign != null)) {
cnst.addElement(new Constraint(local, otherType.getTableName(), foreign, false));
} else {
break;
cnst.addElement(new Constraint(local, foreign, false));
}
}
}
@ -296,6 +308,13 @@ public final class Relation {
return reftype == COLLECTION;
}
/**
* Returns true if this Relation describes a complex object reference property
*/
public boolean isComplexReference() {
return reftype == COMPLEX_REFERENCE;
}
/**
* Tell wether the property described by this relation is to be handled as private, i.e.
* a change on it should not result in any changed object/collection relations.
@ -310,8 +329,8 @@ public final class Relation {
* node. Virtual nodes are objects which are only generated on demand
* and never stored to a persistent storage.
*/
public boolean createPropertyOnDemand() {
return virtual || (accessName != null) || (groupby != null);
public boolean createOnDemand() {
return virtual || (accessName != null) || (groupby != null) || isComplexReference();
}
/**
@ -392,7 +411,9 @@ public final class Relation {
public boolean usesPrimaryKey() {
if (otherType != null) {
if (reftype == REFERENCE) {
return (constraints.length == 1) && constraints[0].foreignKeyIsPrimary();
return constraints.length == 0 ||
((constraints.length == 1) &&
constraints[0].foreignKeyIsPrimary());
}
if (reftype == COLLECTION) {
@ -523,7 +544,7 @@ public final class Relation {
vr.prototype = groupbyPrototype;
vr.filter = filter;
vr.constraints = constraints;
vr.addConstraint(new Constraint(null, null, groupby, true));
vr.addConstraint(new Constraint(null, groupby, true));
vr.aggressiveLoading = aggressiveLoading;
vr.aggressiveCaching = aggressiveCaching;
@ -544,7 +565,7 @@ public final class Relation {
vr.prototype = groupbyPrototype;
vr.filter = filter;
vr.constraints = constraints;
vr.addConstraint(new Constraint(null, null, groupby, true));
vr.addConstraint(new Constraint(null, groupby, true));
return vr;
}
@ -558,7 +579,7 @@ public final class Relation {
StringBuffer q = new StringBuffer();
String prefix = pre;
if (kstr != null) {
if (kstr != null && !isComplexReference()) {
q.append(prefix);
String accessColumn = (accessName == null) ? otherType.getIDField() : accessName;
@ -669,16 +690,14 @@ public final class Relation {
if (propname != null) {
INode home = constraints[i].isGroupby ? parent
: parent.getNonVirtualParent();
String localName = constraints[i].localName;
String value = null;
if ((localName == null) ||
localName.equalsIgnoreCase(ownType.getIDField())) {
if (constraints[i].localKeyIsPrimary()) {
value = home.getID();
} else if (ownType.isRelational()) {
value = home.getString(ownType.columnNameToProperty(localName));
value = home.getString(constraints[i].localProperty());
} else {
value = home.getString(localName);
value = home.getString(constraints[i].localName);
}
if ((value != null) && !value.equals(child.getString(propname))) {
@ -704,14 +723,19 @@ public final class Relation {
continue;
}
Relation crel = otherType.columnNameToRelation(constraints[i].foreignName);
// check if we update the local or the other object, depending on
// whether the primary key of either side is used.
if (constraints[i].foreignKeyIsPrimary()) {
parent.setString(constraints[i].localProperty(), child.getID());
continue;
}
Relation crel = otherType.columnNameToRelation(constraints[i].foreignName);
if (crel != null) {
// INode home = constraints[i].isGroupby ? parent : nonVirtual;
String localName = constraints[i].localName;
if ((localName == null) ||
localName.equalsIgnoreCase(ownType.getIDField())) {
if (constraints[i].localKeyIsPrimary()) {
// only set node if property in child object is defined as reference.
if (crel.reftype == REFERENCE) {
INode currentValue = child.getNode(crel.propName);
@ -733,10 +757,9 @@ public final class Relation {
Property prop = null;
if (ownType.isRelational()) {
prop = home.getProperty(ownType.columnNameToProperty(localName));
// value = home.getString(ownType.columnNameToProperty(localName));
prop = home.getProperty(constraints[i].localProperty());
} else {
prop = home.getProperty(localName);
prop = home.getProperty(constraints[i].localName);
}
if (prop != null) {
@ -747,6 +770,21 @@ public final class Relation {
}
}
/**
* Returns a map containing the key/value pairs for a specific Node
*/
public Map getKeyParts(INode home) {
Map map = new HashMap();
for (int i=0; i<constraints.length; i++) {
if (ownType.getIDField().equals(constraints[i].localName)) {
map.put(constraints[i].foreignName, home.getID());
} else {
map.put(constraints[i].foreignName, home.getString(constraints[i].localProperty()));
}
}
return map;
}
// a utility method to escape single quotes
String escape(String str) {
if (str == null) {
@ -780,13 +818,20 @@ public final class Relation {
*/
public String toString() {
String c = "";
String spacer = "";
if (constraints != null) {
for (int i = 0; i < constraints.length; i++)
c = " constraints: ";
for (int i = 0; i < constraints.length; i++) {
c += spacer;
c += constraints[i].toString();
spacer = ", ";
}
}
return "Relation[" + ownType + "." + propName + ">" + otherType + "]" + c;
String target = otherType == null ? columnName : otherType.toString();
return "Relation " + ownType+"."+propName + " -> " + target + c;
}
/**
@ -795,13 +840,11 @@ public final class Relation {
*/
class Constraint {
String localName;
String tableName;
String foreignName;
boolean isGroupby;
Constraint(String local, String table, String foreign, boolean groupby) {
Constraint(String local, String foreign, boolean groupby) {
localName = local;
tableName = table;
foreignName = foreign;
isGroupby = groupby;
}
@ -837,6 +880,11 @@ public final class Relation {
foreignName.equalsIgnoreCase(otherType.getIDField());
}
public boolean localKeyIsPrimary() {
return (localName == null) ||
localName.equalsIgnoreCase(ownType.getIDField());
}
public String foreignProperty() {
return otherType.columnNameToProperty(foreignName);
}
@ -846,7 +894,7 @@ public final class Relation {
}
public String toString() {
return ownType + "." + localName + "=" + tableName + "." + foreignName;
return localName + "=" + otherType.getTypeName() + "." + foreignName;
}
}
}