TriActive JDO (currently) supports two basic types of collection fields in PersistenceCapableClasses: Set and Map. The JDO specification requires that Lists also be supported; this will be added in a future release 1.
Set fields can be declared as java.util.Collection
,
java.util.Set
, or any of the concrete Set classes supported by
TJDO.
The only concrete type currently supported is java.util.HashSet
.
Similarly, Map fields can be declared as java.util.Map
or any of
the concrete Map classes supported by TJDO.
The only concrete types currently supported are java.util.HashMap
and java.util.Hashtable
.
There is no practical difference between declaring a field as Set vs. HashSet 2. A Set field will be populated by TJDO with a object which is a subclass of HashSet. Similarly for Map vs. HashMap.
Collection fields are one of the few areas in JDO that require additional metadata to be provided. A JDO implementation needs to know the types of objects stored in a collection in order to properly organize storage for them in the database. The new generics facility in Java 5 allows the content type(s) of collections to be declared in the Java code itself but JDO is not defined (yet) to integrate with generics.
The additional information that must be provided is:
The specified types can be classes or interfaces, and do not themselves have to be persistence-capable classes. However, at runtime the actual class of objects stored in a Set or Map must be:
instanceof
the allowed type and
Collection metadata is best illustrated with some examples. Here is a typical Set which represents a many-to-many relationship.
Here, Suppliers can supply many Parts and Parts can have many Suppliers. The relationship is navigable in one direction, from the side where the Set is defined. In other words, we can enumerate all Parts for a given Supplier, but not all Suppliers for a given Part (at least without resorting to a JDO Query). The Supplier class is said to be the "owner" in the relationship. The term "owner" here doesn't imply ownership of the set elements in any enforced sense. In TJDO the two sides of a Set relationship are simply termed "owner" and "element".
TJDO will use database constraints to enforce the semantics of a Set, namely that a given Part will occur in a Supplier.parts set no more than once.
Here is a typical Map. Maps are an n-ary relationship which by default are many-to-many in all but one direction:
Here, Courses have a teachersByStudent map in which Student functions as a key and Teacher is the corresponding value. The cardinalities in the diagram show that:
TJDO will use database constraints to enforce the semantics of a Map, namely that a given Student will occur in a teachersByStudent map no more than once.
By default, every collection field in TJDO is backed in the database by a join table. The join table is dedicated to storing just that relationship, using foreign keys to refer to First-Class objects or storing Second-Class objects directly.
Each collection field is considered to be a separate relationship and will have its own backing storage. In the above example, if we added a Part.suppliers field it would be backed by storage separate from Suppliers.parts. A Part added to Supplier.parts would not cause that Supplier to appear in the corresponding Part.suppliers set. See Inverse Relationships below for how to setup such a correspondence.
Normally, the backing store for a collection is automatically cleared by TJDO when the object which owns the collection is deleted. This automatic clear is done to prevent a foreign key constraint violation, otherwise users would have to code calls to clear() in the jdoPreDelete() method of every class having a collection field.
Collections are cleared exactly as though their clear() method had been called. The clear() occurs after jdoPreDelete() has been called but before the SQL DELETE that deletes the owning object. Any elements that were in the collection are not removed from the database, they are only removed from the collection.
Users that choose to fine-tune their schema DDL can get a performance boost by using cascaded or triggered DELETE to clear collections. On some DBMS's this is done by adding:
ON DELETE CASCADE
to the relevant foreign key constraint(s). Others use database triggers for the same purpose. In either case, to realize the benefit of cascaded deletes for a collection its automatic clear must be disabled. This is done by adding the following metadata extension in the <collection> or <map> element:
<extension vendor-name="triactive" key="clear-on-delete" value="false">
Setting this option delegates responsibility for clearing the collection to the DBMS. It does not affect the DDL auto-generated by TJDO; you must manually modify (and therefore manually execute) your DDL in order to use cascaded or triggered deletes (this may be changed in future versions).
An inverse relationship is a TJDO extension which allows an existing relationship to be inverted, producing a collection as viewed from the opposite side. An inverted relationship shares its backing storage with the existing relationship, so updates through a field on one side are reflected in the corresponding field on the other side.
An inverse relationship can be used:
The relationship being inverted is defined by an existing collection or reference field.
The following TriActive extensions are used to describe inverse relationships. All metadata extensions use the vendor name "triactive":
<extension vendor-name="triactive" key="key" value="value">
and they must be placed in the correct location in the JDO metadata in order to work.
Cardinality | Type | Where to add extension | key | value |
---|---|---|---|---|
Many-To-Many | Set | <collection> for the inverse Set field | "inverse-field" | Name of setField in element class |
Map | <map> for the inverse Map field | "inverse-field" | Name of mapField in key class | |
One-To-Many | Set | <collection> for owner.setField | "owner-field" | Name of ownerField in element class |
<field> for element.ownerField | "collection-field" | Name of setField in owner class | ||
Map | <map> for owner.mapField | "owner-field" | Name of ownerField in value class | |
"key-field" | Name of keyField in value class | |||
<field> for value.ownerField | "map-field" | Name of mapField in owner class |
Note the symmetry in the extensions for one-to-many relationships. For Sets, the <collection> and <field> extensions must refer to each other. For maps, the <map> and <field> extensions must refer to each other.
The usage of all these extensions is best illustrated with some examples.
Consider the previous Suppliers and Parts example. The Supplier.parts field can be inverted and viewed as Part.suppliers. All that's needed is to add the "inverse-field" extension to the inverted side.
The relationship is now navigable in both directions. The two sets represent two sides of the same relationship and are backed by the same storage, so updates to one set are automatically reflected in the other.
An many-to-many set can be inverted even when the element-type is not a specific PersistenceCapable class. For example, if the element-type of Supplier.parts were declared to be Object then the set could of course contain any object, not just Parts. However we can still define a Part.suppliers set as its inverse to get the associated Suppliers for those objects which are in fact Parts. It follows that a many-to-many set can be inverted from multiple classes, so long as each is a subtype of the original element-type.
A similar inversion approach allows us to define both sides of a one-to-many relationship. Consider the usual example of Orders which contain OrderLines. Here's the model before any Set has been defined.
Each OrderLine has one and only one Order, but there is no navigability from orders to their associated lines. If we were to add a normal Order.lines Set field it would get its own backing storage, which would be uncoordinated with the OrderLine.order field. Both would have to be initialized or updated individually, and there would be nothing to guarantee that the two stayed in sync.
Instead we add an Order.lines field as an inverse one-to-many set. The set is the "inverse" of the OrderLine.order field. The "owner-field" and "collection-field" extensions define the inverse relationship.
The relationship is now navigable in both directions. The line items associated with a given Order can be operated upon as though they were stored in a separate collection, although no such separate storage actually exists. TJDO simply provides a Set-like "view" of the element objects.
Because no separate storage exists, the various operations that can be performed on a one-to-many set have unique properties, in particular the operations that modify the set. The following table compares the behavior of the basic Set operations between M:M and 1:M sets (all other Set methods are implemented in terms of these basic operations).
Method | M:M Set | 1:M Set |
---|---|---|
iterator() |
Query for all rows in the set's join table whose owner column equals the set's owner. | Query for all rows in the element table whose "owner-field" equals the set's owner. |
size() |
Count all rows in the set's join table whose owner column equals the set's owner. | Count all rows in the element table whose "owner-field" equals the set's owner. |
contains() |
Test for a row in the set's join table having the given {owner, element} pair. | Test for a row in the element table whose "owner-field" equals the set's owner and whose ID column equals the given element's ID. |
add() |
Insert a new row in the set's join table containing the given {owner, element}. | Update the element's "owner-field" to equal the set's owner, and insert a new row in the element table. |
remove() |
Delete any row from the set's join table equalling the given {owner, element}. | If the element's "owner-field" is nullable, set it to null. Otherwise delete its row from the element table. |
clear() |
Delete all rows from the set's join table whose owner column equals the set's owner. | If the element's "owner-field" is nullable, set it to null in all rows where it currently equals the set's owner. Otherwise delete those rows from the element table. |
Note that, for example, the act of adding an element to a one-to-many set may cause it to mutate, in that the "owner-field" may be updated with a new value. If it does mutate (and its previous "owner-field" value was non-null), the element is in effect removed from the corresponding one-to-many set of the object that owned it previously (in this case, "corresponding" means governed by the same "owner-field").
For example, if an OrderLine is added to the lines in order O1, and then it is subsequently added to the set for order O2, it is effectively removed from the set in order O1, because the owner field can only hold one value at a time. From a Java perspective this may be surprising, but it is a nonetheless desirable consequence of the fact that the relationship has been defined as one-to-many and that cardinality is being enforced.
Manipulating the owner field directly affects set membership in the same way. If the owner field is changed the element is in effect removed from the set of the previous owner and added to the set of the new owner.
"Inverting" a normal many-to-many Map exchanges the roles of owner and key in the map's backing storage. Consider the previous example of Courses,Students and Teachers. The Course.teachersByStudent field can be inverted and viewed as Student.teachersByCourse. The roles of Course and Student are reversed and Course functions as a key with respect to the inverted map.
Like Sets, all that's needed is to add the "inverse-field" extension to the inverted side.
The mapping relationship is now navigable from Student objects as well as from Course objects. The two maps are backed by the same storage, so updates to one map are automatically reflected in the other.
To illustrate an inverse one-to-many map, consider first the following model prior to the addition of any map fields. There are two one-to-many relationships. Each Player object has exactly one Team and one Position (in this example).
If we further declare that every Position on a Team must be unique then there is an implicit three-way relationship that can be modeled as a Map. We can add a playersByPosition field to Team as an inverse one-to-many map. Team functions as the "owner" of the map and Position functions as a "key". The "owner-field", "key-field" and "map-field" extensions define the three-way relationship.
Being a 1:M map, the playersByPosition field occupies no additional storage in the database. TJDO simply provides a Map-like "view" of the Player objects. Furthermore, by explicitly declaring a Map an appropriate database constraint will be added to the Player table to enforce the semantics of a Map (in this case, guaranteeing the uniqueness of every [Team,Position] pair).
In similar fashion we could add a Position.playersByTeam map. That would exchange the owner and key roles so that Position is owner and Team is key, but only as viewed from that map. Both maps can exist concurrently, and both would be defined over the same backing storage; namely, the two fields in Player. The additional map would simply allow navigation from a Position object.
Like one-to-many sets, one-to-many maps have special behavior on update. The following table compares the behavior of the basic Map operations between M:M and 1:M maps (all other Map methods are implemented in terms of these basic operations).
Method | M:M Map | 1:M Map |
---|---|---|
containsKey() |
Test for a row in the map's join table having the given {owner, key} pair. | Test for a row in the value table whose "owner-field" equals the map's owner and whose "key-field" equals the given key. |
containsValue() |
Test for a row in the map's join table having the given {owner, value} pair. | Test for a row in the value table whose "owner-field" equals the map's owner and whose ID column equals the given value's ID. |
get() |
Select the value column from the row in the map's join table having the given {owner, key} pair. | Select the row from the value table whose "owner-field" equals the map's owner and whose "key-field" equals the given key. |
put() |
Insert a new row in the map's join table containing the given {owner, key, value}. | Update the value's "owner-field" to equal the map's owner and the "key-field" to equal the given key. |
remove() |
Delete any row from the map's join table equalling the given {owner, key}. | If the value's "key-field" is nullable, set it to null. Otherwise delete its row from the value table. |
clear() |
Delete all rows from the map's join table whose owner column equals the map's owner. | If the value's "key-field" is nullable, set it to null in all rows where it currently equals the map's owner. Otherwise delete those rows from the value table. |
Like one-to-many sets, operations that modify one-to-many maps can have side effects. The act of adding a value to the Map may cause it to mutate; either or both of the "owner-field" and the "key-field" of the value object may be updated. If the "owner-field" changes, the object is in effect removed from the corresponding inverse collection(s) of any object that owned it previously (meaning from all 1:M sets and/or 1:M maps governed by the same "owner-field").
Footnotes: