Jump to: navigation, search

EclipseLink/Development/JPA2.0/association-override-join-table

< EclipseLink‎ | Development
Revision as of 10:53, 3 June 2009 by Guy.pelletier.oracle.com (Talk | contribs)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Association Override Join Table support

JPA 2.0 Root | Enhancement Request

Issue Summary

JPA 2.0 specification introduced support for relational mappings such as 1-M, M-M etc on embeddable class. With that comes the option to specify an association override to apply to those mappings within the embeddable, namely those mappings that use a join table. Note an association override can also be specified at the entity level and applied to a mapping from a mapper superclass. This is existing functionality, however with the introduction of the join table element within the association override in JPA 2.0, this support needs to be implemented.

At the same time, changes will be made to support association override join columns on a uni-directional one to many from an embeddable class. Note, that support (although not explicitely supported from the spec in JPA 1.0) for association override join columns to 1-1 and 1-M mappings from an embeddable class was already implemented and tested.

Association overrides are described in detail in section 11.1.2 of the specification.

General Solution

For the most part, the solution for this will mainly be in the metadata processing. However, to support an association override join table from an embeddable class will require some changes to core code, namely to AggregateObjectMapping.

Note: Support for association overrides to embeddable collection mappings containing a M-M or unidirectional 1-M is not supported (as per the spec, section 2.6)

Metadata processing

Much of the metadata processing for handling association overrides is already in place. That is, merging/override class level overrides with attribute level overrides for individual attributes. This includes handling any dot notation names for keys and values etc. The actual handling of the association override however is what needs to be implemented.

The application of overrides from an entity to a mapped superclass level was fairly straight forward. When processing an attribute, the processing of any details of that attribute (join columns or join table for example) is a simple check of does an entity level override exist, if so, use it, otherwise use the overrides defined directly on the attribute. Those overrides are then processed in the same manner, defaulting where necessary and applied directly to that attributes mapping.

Overriding for an embedded mapping case posed a slightly more complex issue. Currently, an embeddable class is only processed once and attribute overrides are currently supported through the use of field name translations on the embedded mapping. Field name translations can also be used in the case of an association override for a 1-1 or 1-M (non-unidirectional). However, core changes were needed to support M-M and uni-directional 1-M's using join columns since providing field name translations was not sufficient for these mappings.

To support these mappings, I provided a two step solution.

First off, note the embeddable classes continue to only be processed once. That is, internally EclipseLink builds one descriptor for it and it contains a list of its associated mappings. At initialize time this descriptor is then cloned and set as the reference to all embedded mappings that reference it (this includes the cloning the mappings on the embedabble descriptor as well).

From metadata processing, when processing an embedded mapping that had association overrides to apply to either a M-M or a unidirectional 1-M using join columns, the metadata processing looks up the accessor from the embeddable, spins/processes a new mapping using the overrides (in the same way the original mapping from the embeddable accessor was processed, taking defaults into considereation etc.) and store that override mapping mappings on the embedded mapping in a map keyed by attribute name.

/** 
 * List of many to many mapping overrides to apply at initialize time to 
 * their cloned aggregate mappings. 
 */
protected transient List<ManyToManyMapping> overrideManyToManyMappings;
    
/** 
 * List of unidirectional one to many mapping overrides to apply at 
 * initialize time to their cloned aggregate mappings. 
 */
protected transient List<UnidirectionalOneToManyMapping> overrideUnidirectionalOneToManyMappings;

That's all the metadata processing is responsible for and can do at this point.

Core processing

At descriptor initialize time when iterating over the mappings, embedded mappings (AggregateObjectMappings) would need to perform some extra initializing method. Namely, once the embeddable descriptor (reference descriptor) is cloned, including its mappings, the embedded mapping would then iterate over its map of override mappings provided by the metatata processing and apply those its cloned descriptors mappings as follows:

setReferenceDescriptor(clonedDescriptor);

// Apply any override m2m mappings to their cloned mappings.
for (ManyToManyMapping overrideMapping : overrideManyToManyMappings) {
  DatabaseMapping mapping = clonedDescriptor.getMappingForAttributeName(overrideMapping.getAttributeName());
            
  if (mapping.isManyToManyMapping()) {
    ManyToManyMapping mappingClone = (ManyToManyMapping) mapping;
    mappingClone.setRelationTable(overrideMapping.getRelationTable());
    mappingClone.setSourceKeyFields(overrideMapping.getSourceKeyFields());
    mappingClone.setSourceRelationKeyFields(overrideMapping.getSourceRelationKeyFields());
    mappingClone.setTargetKeyFields(overrideMapping.getTargetKeyFields());
    mappingClone.setTargetRelationKeyFields(overrideMapping.getTargetRelationKeyFields());
  } else {
    // Silently ignored for now. These override mappings are set and
    // controlled through JPA metadata processing.
  }
}
        
// Apply any override uni-directional 12m mappings to their cloned mappings.
for (UnidirectionalOneToManyMapping overrideMapping : overrideUnidirectionalOneToManyMappings) {
  DatabaseMapping mapping = clonedDescriptor.getMappingForAttributeName(overrideMapping.getAttributeName());
            
  if (mapping.isUnidirectionalOneToManyMapping()) {
    UnidirectionalOneToManyMapping mappingClone = (UnidirectionalOneToManyMapping) mapping;
    mappingClone.setSourceKeyFields(overrideMapping.getSourceKeyFields());
    mappingClone.setTargetForeignKeyFields(overrideMapping.getTargetForeignKeyFields());
  } else {
    // Silently ignored for now. These override mappings are set and
    // controlled through JPA metadata processing.
  }
}

Also, the clone methods for M-M and 1-M had to be modified slightly to ensure any mapping queries are not shared/re-used and that all sql calls were updated/built correctly based on the override mappings join table and keys.

ManyToManyMapping.clone()

clone.setInsertQuery((DataModifyQuery) insertQuery.clone());
clone.setDeleteQuery((DataModifyQuery) deleteQuery.clone());

OneToManyMapping.clone()

clone.addTargetQuery = (DataModifyQuery) this.addTargetQuery.clone();
clone.removeTargetQuery = (DataModifyQuery) this.removeTargetQuery.clone();
clone.removeAllTargetsQuery = (DataModifyQuery) this.removeAllTargetsQuery.clone();

Examples

Annotation

@Embeddable
public class Accredidation {

    @OneToMany(cascade={PERSIST, MERGE})
    @JoinColumn(name="IGNORED_JOIN_COLUMN")
    List<Official> officials;
    
    @ManyToMany(cascade={PERSIST, MERGE})
    @JoinTable(name="IGNORED_JOIN_TABLE")
    List<Witness> witnesses;

    ...
}

@MappedSuperclass
public class RatedBeerConsumer {

    @ManyToMany(cascade=ALL)
    // Expert beer consumer will use the join table as is here, whereas, novice
    // beer consumer will provide an association override.
    @JoinTable(
        name="JPA_CONSUMER_COMMITTEE",
        joinColumns=@JoinColumn(name="CONSUMER_ID", referencedColumnName="ID"),
        inverseJoinColumns=@JoinColumn(name="COMMITTEE_ID", referencedColumnName="ID")
    )
    private List<Committee> committees;

    @Embedded
    // Expert beer consumer will use these overrides, whereas, novice beer 
    // consumer will override them by defining class level overrides.
    @AssociationOverrides({
        @AssociationOverride(name="witnesses", joinTable=@JoinTable(name="EBC_ACCREDIDATION_WITNESS",
            joinColumns=@JoinColumn(name="EBC_ID", referencedColumnName="ID"),
            inverseJoinColumns=@JoinColumn(name="WITNESS_ID", referencedColumnName="ID"))),
        @AssociationOverride(name="officials", joinColumns=@JoinColumn(name="FK_EBC_ID"))
    })
    private Accredidation accredidation;

    ...
}

@Entity
@AssociationOverrides({
    @AssociationOverride(name="committees", joinTable=@JoinTable(name="JPA_NBC_COMMITTEE",
        joinColumns=@JoinColumn(name="NBC_ID", referencedColumnName="ID"),
        inverseJoinColumns=@JoinColumn(name="COM_ID", referencedColumnName="ID"))),
    @AssociationOverride(name="accredidation.officials", joinColumns=@JoinColumn(name="FK_NBC_ID")),
    @AssociationOverride(name="accredidation.witnesses", joinTable=@JoinTable(name="NBC_ACCREDITATION_WITNESS",
        joinColumns=@JoinColumn(name="NBC_ID", referencedColumnName="ID"),
        inverseJoinColumns=@JoinColumn(name="WITNESSID", referencedColumnName="ID")))
})
public class NoviceBeerConsumer extends RatedBeerConsumer {
   ...
}

@Entity
// No association overrides, will use those defined on the mappings from the mapped superclass.
public class ExpertBeerConsumer extends RatedBeerConsumer {
   ...
}

XML

<embeddable class="Accredidation">
  <attributes>
    <one-to-many name="officials">
      <join-column name="IGNORED_JOIN_COLUMN"/>
        <cascade>
          <cascade-persist/>
          <cascade-merge/>
        </cascade>
      </one-to-many>
    <many-to-many name="witnesses">
      <join-table name="IGNORED_JOIN_TABLE"/>
        <cascade>
          <cascade-persist/>
          <cascade-merge/>
        </cascade>
    </many-to-many>
    
    ...
  </attributes>
  
  ...
</embeddable>

<mapped-superclass class="RatedBeerConsumer" access="FIELD">
  <attributes>

    <many-to-many name="committees">
      <!--  
        Expert beer consumer will use the join table as is here, whereas, novice
        beer consumer will provide an association override.    
      -->
      <join-table name="XML_EBC_COMMITTEE">
        <join-column name="XML_EBC_ID" referenced-column-name="ID"/>
        <inverse-join-column name="XML_COMMITTEE_ID" referenced-column-name="ID"/>
      </join-table>
      <cascade>
        <cascade-all/>
      </cascade>
    </many-to-many>

    <embedded name="accredidation">
      <!--
        Expert beer consumer will use these overrides, whereas, novice beer 
        consumer will override them by defining class level overrides. 
      -->
      <association-override name="witnesses">
        <join-table name="XML_EBC_ACCREDIDATION_WITNESS">
          <join-column name="XML_EBC_ID" referenced-column-name="ID"/>
            <inverse-join-column name="XML_WITNESS_ID" referenced-column-name="ID"/>
        </join-table>
      </association-override>
      <association-override name="officials">
        <join-column name="FK_EBC_ID"/>
      </association-override>
    </embedded>

    ...
  </attributes>

  ...
</mapped-superclass>

<entity name="XML_NBC" class="NoviceBeerConsumer" access="PROPERTY">
  <association-override name="committees">
    <join-table name="XML_NBC_COMMITTEE">
      <join-column name="XML_NBC_ID" referenced-column-name="ID"/>
      <inverse-join-column name="XML_COM_ID" referenced-column-name="ID"/>
    </join-table>
  </association-override>
  <association-override name="accredidation.officials">
    <join-column name="FK_NBC_ID"/>
  </association-override>
  <association-override name="accredidation.witnesses">
    <join-table name="XML_NBC_ACCREDITATION_WITNESS">
      <join-column name="XML_NBC_ID" referenced-column-name="ID"/>
        <inverse-join-column name="XML_WITNESSID" referenced-column-name="ID"/>
      </join-table>
  </association-override>

  ...
</entity>

<entity name="XML_EBC" class="ExpertBeerConsumer" access="PROPERTY">

  <!-- No association overrides, will use those defined on the mappings from the mapped superclass. -->

  ...
</entity>

Work Required

  1. Develop model for testing
    approx 5 day
  2. Update Processing
    approx 10 days