diff --git a/src/helma/framework/core/TypeManager.java b/src/helma/framework/core/TypeManager.java index 9f84c062..988ea6ef 100644 --- a/src/helma/framework/core/TypeManager.java +++ b/src/helma/framework/core/TypeManager.java @@ -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); } - } + } */ } } diff --git a/src/helma/objectmodel/db/DbMapping.java b/src/helma/objectmodel/db/DbMapping.java index a4487f8e..77339311 100644 --- a/src/helma/objectmodel/db/DbMapping.java +++ b/src/helma/objectmodel/db/DbMapping.java @@ -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); diff --git a/src/helma/objectmodel/db/MultiKey.java b/src/helma/objectmodel/db/MultiKey.java new file mode 100644 index 00000000..5e72f03e --- /dev/null +++ b/src/helma/objectmodel/db/MultiKey.java @@ -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 + "]"); + } +} diff --git a/src/helma/objectmodel/db/Node.java b/src/helma/objectmodel/db/Node.java index ea663e71..e60d90ec 100644 --- a/src/helma/objectmodel/db/Node.java +++ b/src/helma/objectmodel/db/Node.java @@ -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 diff --git a/src/helma/objectmodel/db/NodeManager.java b/src/helma/objectmodel/db/NodeManager.java index d40c7d7a..1649e052 100644 --- a/src/helma/objectmodel/db/NodeManager.java +++ b/src/helma/objectmodel/db/NodeManager.java @@ -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); diff --git a/src/helma/objectmodel/db/Relation.java b/src/helma/objectmodel/db/Relation.java index bde04c9b..f02d4c66 100644 --- a/src/helma/objectmodel/db/Relation.java +++ b/src/helma/objectmodel/db/Relation.java @@ -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) { @@ -448,13 +469,13 @@ public final class Relation { return null; } - // if the collection node is prototyped, return the app's DbMapping + // if the collection node is prototyped, return the app's DbMapping // for that prototype if (prototype != null) { return otherType; } - // create a synthetic DbMapping that describes how to fetch the + // create a synthetic DbMapping that describes how to fetch the // collection's child objects. if (virtualMapping == null) { virtualMapping = new DbMapping(ownType.app); @@ -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" + 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; } } }