The following sections enumerate the myriad of field mappings JDOR
supports. JDOR augments standard persistence metadata's
field
element with many new object-relational
attributes and sub-elements. As we explore the library of standard
mappings, we introduce each of these enhancements in context.
Note | |
---|---|
Kodo allows you to create custom field mappings for unsupported field types or database schemas. See the Reference Guide's Chapter 7, Mapping for complete coverage of Kodo JDO's mapping capabilities. |
As you may have already noticed from the mapping document we have
been constructing throughout this chapter, subclasses can map
superclass fields by setting the field
element's
name
attribute to
<superclass-name>.<field-name>
. For example,
Subscription
maps its Contract
superclass' id
field:
<class name="Subscription" table="CNTRCT.SUB"> <field name="Contract.id" column="ID"/> ... </class>
If Contract
were in a different package than
Subscription
, we would have to fully qualify
Contract
's class name:
<class name="Subscription" table="CNTRCT.SUB"> <field name="org.mag.subscribe.Contract.id" column="ID"/> ... </class>
As we tour the standard JDOR field mappings throughout this chapter, keep in mind that any of them can be applied to a superclass field just as easily as to a field in the current class.
A basic field mapping stores the field value
directly into a database column. Primitives, primitive wrappers,
String
s, Date
s,
Locale
s, and any other supported type that
can be easily translated into a native JDBC value default to
basic mapping. In fact, you have already seen examples of basic
field mappings in this chapter - for example, the mapping of
Magazine
's primary key fields in
Example 15.3, “Datastore Identity Mapping”.
To write a basic field mapping, just name the column that holds
the field value. The field
element accepts a
column
attribute or nested column
elements for this purpose. We introduced columns and the
attribute/element dichotomy already in
Section 15.6, “Column”.
Note | |
---|---|
Kodo stores Java 5 |
Below we present an updated diagram of our model and its associated database schema, followed by its representation as a mapping metadata document. All basic fields are now mapped.
Example 15.11. Basic Field Mapping
<?xml version="1.0"?> <orm> <package name="org.mag"> <class name="Magazine" table="MAG"> <field name="isbn"> <column name="ISBN" jdbc-type="char" length="15"/> </field> <field name="title" column="TITLE"/> <field name="price" column="PRICE"/> <field name="copiesSold" column="COPIES"/> ... </class> <class name="Article" table="ART"> <field name="id" column="ID"/> <field name="title" column="TITLE"/> ... </class> </package> <package name="org.mag.pub"> <class name="Company" table="COMP"> <field name="name" column="NAME"/> <field name="revenue" column="REV"/> ... </class> <class name="Author" table="AUTH"> <field name="firstName" column="FNAME"/> <field name="lastName" column="LNAME"/> ... </class> </package> <package name="org.mag.subscribe"> <class name="Subscription" table="CNTRCT.SUB"> <field name="Contract.id" column="ID"/> <field name="startDate" column="START"/> <field name="payment" column="PAY"/> ... </class> <class name="LifetimeSubscription"> <field name="eliteClub" column="ELITE"/> ... </class> <class name="TrialSubscription" table="CNTRCT.TRIAL_SUB"> <field name="endDate" column="END"/> ... </class> <class name="Subscription$LineItem" table="CNTRCT.LINE_ITEM"> <field name="Contract.id" column="ID"/> <field name="price" column="PRICE"/> <field name="num" column="NUM"/> ... </class> </package> </orm>
Unlike Java, most relational databases cannot transparently
store strings of arbitrary length. The standard database
string types CHAR
and VARCHAR
are often limited to holding 255 characters or less.
To store longer strings, you must use the
CLOB
column type.
CLOB
stands for
Character Large OBject. In JDOR, mapping a Java
String
field to a database CLOB
column is exactly like creating any other basic mapping.
The only difference is that you must use the column
element's jdbc-type
attribute to
tell the JDOR implementation to use JDBC's CLOB
APIs rather than the standard string APIs.
Our sample model uses two large string fields:
Contract.terms
and
LineItem.comments
. Since Contract
uses a subclass-table
mapping,
LineItem
maps both these fields to its
own table:
In mapping metadata, CLOB
mapping looks like
this:
Example 15.12. CLOB Field Mapping
<class name="Subscription$LineItem" table="CNTRCT.LINE_ITEM"> <field name="Contract.terms"> <column name="TERMS" jdbc-type="clob"/> </field> <field name="comments"> <column name="COMMENTS" jdbc-type="clob"/> </field> ... </class>
Note | |
---|---|
In Kodo, you can set the |
BLOBs
are Binary Large
OBjects. JDOR uses BLOB
columns to store
byte
arrays and serialized Java objects.
Mapping a byte[]
field to a
BLOB
column is a basic mapping.
For example, let's map our model's
Article.content
field to
the ART.CONTENT
BLOB
column:
Example 15.13. Byte Array Field Mapping
<class name="Article" table="ART"> <field name="content" column="CONTENT"/> ... </class>
Mapping a serialized Object
field to a
BLOB
column is like mapping a
byte[]
field, except that you must
explicitly instruct JDOR to serialize the field value with the
serialized
attribute. Suppose that instead
of a byte[]
, our
Article.content
field is of type
Object
, and we want to serialize that
Object
to the CONTENT
column:
In Section 15.5, “Datastore Identity”, you saw how to
use the datastore-identity
element's
strategy
and sequence
attributes to
automatically generate datastore identity values. You can apply
the same pattern to auto-generate values for fields of
new objects. The field
element's
value-strategy
attribute accepts the same strategies as
datastore-identity
's strategy
attribute. The field
element also has a
sequence
attribute that functions exactly like
datastore-identity
's sequence
attribute. Let's modify our mappings to set
Article
's id
field automatically
from the ArticleSeq
sequence, and to make the
LineNumber.num
field auto-incrementing:
Example 15.15. Automatic Field Values
<?xml version="1.0"?> <orm> <package name="org.mag"> <sequence name="ArticleSeq" datastore-sequence="ART_SEQ"/> <class name="Article" table="ART"> <!-- specifying the sequence attribute defaults the --> <!-- value-strategy attribute to "sequence" automatically --> <field name="id" column="ID" sequence="ArticleSeq"/> ... </class> ... </package> <package name="org.mag.subscribe"> <class name="Subscription$LineItem" table="CNTRCT.LINE_ITEM"> <field name="num" column="NUM" value-strategy="autoassign"/> ... </class> ... </package> </orm>
Note | |
---|---|
Kodo supports the standard
Kodo also defines a |
Using the autoassign
or identity
strategy on a field may cause the
PersistenceManager
to
flush when retrieving the value of that field on a persistent-new
instance. If the field is a primary key field, retrieving the
object id of a persistent-new instance may also cause a flush, just
as it does for new instances using the autoassign
datastore identity strategy.
See Section 15.5, “Datastore Identity” for the
complete explanation.
Sometimes a a logical record is spread over multiple database tables. JDOR calls a class' declared table the primary table, and calls other tables that make up a logical record secondary tables. You can map any persistent field to a secondary table. Just write the standard field mapping, then perform these two additional steps:
Set the field
element's table
attribute to the name of the secondary table
housing the mapped columns.
Nest a join
element within the
field
that describes how the secondary
table joins to the class' primary table. The join
element goes before the field's nested
column
elements, if any. We covered
joins in Section 15.7, “Joins”. In
fact, we already used the join
element
to join a subclass table to its superclass table in
Section 15.8.2, “new-table”.
Secondary table joins, however, have one caveat that
subclass table joins do not. In a subclass join, you
know that there will always be a record in the superclass
table for every record in the subclass table. In a
secondary table join, that is not the case. Some databases
are maintained such that primary table rows may
not have matching secondary table rows. When you load
objects based on these records, you want the fields mapped
to the secondary table to come back null
.
Joins that produce null
s for missing
records are called outer joins. You
may recall having read about another use for outer joins in
Section 15.9.3, “none”. If
your field is mapped to a secondary table that doesn't
necessarily have a row for every primary table row, set the
join
element's outer
attribute to true
.
Note | |
---|---|
When the |
In the following example, we move the Article.content
field we mapped in
Section 15.11.2.2, “BLOB” into an outer-joined
secondary table, like so:
Example 15.16. Secondary Table Field Mapping
<class name="Article" table="ART"> <field name="id" column="ID"/> <field name="content" table="ART_DATA" column="CONTENT"> <join outer="true"> <column name="ART_ID" target="ID"/> </join> </field> ... </class>
Because the target table ART
only has a
single primary key column, we can use
join
shortcuts to get rid of the nested column
element. Our final mapping becomes:
<class name="Article" table="ART"> <field name="id" column="ID"/> <field name="content" table="ART_DATA" column="CONTENT"> <join outer="true" column="ART_ID"/> </field> ... </class>
A direct relation is a non-embedded persistent field that holds a
reference to another persistence-capable object. Our model
has three direct relations: Magazine
's
publisher
field is a direct relation
to a Company
,
Magazine
's coverArticle
field is a
direct relation to Article
, and
the LineItem.magazine
field is a direct relation
to a Magazine
. Direct relations are
represented in the database by foreign key columns:
You should be familiar with foreign key mapping from
Section 15.7, “Joins”. Additionally, we
have already demonstrated mapping foreign key columns onto the
join
element in
Section 15.8.2, “new-table” and
Section 15.11.4, “Secondary Tables”.
The only difference now is that direct relations map the foreign
key columns directly onto the field
element:
Example 15.17. Direct Relation Field Mapping
<?xml version="1.0"?> <orm> <package name="org.mag"> <class name="Magazine" table="MAG"> <field name="isbn"> <column name="ISBN" jdbc-type="char" length="15"/> </field> <field name="title" column="TITLE"/> <field name="coverArticle"> <column name="COVER_ID" target="ID"/> </field> <field name="publisher"> <column name="PUB_ID" target="CID"/> </field> ... </class> <class name="Article" table="ART"> <field name="id" column="ID"/> ... </class> ... </package> <package name="org.mag.pub"> <class name="Company" table="COMP"> <datastore-identity column="CID"/> ... </class> ... </package> <package name="org.mag.subscribe"> <class name="Subscription$LineItem" table="CNTRCT.LINE_ITEM"> <field name="magazine"> <column name="MAG_ISBN" target="ISBN"/> <column name="MAG_TITLE" target="TITLE"/> </field> ... </class> ... </package> </orm>
Or, after applying syntactic shortcuts:
<?xml version="1.0"?> <orm> <package name="org.mag"> <class name="Magazine" table="MAG"> <field name="isbn"> <column name="ISBN" jdbc-type="char" length="15"/> </field> <field name="title" column="TITLE"/> <field name="coverArticle" column="COVER_ID"/> <field name="publisher" column="PUB_ID"/> ... </class> <class name="Article" table="ART"> <field name="id" column="ID"/> ... </class> ... </package> <package name="org.mag.pub"> <class name="Company" table="COMP"> <datastore-identity column="CID"/> ... </class> ... </package> <package name="org.mag.subscribe"> <class name="Subscription$LineItem" table="CNTRCT.LINE_ITEM"> <field name="magazine"> <column name="MAG_ISBN" target="ISBN"/> <column name="MAG_TITLE" target="TITLE"/> </field> ... </class> ... </package> </orm>
Note | |
---|---|
A direct relation between types |
The direct relations above are all based on forward foreign keys. But relations can also be based on inverse keys. We described inverse foreign keys in Section 15.7, “Joins”.
For example, suppose that instead of Magazine
having a relation to its cover
Article
, Article
has a
relation to the Magazine
it serves as
the cover of. We can represent this
relation without changing our schema:
Example 15.18. Inverse Key Relation Field Mapping
Notice that the Article.coverOf
field
refers to the foreign key column in the
MAG
table by using the fully qualified
column name.
<class name="Magazine" table="MAG"> ... </class> <class name="Article" table="ART"> <field name="id" column="ID"/> <field name="coverOf"> <column name="MAG.PUB_ID" target="ID"/> </field> ... </class>
After syntactic shortuts:
<class name="Magazine" table="MAG"> ... </class> <class name="Article" table="ART"> <field name="id" column="ID"/> <field name="coverOf" column="MAG.PUB_ID"/> ... </class>
In the previous sections, we saw that a single foreign key
from the MAG
table to the
ART
table can model an object relation from
Magazine
to
Article
(through the Magazine.coverArticle
field), or from Article
to
Magazine
(though the
Article.coverOf
field). A natural question is whether
the foreign key can represent both relations at the same time.
The answer is yes.
When two fields are logical inverses of each other like
Magazine.coverArticle
and
Article.coverOf
, they form a
bidirectional relation. And when the two
fields of a bidirectional
relation share the same database mapping, JDOR formalizes the
connection with the mapped-by
field
attribute. Using the mapped-by
attribute,
we can map both Magazine.coverArticle
and
Article.coverOf
as follows:
Example 15.19. Mapping a Bidirectional Relation
<class name="Magazine" table="MAG"> <field name="coverArticle" column="PUB_ID"/> ... </class> <class name="Article" table="ART"> <field name="coverOf" mapped-by="coverArticle"/> ... </class>
Marking Article.coverOf
as mapped-by
Magazine.coverArticle
means two
things:
Article.coverOf
uses the foreign key
mapped by Magazine.coverArticle
, but
inverses it. In fact, it is illegal to specify
any additional mapping information when you use the
mapped-by
attribute. All mapping
information is read from the referenced field.
Magazine.coverArticle
is the
"owner" of the relation. The field that specifies the
mapping data is always the owner. This means that
changes to the Magazine.coverArticle
field are reflected in the database, while changes to
the Article.coverOf
field alone are
not. Changes to Article.coverOf
may
still affect the JDOR implementation's cache, however.
Thus, it is very important that you keep your
object model consistent by properly maintaining both
sides of your bidirectional relations at all times.
Note | |
---|---|
It is more efficient to make the field that
maps to the foreign key's natural forward
direction the owner of a bidirectional
relation. Specify the mapping data on the
forward foreign key side, and use |
You should always take advantage of the mapped-by
attribute rather than mapping each field of a
bidirectional relation independently. Failing to do so may
result in the JDOR implementation trying to update the database
with conflicting data. Be careful to only mark one side of
the relation as mapped-by
, however. One side
has to actually do the mapping!
Note | |
---|---|
You can configure Kodo to automatically synchronize both sides of a bidirectional relation, or to perform various actions when it detects inconsistent relations. See Section 5.4, “Managed Inverses” in the Reference Guide for details. |
Collections and arrays of primitive wrappers,
String
s, Date
s, or anything
else that can be directly stored in a database column all fall
under the umbrella of basic collection mapping. Basic collection
mapping is just a special case of secondary table mapping, as seen
in Section 15.11.4, “Secondary Tables”. Basic
collections map to a secondary table consisting of three
components:
Foreign key columns linking back to the owning class'
primary table. As with all secondary table mappings, these
foreign key columns are mapped to a join
element.
A column to hold a collection element. Each row of the
collection table holds a single collection or
array element. Logically enough, the element
element represents a collection or array element.
An optional ordering column. Relational databases do
not preserve record order. This column stores the
relative position of elements within the collection or
array so that the JDOR implementation can retrieve them
in the same order they were in when stored. The
order
element anchors ordering column
information in mapping metadata.
Note | |
---|---|
In addition to supporting an ordering column, Kodo allows you to order on the collection element values, or, in the case of relations, fields of the related type. See Section 6.4.2.4, “Order-By” in the Reference Guide for details. |
The Article.subtitles
field is the only
basic collection in our model. This List
of String
s is mapped as follows:
In mapping metadata, this becomes:
Example 15.20. Basic Collection Field Mapping
<class name="Article" table="ART"> <field name="id" column="ID"/> <field name="subtitles" table="ART_SUBS"> <join> <column name="ART_ID" target="ID"/> </join> <element column="SUBTITLE"/> <order column="ORD"/> </field> ... </class>
Taking advantage of join shortuts yields:
<class name="Article" table="ART"> <field name="id" column="ID"/> <field name="subtitles" table="ART_SUBS"> <join column="ART_ID"/> <element column="SUBTITLE"/> <order column="ORD"/> </field> ... </class>
The same pattern applies to mapping collections of BLOBs or CLOBs, or any elements that can be stored in one or more data columns.
An association table consists of two foreign
keys, plus an optional ordering column. In other words, if you
replace the data columns of a basic collection table
(Section 15.11.6, “Basic Collections”) with another
foreign key, you produce an association table. As its name implies,
each row of an association table associates two objects together.
JDOR uses association tables to represent collections of
persistence-capable objects: one foreign key refers back to the
collection's owner, and the other refers to a collection element.
Our model's Magazine.articles
and
Company.subscriptions
fields both have association table
mappings.
Note | |
---|---|
An association table relation between types
|
In metadata, an association table mapping is exactly like a basic
collection mapping
(Section 15.11.6, “Basic Collections”), except that
the element
columns represent a foreign key.
You should be very familiar with foreign key mapping by now. See
Section 15.7, “Joins” for a refresher.
Example 15.21. Association Table Collection Field Mapping
<?xml version="1.0"?> <orm> <package name="org.mag"> <class name="Magazine" table="MAG"> <field name="isbn"> <column name="ISBN" jdbc-type="char" length="15"/> </field> <field name="title" column="TITLE"/> <field name="articles" table="MAG_ARGS"> <join> <column name="MAG_ISBN" target="ISBN"/> <column name="MAG_TITLE" target="TITLE"/> </join> <element> <column name="ART_ID" target="ID"/> </element> </field> ... </class> <class name="Article" table="ART"> <field name="id" column="ID"/> ... </class> ... </package> <package name="org.mag.pub"> <class name="Company" table="COMP"> <datastore-identity column="CID" strategy="autoassign"/> <field name="subscriptions" table="COMP_SUBS"> <join> <column name="COMP_ID" target="CID"/> </join> <element> <column name="SUB_ID" target="ID"/> </element> </field> ... </class> ... </package> <package name="org.mag.subscribe"> <class name="Subscription" table="CNTRCT.SUB"> <field name="Contract.id" column="ID"/> ... </class> ... </package> </orm>
Applying join shortcuts yields:
<?xml version="1.0"?> <orm> <package name="org.mag"> <class name="Magazine" table="MAG"> <field name="isbn"> <column name="ISBN" jdbc-type="char" length="15"/> </field> <field name="title" column="TITLE"/> <field name="articles" table="MAG_ARGS"> <join> <column name="MAG_ISBN" target="ISBN"/> <column name="MAG_TITLE" target="TITLE"/> </join> <element column="ART_ID"/> </field> ... </class> <class name="Article" table="ART"> <field name="id" column="ID"/> ... </class> ... </package> <package name="org.mag.pub"> <class name="Company" table="COMP"> <datastore-identity column="CID" strategy="autoassign"/> <field name="subscriptions" table="COMP_SUBS"> <join column="COMP_ID"/> <element column="SUB_ID"/> </field> ... </class> ... </package> <package name="org.mag.subscribe"> <class name="Subscription" table="CNTRCT.SUB"> <field name="Contract.id" column="ID"/> ... </class> ... </package> </orm>
None of our model's association table collections are ordered,
but if they were, we could use the order
mapping element to specify the ordering column, just as in a
basic
collection mapping.
Association tables have no built-in sense of direction. Each
row links two objects, period. You can look at the
MAG_ARTS
table in our example above as
linking each Magazine
to its
Article
s, or as linking each
Article
to the Magazine
s it
appears in. The two outlooks are equally valid. This makes
association tables perfect candidates for representing
bidirectional relations.
Section 15.11.5.2, “Bidirectional Relations” examined direct
bidirectional relations that share a foreign key. Association
table bidirectional relations share association table rows
instead. Aside from that, all the same concepts apply.
In particular, you still use the mapped-by
attribute to mark one collection field as mapped and "owned"
by another collection field. The owning field is responsible
for manipulating association table records - the same caveats
about keeping your bidirectional relations synchronized apply
equally to collections.
In the example below, we have added an
Article.magazines
field as the logical inverse of the
the Magazine.articles
field.
Example 15.22. Mapping Bidirectional Association Table Relations
<?xml version="1.0"?> <orm> <package name="org.mag"> <class name="Magazine" table="MAG"> <field name="isbn"> <column name="ISBN" jdbc-type="char" length="15"/> </field> <field name="title" column="TITLE"/> <field name="articles" table="MAG_ARGS"> <join> <column name="MAG_ISBN" target="ISBN"/> <column name="MAG_TITLE" target="TITLE"/> </join> <element column="ART_ID"/> </field> ... </class> <class name="Article" table="ART"> <field name="id" column="ID"/> <field name="magazines" mapped-by="articles"/> ... </class> ... </package> </orm>
Note | |
---|---|
Association tables are symmetric; therefore, the choice of owning vs. non-owning side is arbitrary. However, only the owning side of the relation can order the its collection elements. |
Inverse key collection mappings are the only standard collection mappings that do not use a secondary table. Instead, the mapping is based on an inverse foreign key in the table of the related class. Rather than try to explain the mapping abstractly, let's jump to an example.
Note | |
---|---|
Inverse key collection relations are also called one-to-many relations. |
The Subscription.items
field in our model is
an inverse key collection. Subscription.items
is a Collection
of LineItem
objects. There is a foreign key from
LineItem
's table, CNTRCT.LINE_ITEM
,
to Subscription
's table,
CNTRCT.SUB
. The relation is loaded by selecting
all LineItem
records whose foreign key
links back to the owning Subscription
's
record.
This situation may look familiar to you. It is, in fact, exactly
like the inverse key direct relation from
Section 15.11.5.1, “Inverse Keys”. The only thing
separating this mapping from the inverse key direct relation
mapping is that in this case, multiple rows in
CNTRCT.LINE_ITEM
might have a foreign key to the same
CNTRCT.SUB
record. At the object level, this
results in a collection rather than a direct reference.
This difference is also reflected in the mapping metadata. While
direct references are mapped to field
,
collection elements are mapped to field
's nested
element
element. Also, you do not have to
fully qualify the name of the foreign key columns in an inverse
key collection, as you do in an inverse key direct relation.
That is because the lack of a secondary table is a hint to the
JDOR implementation that you are using an inverse key collection,
and it knows to look in the table of the related class
for the foreign key columns.
Example 15.23. Inverse Key Collection Field Mapping
<?xml version="1.0"?> <orm> <package name="org.mag.subscribe"> <class name="Subscription" table="CNTRCT.SUB"> <field name="Contract.id" column="ID"/> <field name="items"> <element> <column name="SUB_ID" target="ID"/> </element> </field> ... </class> <class name="Subscription$LineItem" table="CNTRCT.LINE_ITEM"> <field name="Contract.id" column="ID"/> ... </class> ... </package> </orm>
Or with join shortcuts in place:
<?xml version="1.0"?> <orm> <package name="org.mag.subscribe"> <class name="Subscription" table="CNTRCT.SUB"> <field name="Contract.id" column="ID"/> <field name="items"> <element column="SUB_ID"/> </field> ... </class> <class name="Subscription$LineItem" table="CNTRCT.LINE_ITEM"> <field name="Contract.id" column="ID"/> ... </class> ... </package> </orm>
Though it is rare, you can map an order column to an inverse key
collection to maintain collection order. You use the
order
mapping element, as in any other collection
mapping. The order column must be in the same table as the foreign
key column(s).
Note | |
---|---|
Kodo allows you to specify an order column even in bidirectional inverse key collection mappings. |
Most inverse key collections are actually half of a
bidirectional relation. The other half is a standard
direct relation mapping
(Section 15.11.5, “Direct Relations”) onto the foreign
key. In fact, it is unusual to write an inverse key collection
mapping as we did above; the vast majority of inverse key
collections simply use the mapped-by
attribute introduced in
Section 15.11.5.2, “Bidirectional Relations” to refer to
their partner field. Our model's
Magazine.publisher
and
Company.magazines
fields are perfect illustrations
of the normal pairing between a direct relation and an inverse
key collection.
Example 15.24. Direct Relation / Inverse Key Collection Pair
<?xml version="1.0"?> <orm> <package name="org.mag"> <class name="Magazine" table="MAG"> <field name="publisher" column="PUB_ID"/> ... </class> ... </package> <package name="org.mag.pub"> <class name="Company" table="COMP"> <datastore-identity column="CID"/> <field name="mags" mapped-by="publisher"/> ... </class> ... </package> </orm>
JDOR supports maps of all kinds. Your map keys can be basic types
like String
s, more interesting types like
BLOBs and CLOBs, or even persistence-capable objects. Map values
have equal flexibility. Rather than treat each combination of key
and value types as a separate mapping, this section explains the
concepts of map mapping in JDOR. Armed with a solid understanding
of map concepts, you will be able to write a mapping for any
combination of key and value classes, because object-relational
mapping in JDOR is remarkably consistent.
There are two ways to represent a map. The first is to write the map to a secondary table. You may recall secondary table mapping of singular values from Section 15.11.4, “Secondary Tables”. In a map's secondary table, each row represents a map entry. Therefore, every map's secondary table has the following three components:
Foreign key columns linking back to the owning class'
primary table. As with all secondary table mappings, these
foreign key columns are mapped to a join
element.
Columns to hold the entry's key. If the map uses a simple
key type like String
, these columns
will be standard data columns. If the map uses
persistence-capable objects as keys, these will be foreign
key columns linking to the records for those objects. Key
columns are anchored to to a key
element
nested in the field
element.
Columns to hold the entry's value. Like key columns, value
columns can be direct data columns or foreign key columns,
depending on the map's value type. You specify map value
columns on the field
's value
element.
In short, secondary table map mapping is like a combination of basic collection mapping (Section 15.11.6, “Basic Collections”) and association table collection mapping (Section 15.11.7, “Association Table Collections”), depending on the key and value types involved.
Article.authors
is the lone map field in our
model. For each author of the article, this field maps the
author's last name to the persistent Author
instance.
To translate this to mapping metadata, we perform a basic
secondary table mapping, then add the key columns to the
key
element and the value foreign key columns to the
value
element.
Example 15.25. Map Field Mapping
<?xml version="1.0"?> <orm> <package name="org.mag"> <class name="Article" table="ART"> <field name="id" column="ID"/> <field name="authors" table="ART_AUTHS"> <join> <column name="ART_ID" target="ID"/> </join> <key column="LNAME"/> <value> <column name="AUTH_ID" target="AID"/> </value> </field> ... </class> ... </package> <package name="org.mag.pub"> <class name="Author" table="AUTH"> <datastore-identity sequence="AuthorSeq"> <column name="AID" sql-type="INTEGER64"/> </datastore-identity> ... </class> ... </package> </orm>
Or with join shortuts in place:
<?xml version="1.0"?> <orm> <package name="org.mag"> <class name="Article" table="ART"> <field name="id" column="ID"/> <field name="authors" table="ART_AUTHS"> <join column="ART_ID"/> <key column="LNAME"/> <value column="AUTH_ID"/> </field> ... </class> ... </package> <package name="org.mag.pub"> <class name="Author" table="AUTH"> <datastore-identity> <column name="AID" sql-type="INTEGER64"/> </datastore-identity> ... </class> ... </package> </orm>
The second way to represent a map in JDOR is as a set of
persistence-capable values with derived keys. In the example
above, the key for each Author
is her last name. So rather than create a secondary table
with a dedicated key column, we can simply map the Author
values, then declare that the key is mapped by each
Author
's last name. This lets us get rid
of the key column altogether.
Example 15.26. Derived Key Mapping
<?xml version="1.0"?> <orm> <package name="org.mag"> <class name="Article" table="ART"> <field name="id" column="ID"/> <field name="authors" table="ART_AUTHS"> <join column="ART_ID"/> <key mapped-by="lastName"/> <value column="AUTH_ID"/> </field> ... </class> ... </package> <package name="org.mag.pub"> <class name="Author" table="AUTH"> <datastore-identity> <column name="AID" sql-type="INTEGER64"/> </datastore-identity> ... </class> ... </package> </orm>
When the key is derived from a field of the value type, you aren't limited to using a secondary table to hold the map. In fact, you can take any mapping that works for a collection of persistence-capable objects and turn it into a map mapping in two steps:
Add a key
XML element. Set its
mapped-by
attribute to the name of
a field in the persistence-capable value class. JDOR will
automatically map each entry on the value of this field.
Use the value
XML element instead of the
element
collection element to map the
related persistence-capable objects.
The diagram above depicts the mapping of a
Subscription
's LineItem
s. We
created this mapping in
Section 15.11.8, “Inverse Key Collections”. The example
below turns this mapping into a map. Each map entry associates a
LineItem
's num
field value
to that LineItem
.
Example 15.27. Inverse Foreign Key Map Mapping
public class Subscription extends Contract { private Map<Long,LineItem> lineItems; ... } <?xml version="1.0"?> <orm> <package name="org.mag.subscribe"> <class name="Subscription" table="CNTRCT.SUB"> <field name="Contract.id" column="ID"/> <field name="items"> <key mapped-by="num"/> <value column="SUB_ID"/> </field> ... </class> <class name="Subscription$LineItem" table="CNTRCT.LINE_ITEM"> <field name="Contract.id" column="ID"/> ... </class> ... </package> </orm>
Chapter 5, Metadata describes JDO's concept of embedded objects. The field values of embedded objects are stored as part of the owning record, rather than as a separate database record. Thus, instead of mapping a relation to an embedded object as a foreign key, you map all the fields of the embedded instance to columns in the owning field's table.
To perform an embedded mapping, nest the embedded
element in the embedded field
. This element
has the following attributes:
null-indicator-column
: Set this attribute
to the name of a column that indicates whether the embedded
instance is null. When the JDOR implementation retrieves
the embedded object, it will check this column first. If
the column value is null
, the JDOR
runtime will load null
into the field
that owns the embedded object. If the column value is not
null
, the JDOR runtime will set the
owning field to a new embedded object instance, and will
load that instance's fields with data from their mapped
columns.
The null indicator column can be a column that is also mapped to an embedded field, or can be an artificial column whose sole purpose is to indicate whether the embedded instance is null. In the latter case, the JDOR implementation will set this column automatically.
If you do not specify a null indicator column, then an
embedded instance will never get loaded as
null
. Even if you commit an owning object with
its embedded field set to null
, the next
time you re-read the owning object its embedded field
will get loaded with a non-null embedded instance. The
embedded instance will have default values for all of its
fields.
Within the embedded
element, nest
field
elements for all the fields of the embedded
object. Map these fields as you would any other fields; you can
even map recursive embedded fields, to arbitrary depth.
In our model, Company
embeds its
Address
, as pictured in the diagram above.
Author
also embeds its
Address
. Let's see how this looks in mapping metadata:
Example 15.28. Embedded Field Mapping
This example uses the same column names for
Address
's embedded fields in both the
COMP
and AUTH
tables.
Note, though, that nothing prevents you from using completely
different mappings each time you embed an object.
<?xml version="1.0"?> <orm> <package name="org.mag.pub"> <class name="Company" table="COMP"> <field name="address"> <embedded null-indicator-column="STREET"> <field name="street" column="STREET"/> <field name="city" column="CITY"/> <field name="state"> <column name="STATE" jdbc-type="char" length="2"/> </field> <field name="zip" column="ZIP"/> </embedded> </field> ... </class> <class name="Author" table="AUTH"> <field name="address"> <embedded null-indicator-column="STREET"> <field name="street" column="STREET"/> <field name="city" column="CITY"/> <field name="state"> <column name="STATE" jdbc-type="char" length="2"/> </field> <field name="zip" column="ZIP"/> </embedded> </field> ... </class> ... </package> </orm>
We set the null indicator column to STREET
.
Any time the STREET
column is
null
, JDOR loads null
into the
address
field. Any time STREET
is not null, JDOR loads a new Address
instance into the address
field,
and populates that Address
with data
based on the above field mappings.
This section focused on fields that hold a direct reference to
an embedded object. However, JDOR also supports collections that
contain embedded object elements, and maps that contains embedded
object keys, values, or both. In these cases, the columns for the
embedded object's fields are part of the collection or map table.
Mapping embedded elements, keys, or values is exactly like mapping
an embedded field, except that instead of nesting the
embedded
element under the
field
element, you nest it under the
element
, key
, or value
elements, respectively. For example, if
Company
had a collection of embedded addresses, like so:
Then the mapping would be:
Example 15.29. Embedded Collection Elements
<class name="Company" table="COMP"> <datastore-identity column="CID" strategy="autoassign"/> <field name="addresses" table="COMP_ADDRS"> <join column="COMP_ID"/> <element> <embedded> <field name="street" column="STREET"/> <field name="city" column="CITY"/> <field name="state"> <column name="STATE" jdbc-type="char" length="2"/> </field> <field name="zip" column="ZIP"/> </embedded> </element> </field> ... </class>
Normally, you will not use a null indicator column for embedded collection elements, map keys, and map values. Only do so if you plan on storing null values in your collection or map.