Skip to main content

Notice: this Wiki will be going read only early in 2024 and edits will no longer be possible. Please see: https://gitlab.eclipse.org/eclipsefdn/helpdesk/-/wikis/Wiki-shutdown-plan for the plan.

Jump to: navigation, search

Difference between revisions of "EclipseLink/Development/JPA 1.0/table per class"

(Future)
(Metadata processing)
Line 24: Line 24:
  
 
#When processing an entity that is within a table per class inheritance hierarchy, that entity will need to process the accessors from its superclasses (similarly as is currently done for mapped superclasses).  
 
#When processing an entity that is within a table per class inheritance hierarchy, that entity will need to process the accessors from its superclasses (similarly as is currently done for mapped superclasses).  
 +
## @OneToOne
 +
## @ManyToOne
 +
## @OneToMany
 +
## @ManyToMany
 +
## @Id
 +
## @Basic
 +
## @BasicCollection
 +
## @BasicMap
 +
## etc ..
 +
# Should entity level metadata should also be inherited and processed under the context of the inheritance subclass.
 +
##@OptimisticLocking
 +
##@Cache
 +
##@IdClass
 +
##@ChangeTracking
 +
##@CopyPolicy
 +
##@ReadOnly
 +
##@ExistenceChecking
 +
##@ExcludeDefaultListeners
 +
##@ExcludeSuperclassListeners
 +
## etc.
 
#For each mapping type (1-1, M-1, 1-M and M-M), the metadata processing of non-owning sides will need to be updated. When processing a non-owning side the list of source keys is grabbed from the target key list from the owning side. In a TABLE_PER_CLASS strategy we will need to clone and update those fields to use the correct database table from the inheritance subclass.
 
#For each mapping type (1-1, M-1, 1-M and M-M), the metadata processing of non-owning sides will need to be updated. When processing a non-owning side the list of source keys is grabbed from the target key list from the owning side. In a TABLE_PER_CLASS strategy we will need to clone and update those fields to use the correct database table from the inheritance subclass.
 
#Discriminator columns and values do no apply within a TABLE_PER_CLASS strategy; therefore they will be ignored if specified by the user. Should we log a warning message or just silently ignore them?
 
#Discriminator columns and values do no apply within a TABLE_PER_CLASS strategy; therefore they will be ignored if specified by the user. Should we log a warning message or just silently ignore them?
 
The metadata processing currently will process the relational accessors. I assume the subclasses should also inherit things like @OptimisticLocking, @Cache etc.
 
  
 
==Core changes==
 
==Core changes==

Revision as of 17:00, 3 December 2008

Table Per Class

JPA 2.0 Root | Enhancement Request

Issue Summary

Implement a TABLE_PER_CLASS inheritance strategy that is currently an optional feature in the JPA spec. The initial implement should support the following mappings:

  • One to one (both target and source foreign key)
  • Many to one
  • One to many
  • Many to many

The implementation should provide named query support. For a list of those items that are not or only partially addressed/supported as this time see the future section below.

Also, see JPA 2.0 section 2.12.2 for more details.

General solution

The general solution requires two main changes. Internally, we will need a new TablePerClassInheritancePolicy, which will handle the querying of necessary tables within a table per class hierarchy. The policy will require minimal hooks into the core code, therefore reducing dependencies and risks of interruptions to other features. The general solution will be focused towards usage from JPA and not from core alone (although there is nothing stopping someone from using directly). That is, the testing of this feature will be done through JPA tests. No new tests will be added to core.test however we'll ensure that the existing LRG continues to pass after the implementation.

Metadata processing

The metadata processing will require a number of changes.

  1. When processing an entity that is within a table per class inheritance hierarchy, that entity will need to process the accessors from its superclasses (similarly as is currently done for mapped superclasses).
    1. @OneToOne
    2. @ManyToOne
    3. @OneToMany
    4. @ManyToMany
    5. @Id
    6. @Basic
    7. @BasicCollection
    8. @BasicMap
    9. etc ..
  2. Should entity level metadata should also be inherited and processed under the context of the inheritance subclass.
    1. @OptimisticLocking
    2. @Cache
    3. @IdClass
    4. @ChangeTracking
    5. @CopyPolicy
    6. @ReadOnly
    7. @ExistenceChecking
    8. @ExcludeDefaultListeners
    9. @ExcludeSuperclassListeners
    10. etc.
  3. For each mapping type (1-1, M-1, 1-M and M-M), the metadata processing of non-owning sides will need to be updated. When processing a non-owning side the list of source keys is grabbed from the target key list from the owning side. In a TABLE_PER_CLASS strategy we will need to clone and update those fields to use the correct database table from the inheritance subclass.
  4. Discriminator columns and values do no apply within a TABLE_PER_CLASS strategy; therefore they will be ignored if specified by the user. Should we log a warning message or just silently ignore them?

Core changes

The TABLE_PER_CLASS strategy will be created and will subclass a new AbstractInheritancePolicy. The AbstractInheritancePolicy will contain those items from the existing InheritancePolicy that are common to both policies. The InheritancePolicy, aside from having the common code extracted from it, will remain the same and will continue to provide our solution to the JOINED and SINGLE_TABLE inheritance strategies. These strategies require an indicator field.

Through-out the Eclipselink code, the descriptor.hasInheritance() method is heavily depended on. Typically this method should always be called before a getInheritance() call since the InheritancePolicy is lazy initialized within that method. This API will be changed/extended slightly to the following:

Method Behavior
hasInheritance() Returns true if an inheritance policy has been set. Does not indicate the specify type of policy.
getAbstractInheritancePolicy() Returns the general (common level) inheritance policy.
hasIndicatorInheritance() Returns true is an InheritancePolicy has been set
getInheritancePolicy() Assumes hasIndicatorInheritance() has been called before hand otherwise, there are two possible outcomes.
  1. If no policy has been set, an InheritancePolicy will be lazily initialized and returned.
  2. If a TablePerClassInheritancePolicy has been set, a ClassCastException will occur. In short EclipseLink, should never call this method before calling hasIndicatorInheritance()
hasTablePerClassInheritance() Returns true is a TablePerClassInhertiancePolicy has been set
getTablePerClassInheritance() Assumes hasTablePerClassInheritance() has been called before hand, otherwise, there are two possible outcomes.
  1. If no policy has been set, a TablePerClassInheritancePolicy will be lazily initialized and returned.
  2. If an InheritancePolicy has been set, a ClassCastException will occur. In short EclipseLink, should never call this method before calling hasTablePerClassInheritance()

TablePerClassInheritance policy core functionality

Essentially, the main purpose of the TablePerClassInheritancePolicy will be to control the trigger of selects to occur on the database with minimal hooks back into core. Each class in the inheritance hierarchy will have a TablePerClassInheritance policy set which contains a list of its immediate children classes.

Through the ‘to-many’ relational mappings we will prepare and cache selection queries for each subclass of the inheritance hierarchy. These selection queries will be created by cloning the source mapping and modifying its metadata. That cloned mapping will then be initialized and we will grab the selection query that was created as a result of that initialization. ‘To-one’ relational mappings are a little easier and will simply re-use the descriptor’s query manager read object query which uses the object builders primary key expression. There is currently no extra preparation for 'to-one' mappings. See ReadObjectQuery hook for more information (as I fear this could be limiting and potentially not adequate)

The way we build and cache the 'to-many' selection queries are as follows:

  • The clone and caching of these selection queries will be triggered from ObjectBuilder.buildAttributesIntoObject method before we execute the readFromRowIntoObject. This creation and cloning will occur if the reference descriptor from the mapping is within a table per class inheritance strategy and we haven’t already prepared a selection queries for the given source mapping.
  • The source mapping will be used as the cache key, therefore, we will need to add a reference from DatabaseQuery back to its source mapping. This reference will be set during the initializeSelectionQuery called during ForeignReferenceMapping initialize.

Currently extra preparation is only needed for ‘to-many’ mappings. Their prepare methods are as follows:

OneToMany prepare

protected void prepareOneToManySelectionQuery(OneToManyMapping sourceMapping, AbstractSession session) {
  // Do nothing if a selection query has already been built and cached for this source mapping.
  if (! selectionQueriesForAllObjects.containsKey(sourceMapping)) {

    // Clone the mapping because in reality that is what we have, that is, a 1-M mapping to
    // each class of the hierarchy.
    OneToManyMapping oneToMany = (OneToManyMapping) sourceMapping.clone();

   // Update the foreign key fields on the mapping. Basically, take the
   // table name off and let the descriptor figure it out.
    Vector<DatabaseField> targetForeignKeyFields = new Vector<DatabaseField>();     
    for (DatabaseField fkField : oneToMany.getTargetForeignKeysToSourceKeys().keySet()) {
      targetForeignKeyFields.add(new DatabaseField(fkField.getName()));
    }
                    
    // Update our foreign key fields and clear the key maps. They will
    // be populated again on initialize.
    oneToMany.setTargetForeignKeyFields(targetForeignKeyFields);
    oneToMany.getTargetForeignKeysToSourceKeys().clear();
    oneToMany.getSourceKeysToTargetForeignKeys().clear();
        
    // Set the new reference class
    oneToMany.setReferenceClass(getDescriptor().getJavaClass());
    oneToMany.setReferenceClassName(getDescriptor().getJavaClassName());

    // Force the selection criteria to be re-built.
    // This is a new flag that ensures the selection criteria is completely re-built and
    // not 'AND' with an the existing selection criteria that would have been copied
    // over in the clone.
    oneToMany.setForceInitializationOfSelectionCriteria(true);

    // Now initialize the mapping
    oneToMany.initialize(session);
            
    // The selection query should be initialized with all the right information now, 
    // cache it for quick retrieval.
    DatabaseQuery selectionQuery = oneToMany.getSelectionQuery();

    // By default its source mapping will be the cloned mapping, we need to set the 
    // actual source mapping so that we can look it up correctly.
    selectionQuery.setSourceMapping(sourceMapping);

    // Cache the selection query for this source mapping.
    selectionQueriesForAllObjects.put(sourceMapping, selectionQuery);
  }
}

ManyToMany prepare

protected void prepareManyToManySelectionQuery(ManyToManyMapping sourceMapping, AbstractSession session) {
  // Do nothing if a selection query has already been built and cached for this source mapping.
  if (! selectionQueriesForAllObjects.containsKey(sourceMapping)) {
     
    // Clone the mapping because in reality that is what we have, that is, a M-M mapping to
    // each class of the hierarchy.
    ManyToManyMapping manyToMany = (ManyToManyMapping) sourceMapping.clone();
    
    // The clone method will actually clone all the key fields as well so we need only
    // to update them in this case.
    for (DatabaseField keyField : manyToMany.getTargetKeyFields()) {
      keyField.setTable(new DatabaseTable());
    }
        
    // Set the new reference class
    manyToMany.setReferenceClass(getDescriptor().getJavaClass());
    manyToMany.setReferenceClassName(getDescriptor().getJavaClassName());

    // Force the selection criteria to be re-built.
    // This is a new flag that ensures the selection criteria is completely re-built and
    // not 'AND' with an the existing selection criteria that would have been copied
    // over in the clone.
    manyToMany.setForceInitializationOfSelectionCriteria(true);

    // Now initialize the mapping
    manyToMany.initialize(session);
            
    // The selection query should be initialized with all the right 
    // information now, cache it for quick retrieval.
    DatabaseQuery selectionQuery = manyToMany.getSelectionQuery();

    // By default its source mapping will be the cloned mapping, we
    // need to set the actual source mapping so that we can look it
    // up correctly.
    selectionQuery.setSourceMapping(sourceMapping);

    // Cache the selection query for this source mapping.
    selectionQueriesForAllObjects.put(sourceMapping, selectionQuery);
  }
}

The selection query from those prepare methods will then be executed through hooks from ReadObjectQuery and ReadAllQuery.

ReadObjectQuery hook

The first attempt to find the object will be made using the query’s reference descriptor. If no object is found and the reference descriptor is within a table per class inheritance hierarchy then through the policy we will fire off queries against the children classes.

...

row = getQueryMechanism().selectOneRow();

if (row == null && getDescriptor().hasTablePerClassInheritance()) {
  Object returnValue = getDescriptor().getTablePerClassInheritancePolicy().selectOneChildObject(this);
  setExecutionTime(System.currentTimeMillis());
  return returnValue;
}

...

Where the meat of the table per class inheritance policy code looks as follows:

Note: I currently re-used the descriptors query manager read object query. I wonder if this is good enough or if the original query should be cloned. I venture into that route and encountered some issues (namely expression fields pointing to incorrect tables etc).

/**
 * INTERNAL:
 * Select one object from a subclass.
 */
 public Object selectOneChildObject(ReadObjectQuery query) throws DescriptorException {
   // Go through the list of child descriptors to look for the object. Stop as soon
   // as we find it. Otherwise null will be returned. 
   for (ClassDescriptor childDescriptor : (Vector<ClassDescriptor>) getChildDescriptors()) {
     Object object = childDescriptor.getTablePerClassInheritancePolicy().selectOneObject(query);
      
    // We found an object, stop querying.      
     if (object != null) {
       return object;
     }
   }

   return null;
 }
    
 /**
  * INTERNAL:
  * Select one object of any concrete subclass.
  * One to one mappings, we'll just clone the translation row, update only 
  * the primary key fields and re-execute as needed (until we find the 
  * object). The reason I say only the primary key fields is because we may
  * be dealing with a target foreign key relationship and the primary key
  * field may or may not be available. If it is not available, we won't fire
  * the query.
  */
  protected Object selectOneObject(ReadObjectQuery query) throws DescriptorException {
    Object result = null;
        
    // We have to update the translation row to be to the correct field.
    AbstractRecord translationRow = (AbstractRecord) query.getTranslationRow().clone();
    Vector allFields = new Vector();
        
    // If we don't find a pk field for our descriptor then obviously there 
    // is no need to execute a query. This case will hit when dealing with
    // one to one target foreign keys.
    boolean pkFound = false;
        
    for (DatabaseField field : (Vector<DatabaseField>) translationRow.getFields()) {
      if (isPrimaryKeyField(field)) {
        // Remove the table and let the descriptor figure it out.
        allFields.add(new DatabaseField(field.getName()));
        pkFound = true;
      } else {
        primaryKeyFields.add(field);
      }
    }
        
    if (pkFound) {
      translationRow.getFields().clear();
      translationRow.getFields().addAll(allFields);
      result = query.getSession().executeQuery(getDescriptor().getQueryManager().getReadObjectQuery(), translationRow);
    }
        
    return result;
  }

ReadAllQuery hook

After executing the initial query and gathering the results, before returning we will check to see if the reference descriptor is part of table per class inheritance policy and trigger the prepared selection queries from the child classes for the source mapping associated with the query. Note: If the query execution is as a result of a named query, selection queries will not have been built. We therefore, clone the query and re-execute it against the child class/descriptor.

...

// Add the other (already registered) results and return them.
if (getDescriptor().hasTablePerClassInheritance()) {
  result = containerPolicy.concatenateContainers(result, getDescriptor().getTablePerClassInheritancePolicy().selectAllChildObjects(this)); 
}

...

Where the meat of the table per class inheritance policy code looks as follows:

/**
 * INTERNAL:
 * Select all objects from this descriptors immediate children in a
 * table per class hierarchy. This is accomplished by selecting all of 
 * the sub classes and then merging the objects.
 */
 public Object selectAllChildObjects(ReadAllQuery query) throws DatabaseException {
   ContainerPolicy containerPolicy = query.getContainerPolicy();
   Object objects = containerPolicy.containerInstance(1);
        
   for (ClassDescriptor childDescriptor : (Vector<ClassDescriptor>) getChildDescriptors()) {
     objects = containerPolicy.concatenateContainers(objects, childDescriptor.getTablePerClassInheritancePolicy().selectAllObjects(query));
   }

   return objects;
 }
    
 /**
  * INTERNAL:
  */
  protected Object selectAllObjects(ReadAllQuery query) {
    Object results; 
        
    // If we came from a source mapping the execute the selection query
    // we prepared from it.
    if (selectionQueriesForAllObjects.containsKey(query.getSourceMapping())) {
      return query.getExecutionSession().executeQuery(selectionQueriesForAllObjects.get(query.getSourceMapping()), query.getTranslationRow());  
      } else {
        // A named query has been executed and we therefore have no 
        // selection query to execute. Let's query based on what we have.
        ReadAllQuery readAllQuery = (ReadAllQuery) query.clone();
        readAllQuery.setDescriptor(getDescriptor());
        readAllQuery.setReferenceClass(getDescriptor().getJavaClass());
        readAllQuery.setReferenceClassName(getDescriptor().getJavaClassName());
            
        results = query.getExecutionSession().executeQuery(readAllQuery, query.getTranslationRow());
      }
        
      return results;
  }

Work schedule

  1. Update core
    approx 15 days - create new table per class inhertiance policy with core hooks
  2. Update Metadata processing
    approx 5 days - processing of inherited mappings and updates to target key fields
  3. Develop model for testing
    approx 5 days - full model for testing 1-1, M-1, 1-M and M-M along with named queries

Future

The following list contains item that were not tested, addressed or just things that came up during the design/implementation. There are however, some things that we may get for 'free'. Or partially get for 'free'

  • Multiple tables - Should probably test this since we avoid setting table names when preparing selection queries (in turn letting the descriptor figure it out).
  • Min and max result - Should partial work in that min and max should be applied to individual queries (but not to the full result list as a whole).
  • Optimistic/pessimistic locking
  • Caching
  • Cursors
  • ProjectClassGenerator
  • XMLWriter
  • OXM mappings
  • EntityResult

Back to the top