TriActiveJDO (currently) supports two basic types of collection fields in PersistenceCapableClasses: Set and Map. The JDO specification requires a third type, List , to be supported also; this will be added in a future release.
Developers should declare Set fields as java.util.Set
in
the Java source, whereas Map fields must be declared as
java.util.Map
.
Set fields can also be declared as java.util.Collection
, but the
underlying implementation is always that of a Set.
The implication of this is that, for the present, persistent fields
cannot be declared as concrete implementation classes such as those in
java.util
(in other words, HashSet
,
HashMap
, etc. despite the fact that these are
default persistent types).
Although support for such types will be added in future TJDO versions (in order
to approach full compliance with the JDO spec), it is nevertheless recommended
that persistent collection fields always be declared as an interface type.1
Collection fields are one of the few areas in JDO that require additional metadata to be provided. Java itself has no mechanism (yet2) to declare the content type(s) of collections, but a JDO implementation needs such declarations in order to properly organize storage for the collection in the database.
The additional information that must be provided is:
For example:
public class Department { private Set employees; private Map employeesByName; ... } <jdo> <package name="com.triactive.jdo.example"> <class name="Department"> <!-- A Set field. --> <field name="employees"> <collection element-type="Employee"/> </field> <!-- A Map field. --> <field name="employeesByName"> <map key-type="String" value-type="Employee"/> </field> ... </class> </package> </jdo>
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
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).
Inverse relationships are a TJDO extension for persistent collections. An inverse relationship is the preferred way to define collections that represent 1:Many associations between objects. Rather than using a separate, dedicated table to store the relationship (i.e. the "contents" of the Set or Map), a field is designated (or, for a Map, two fields) in the "many"-side object to govern the relationship. Aside from utilizing database resources more efficiently, an inverse relationship has the benefit of automatically enforcing its 1:Many cardinality.
Inverse relationships also usually imply ownership, in the traditional sense that the "one"-side (owner) object manages the lifecycle of its dependent (owned) objects on the "many"-side. However, in TJDO the term "owner" is generically applied to any object on the "one"-side of an inverse relationship, regardless of whether that object manages the lifecycle of the collection contents.
In an inverse relationship, the owner has a field that represents the collection of owned objects, and each of the owned objects has a field that references its owner. This relationship must be defined in the JDO Metadata of both the owned object and the owning object, and the definitions must be consistent; that is, they must refer to each other (hence the term inverse).
The following TriActive extensions are used to describe inverse relationships:
vendor-name | key | value | Where to add extension |
---|---|---|---|
"triactive" | "owner-field" | Name of the owner field in the element class | The <collection> element for the Set field in the owner class |
"triactive" | "collection-field" | Name of Set field in the owner class | The <field> element for the owner field in the element class |
vendor-name | key | value | Where to add extension |
---|---|---|---|
"triactive" | "owner-field" | Name of the owner field in the value class | The <map> element for the Map field in the owner class |
"triactive" | "key-field" | Name of the key field in the value class | The <map> element for the Map field in the owner class |
"triactive" | "map-field" | Name of Map field in the owner class | The <field> element for the owner field in the value class |
As an example, consider a typical order/line item relationship. An order may contain zero, one, or more line items, and each line item is associated with one and only one order.
public class Order { private String orderNumber; private Set lineItems; // aggregates all LineItems having a given 'order' value ... } public class LineItem { private Order order; // controls which Order's 'lineItems' will contain this object private Item item; private int quantity; ... } <jdo> <package name="com.triactive.jdo.example"> <class name="Order"> <field name="lineItems"> <collection element-type="LineItem"> <extension vendor-name="triactive" key="owner-field" value="order"/> </collection> </field> ... </class> </package> </jdo> <jdo> <package name="com.triactive.jdo.example"> <class name="LineItem"> <field name="order" null-value="exception"> <extension vendor-name="triactive" key="collection-field" value="lineItems"/> </field> ... </class> </package> </jdo>
The line items associated with a given Order can now be operated upon as though they were stored in a separate collection, although no such separate storage actually exists.
Because no separate storage exists, the various operations that can be performed on an inverse set have unique properties, in particular the operations that modify the set. The following table compares the behavior of the basic Set operations between normal and inverse sets (all other Set methods are implemented in terms of these basic Set operations).
Method | Normal Set | Inverse 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. |
remove() |
Delete any row from the set's join table equalling the given {owner, element}. | Update the element's "owner-field" to null. |
clear() |
Delete all rows from the set's join table whose owner column equals the set's owner. | Update the "owner-field" to null in all rows in the element table where it currently equals the set's owner. |
Note that, for example, the act of adding an element to an inverse 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 inverse collection(s) of the object that owned it previously (in this case, "corresponding" means governed by the same "owner-field").
For example, if a LineItem is added to the lineItems set for order O1, then it is subsequently added to the lineItems 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. This is a perhaps surprising, but nonetheless desirable consequence of the fact that an inverse relationship is by nature 1:Many.
To illustrate an example of an inverse map, assume we were to modify the above example such that line items now have a line number, and all line numbers for a given order must be unique. We can add a Map field to Order that uniquely maps line numbers to their respective line item objects.
public class Order { private String orderNumber; private Set lineItems; // aggregates all LineItems having a given 'order' value private Map lineItemsByLineNo; // aggregates all LineItems having a given 'order' value, // indexed by their 'lineNo' ... } public class LineItem { private Order order; // controls which Order's 'lineItems' AND 'lineItemsByLineNo' fields // will contain this object private int lineNo; // serves as the key for this object in the 'lineItemsByLineNo' map private Item item; private int quantity; ... } <jdo> <package name="com.triactive.jdo.example"> <class name="Order"> <field name="lineItems"> <collection element-type="LineItem"> <extension vendor-name="triactive" key="owner-field" value="order"/> </collection> </field> <field name="lineItemsByLineNo"> <map key-type="int" value-type="LineItem"> <extension vendor-name="triactive" key="owner-field" value="order"/> <extension vendor-name="triactive" key="key-field" value="lineNo"/> </map> </field> ... </class> </package> </jdo> <jdo> <package name="com.triactive.jdo.example"> <class name="LineItem;"> <field name="order"> <extension vendor-name="triactive" key="collection-field" value="lineItems"/> <extension vendor-name="triactive" key="map-field" value="lineItemsByLineNo"/> </field> ... </class> </package> </jdo>
Note that the LineItem.order field now serves to define the owner for both the Order.lineItems set and the Order.lineItemsByLineNo map.
Being an inverse map, the lineItemsByLineNo field occupies no additional storage in the database; the Map implementation provided by TJDO simply provides a Map-like "view" of the owned objects. In addition, TJDO will automatically define an appropriate database constraint on the value table to ensure that the necessary uniqueness is maintained (in this case, ensuring that the combination of order and lineNo remains unique within the LineItem table).
The following table compares the behavior of the basic Map operations between normal and inverse maps.
Method | Normal Map | Inverse 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}. | Update the value's "key-field" to null. |
clear() |
Delete all rows from the map's join table whose owner column equals the map's owner. | Update the "key-field" to null in all rows in the value table where the "owner-field" currently equals the map's owner. |
Like inverse sets, operations that modify inverse maps can have side effects. The act of adding a value to an inverse map may cause it to mutate; in the case of maps, 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 inverse sets and/or inverse maps governed by the same "owner-field").
Footnotes:
HashSet
instead of a Set
, at least in
newly-written code, but there can be a performance penalty.
That's because in each case, the JDO runtime will populate a Set field with
an object of some internal implementation class that either extends
HashSet
or implements Set
, respectively, yet is
backed by the database.
For a variety of reasons, a class that extends HashSet
has fewer
opportunities to optimize its database access than one that fully supplies its
own implementation of Set
(among other things, the contents of a
HashSet
must always be fully loaded into memory).