Javadogs,
We have an issue with regard to the
behavior of relationships mapped using mapped-by, where there is a
bidirectional relationship instantiated by a single database artifact,
either a foreign key or a row in a join table.
The spec currently defines a subset of
behavior where relationships are updated by the program, and the memory
model must be updated to reflect the change in the datastore after
flush (including flush for commit).
The spec doesn't define the behavior when
instances are deleted or primary keys are updated.
There are interacting metadata tags that
affect the way the database schema is defined. Column attribute
allows-null specifies whether the column is nullable or not.
Foreign-key attribute delete-action specifies how the foreign key is
defined in the database. Field attribute mapped-by specifies the field
on the other side that shares a mapping artifact in the database. Field
attribute null-value specifies the behavior of null values at flush
time. Field attribute dependent specifies that there is a dependency
relationship (if the instance is deleted, the instance referred to by
the dependent field is also deleted).
The proposals below do not add any more
metadata concepts, but reuse existing concepts. The proposals are
intended for incorporation into the JDO 2 maintenance release.
Many-many relationships:
These are implemented as join tables, in
which each row in the join table corresponds to an element (or key or
value) in a field on each of the two sides. The join table is typically
mapped on one side, and the field on the other side refers to the field
with a mapped-by attribute. But the behavior is the same regardless of
which side defines the mapping.
If either side adds an instance to the map
or collection, at flush time a new row is added to the join table. The
specification requires that for consistency, the other side's
collection be updated in memory.
If either side removes an instance from the
map or collection, at flush time the corresponding row is removed from
the join table. The specification requires that for consistency, the
other side's collection be updated in memory to remove the
corresponding element or entry from the collection or map.
If an instance participating in a
relationship is deleted, and the field referring to the other side is
defined as dependent, then all instances of the other side are deleted,
and all rows in the join table corresponding to the deleted instances
are deleted.
If an instance participating in a
relationship is deleted, and the field referring to the other side is
not defined as dependent, at flush time the corresponding rows are
removed from the join table. I believe that the specification requires
that for consistency, all of the other side's collections be updated in
memory to remove the corresponding element or entry from the collection
or map. But I think it would be a good idea to add this explicitly to
the specification.
One-many relationships:
The foreign key is typically specified on
the "many" side, which declares a reference type; and the "one" side
declares an element, key, or value, and uses the mapped-by attribute.
The specification doesn't require the mapped-by attribute on the "one"
side; it's just less work to define it there.
These relationships might also be mapped
using a join table in which the existence of a relationship is
indicated by a row in the join table. The semantics and behavior are
the same as if the column defining the relationship is in the "many"
side's mapping.
If the "many" side reference in instance X
is updated (from instance A to instance B) then the spec requires that
the underlying database column(s) be updated from referring to A to
referring to B. The memory model is made consistent, by removing X from
A's collection and adding X to B's collection.
If the "one" side of instance B adds
instance X (which currently refers to A) to its collection, at flush
time the specification requires that the underlying database column be
updated in the row corresponding to instance X to refer to B. The
memory model is made consistent, by removing X from A's collection and
updating X to refer to B. A conflicting update is one in which some
other instance of the "one" type also adds X to its collection, or
instance X is changed so it refers to an instance of the "one" type
other than B.
If the "one" side of instance A removes
instance X (which currently refers to A) from its collection,
and instance X has not been added to a different collection and its
reference has not been changed to another instance, the spec is silent.
<proposed>
If the field in X is defined as
null-value="exception", then at flush time this is an error. If the
column that defines the relationship is defined as allows-null="false",
then this is an error, as there is no known value to which the column
can be changed. If the column is defined as allows-null="true" and the
field in X is defined as null-value="none", then the column is updated
to null and the memory model is changed so that the reference in X is
null.
Deleting an instance X on the "many" side
results in removing the instance from the database and updating the
memory model to be consistent. If the field of instance A that contains
X is instantiated in memory, then the collection is updated to remove X.
Deleting an instance A on the "one" side
that contains an instance X is ok if instance X is also deleted in the
same transaction. There are two cases:
o If the collection field is defined as
dependent-element="true" then instance X is also deleted by the jdo
implementation.
o If instance X is not also deleted by the
program, and the collection field is defined with
dependent-element="false" then instance X is not deleted. If the field
in X is defined as null-value="exception", then at flush time this is
an error, as the memory model cannot be made consistent. If the column
that defines the relationship is defined as allows-null="false", then
this is an error, as there is no known value to which the column can be
changed. If the column is defined as allows-null="true", and the field
in X is defined as null-value="none", then at flush time the column is
updated to null and the memory model is changed so that the reference
in X is null. NOTE: This behavior can be done automatically by the
database if the foreign-key specifies delete-action="".
</proposed>
One-one relationships:
These relationships are symmetric from the
object model perspective, as currently the only metadata to describe
whether one side or the other can exist independently is the null-value
attribute of field. When mapped, typically the mapping is to a single
unique foreign key column. This column is defined on one side of the
relationship, and the mapping of the other side uses the mapped-by
attribute. For the purposes of this discussion, I'll refer to the side
containing the foreign key in its mapping as the mapped side, and the
other as the mapped-by side.
If the mapped-by side reference of instance
C is updated (from instance Y to instance Z) then at flush time the
underlying database column(s) of Z is updated to refer to C. The
specification requires that the memory model be made consistent, by
changing Z to refer to C. Also, since there can only be one value in
the database column containing C, the row corresponding to Y must be
changed, but if Y has not also been updated to refer to another
instance and no other instance has been updated to refer to Y, there is
no value to which to set Y's reference except null.
<proposed>
If the field in Y is defined as
null-value="exception", this is an error. If the field is defined as
null-value="none", and the column defining the relationship is defined
as allows-null="false", this is an error. If the field is defined as
null-value="none", and the column defining the relationship is defined
as allows-null="true", then the column is updated to null and the
memory model is made consistent by setting the field in Y to null.
</proposed>
If the mapped side of instance Y is set to
instance D (and Y currently refers to C), the spec requires that the
underlying database column be updated in instance Y to refer to D. The
memory model is made consistent, by setting the reference in D to Y and
setting C's reference to null.
<proposed>
If the field in C is defined as
null-value="exception", this is an error. If the field is defined as
null-value="none", the column in the row corresponding to instance Y is
updated to refer to D and there is no longer any row with a reference
to C.
</proposed>
If the mapped-by side reference of instance
C is deleted (and currently instance C refers to Y and vice versa) and
the field in C is defined as dependent then at flush time both C and Y
are deleted.
If the field in C is not defined as
dependent and Y has not been changed to refer to another instance, then
at flush time the reference in Y to C must be set to null.
<proposed>
If the field in Y is defined as
null-value="exception", this is an error. If the field is defined as
null-value="none", and the column defining the relationship is defined
as allows-null="false", this is an error. If the field is defined as
null-value="none", and the column defining the relationship is defined
as allows-null="true", then the column in Y is updated to null and the
memory model is made consistent by setting the field in Y to null.
</proposed>
If the mapped side reference of instance Y
is deleted (and currently instance Y refers to C and vice versa) and
the field in Y that refers to C is defined as dependent, then at flush
time the rows corresponding to Y and C are deleted.
If the field is not defined as dependent,
then at flush time the field in C must be set to null.
<proposed>
If the field in C is defined as
null-value="exception", this is an error. If the field is defined as
null-value="none", the row corresponding to instance Y is deleted and
there is no instance that refers to C.
</proposed>
<spec>
The field on the other side of the
relationship can be mapped by using the mapped-by at-
tribute identifying the field on the side
that defines the mapping. Regardless of which side
changes the relationship, flush (whether
done as part of commit or explicitly by the user)
will modify the datastore to reflect the
change and will update the memory model for con-
sistency. There is no further behavior
implied by having both sides of the relationship map
to the same database column(s). In
particular, making a change to one side of the relation-
ship does not imply any runtime behavior by
the JDO implementation to change the other
side of the relationship in memory prior to
flush, and there is no requirement to load fields
affected by the change if they are not
already loaded. This implies that if the RetainVal-
ues flag or DetachAllOnCommit is set to
true, and the relationship field is loaded, then
the implementation will change the field on
the other side so it is visible after transaction
completion.
Conflicting changes to relationships cause
a JDOUserException to be thrown at flush
time. Conflicting changes include:
•adding a related instance with a
single-valued mapped-by relationship field to
more than one one-to-many collection
relationship
•setting both sides of a one-to-one
relationship such that they do not refer to each
other
</spec>