Skip to main content

Notice: This Wiki is now read only and edits are no longer 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"

(ManyToMany prepare)
(Table Per Class)
 
(68 intermediate revisions by the same user not shown)
Line 1: Line 1:
 
= Table Per Class =
 
= Table Per Class =
[[EclipseLink/Development/JPA_2.0 | JPA 2.0 Root]] |  [http://bugs.eclipse.org/249860 Enhancement Request]
+
[[EclipseLink/Development/JPA_2.0 | JPA 2.0 Root]] |  {{bug|249860}}
 +
 
 +
[[Talk:EclipseLink/Development/JPA_1.0/table_per_class|Discussion]]
  
 
==Issue Summary==
 
==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:
+
Implement a TABLE_PER_CLASS inheritance strategy that is currently an optional feature in the JPA spec. The initial implement should support the following and provide a testcase(s) for each:
 +
*One to one mapping (both target and source foreign key)
 +
*Many to one mapping
 +
*One to many mapping
 +
*Many to many mapping
 +
*Named query support
  
*One to one (both target and source foreign key)
+
Other items that the implementation should get for free or partially free are as follows (with minimal testing):
*Many to one
+
* Multiple tables
*One to many
+
* Batch reading
*Many to many
+
* Optimistic/Pessimistic locking
 +
* Caching
  
And the new policy 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.
+
The following items will NOT be supported with this feature:
 +
* Joins
 +
* Join fetch
 +
* Update-all
 +
* Delete-all
 +
* Polymorphic queries across mappings.
 +
* Function operators, min, max etc.
 +
* Cursors
 +
* Report query
 +
* OXM Mappings
 +
* XML Writer
 +
* Project class generator
  
 
Also, see JPA 2.0 section 2.12.2 for more details.
 
Also, see JPA 2.0 section 2.12.2 for more details.
Line 17: Line 36:
 
==General solution==
 
==General solution==
  
The general solution requires two main changes. Internally, we will need a new TablePerClassInheritance policy, which will handle the querying of objects 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 requires two main changes. Internally, we will need a new TablePerClassPolicy, 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==
 
==Metadata processing==
Line 24: Line 43:
  
 
#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).  
#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 have the correct database table from the inheritance subclass.
+
## @OneToOne
 +
## @ManyToOne
 +
## @OneToMany
 +
## @ManyToMany
 +
## @Id
 +
## @Basic
 +
## @BasicCollection
 +
## @BasicMap
 +
## etc ..
 +
# Entity level metadata is currently not inherited and processed for subclasses. Internally if specific items are needed or required to be set on a subclass descriptor, it should be addressed in the TablePerClassInheritancePolicy initialize.
 +
##@OptimisticLocking
 +
##@Cache - cache can only be set on the root of the inheritance hierarchy.
 +
##@IdClass - Should only be specified on the root.
 +
##@ChangeTracking - process per class and applies only to the class it is specified on. It is not inherited.
 +
##@CopyPolicy - processed per class and applies only to the class it is specified on. It is not inherited.
 +
##@ReadOnly - Currently ignored on inheritance subclasses. Can only be specified on the root.
 +
##@ExistenceChecking - processed per class and applies only to the class it is specified on. It is not inherited.
 +
##@ExcludeDefaultListeners - processed per class and applies only to the class it is specified on. It is not inherited.
 +
##@ExcludeSuperclassListeners - process per class and applies only to the class it is specified on. It is not inherited.
 +
## etc.
 +
#For each relationship 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.
 
#Discriminator columns and values do no apply within a TABLE_PER_CLASS strategy; therefore they will be ignored if specified by the user.
 
 
  
 
==Core changes==
 
==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.
+
The TablePerClassPolicy will be created and will subclass a the existing InterfacePolicy where we will re-use as much code as possible to achieve the desired functionality. The InheritancePolicy 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:
+
The following API will be added to ClassDescriptor:
  
 
{|{{BMTableStyle}}  
 
{|{{BMTableStyle}}  
Line 40: Line 77:
 
! Behavior  
 
! Behavior  
 
|-  
 
|-  
| hasInheritance()  
+
| hasTablePerClassPolicy()  
| Returns true if an inheritance policy has been set. Does not indicate the specify type of policy.
+
| Returns true is a TablePerClassPolicy has been set and should always be called before a getTablePerClass policy is made.
 
|-  
 
|-  
| getAbstractInheritancePolicy()
+
| getTablePerClassPolicy()  
| Returns the general (common level) inheritance policy.
+
| Assumes hasTablePerClassPolicy() has been called before hand otherwise, there are two possible outcomes.
|-
+
# If no policy has been set, an TablePerClassPolicy will be lazily initialized and returned.  
| hasIndicatorInheritance()
+
# If an InterfacePolicy has been set, a ClassCastException will occur. In short EclipseLink, should never call this method before calling hasTablePerClassPolicy()
| Returns true is an InheritancePolicy has been set
+
|-
+
| getInheritancePolicy()  
+
| Assumes hasIndicatorInheritance() has been called before hand otherwise, there are two possible outcomes.
+
# If no policy has been set, an InheritancePolicy will be lazily initialized and returned.
+
# If a TablePerClassInheritancePolicy has been set, a ClassCastException will occur. In short EclipseLink, should never call this method before calling hasIndicatorInhertiance()
+
|-
+
| hasTablePerClassInheritance()
+
| Returns true is a TablePerClassInhertiancePolicy has been set
+
|-
+
| getTablePerClassInheritance()
+
| Assumes hasTablePerClassInheritance() has been called before hand, otherwise, there are two possible outcomes.
+
# If no policy has been set, a TablePerClassInheritancePolicy will be lazily initialized and returned.  
+
# 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===
+
Note: That setting the TablePerClassPolicy will be done through the existing setInterfacePolicy() method.
  
Essentially, the main purpose of the TablePerClassInheritancePolicy will be to control and cause the multiple selects to occur on the database. 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 we will simply re-use the descriptor’s query manager read object query.
+
===TablePerClassPolicy core functionality===
  
The way we build and cache the selection queries are as follows:
+
Essentially, the main purpose of the TablePerClassPolicy 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 TablePerClassPolicy set which contains a list of its immediate children classes and a reference to its parent descriptor.
  
*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 this mapping.  
+
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
 +
 
 +
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.
 
*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.
  
Line 78: Line 105:
 
<pre>
 
<pre>
 
protected void prepareOneToManySelectionQuery(OneToManyMapping sourceMapping, AbstractSession session) {
 
protected void prepareOneToManySelectionQuery(OneToManyMapping sourceMapping, AbstractSession session) {
   // See if we already built a selection query for the given mapping.  
+
   // Do nothing if a selection query has already been built and cached for this source mapping.
 
   if (! selectionQueriesForAllObjects.containsKey(sourceMapping)) {
 
   if (! selectionQueriesForAllObjects.containsKey(sourceMapping)) {
  
Line 85: Line 112:
 
     OneToManyMapping oneToMany = (OneToManyMapping) sourceMapping.clone();
 
     OneToManyMapping oneToMany = (OneToManyMapping) sourceMapping.clone();
  
    // Create new foreign key fields. The targetForeignKeyFields are not cloned in the mapping
+
  // Update the foreign key fields on the mapping. Basically, take the
    // clone() call therefore we need to clone each one before modifying it.
+
  // table name off and let the descriptor figure it out.
     Vector<DatabaseField> targetForeignKeyFields = new Vector<DatabaseField>();
+
     Vector<DatabaseField> targetForeignKeyFields = new Vector<DatabaseField>();    
       
+
 
     for (DatabaseField fkField : oneToMany.getTargetForeignKeysToSourceKeys().keySet()) {
 
     for (DatabaseField fkField : oneToMany.getTargetForeignKeysToSourceKeys().keySet()) {
       DatabaseField newFKField = new DatabaseField(fkField.getName());
+
       targetForeignKeyFields.add(new DatabaseField(fkField.getName()));
      newFKField.setTableName(getDescriptor().getTableName());
+
      targetForeignKeyFields.add(newFKField);
+
 
     }
 
     }
 
                      
 
                      
Line 132: Line 156:
 
<pre>
 
<pre>
 
protected void prepareManyToManySelectionQuery(ManyToManyMapping sourceMapping, AbstractSession session) {
 
protected void prepareManyToManySelectionQuery(ManyToManyMapping sourceMapping, AbstractSession session) {
   // See if we already built a selection query for the given mapping.  
+
   // Do nothing if a selection query has already been built and cached for this source mapping.
 
   if (! selectionQueriesForAllObjects.containsKey(sourceMapping)) {
 
   if (! selectionQueriesForAllObjects.containsKey(sourceMapping)) {
 
      
 
      
// Clone the mapping because in reality that is what we have, that is, a 1-M mapping to
+
    // Clone the mapping because in reality that is what we have, that is, a M-M mapping to
 
     // each class of the hierarchy.
 
     // each class of the hierarchy.
 
     ManyToManyMapping manyToMany = (ManyToManyMapping) sourceMapping.clone();
 
     ManyToManyMapping manyToMany = (ManyToManyMapping) sourceMapping.clone();
Line 142: Line 166:
 
     // to update them in this case.
 
     // to update them in this case.
 
     for (DatabaseField keyField : manyToMany.getTargetKeyFields()) {
 
     for (DatabaseField keyField : manyToMany.getTargetKeyFields()) {
       keyField.setTableName(getDescriptor().getTableName());
+
       keyField.setTable(new DatabaseTable());
 
     }
 
     }
 
          
 
          
Line 167: Line 191:
 
     selectionQuery.setSourceMapping(sourceMapping);
 
     selectionQuery.setSourceMapping(sourceMapping);
  
 
+
    // Cache the selection query for this source mapping.
 
     selectionQueriesForAllObjects.put(sourceMapping, selectionQuery);
 
     selectionQueriesForAllObjects.put(sourceMapping, selectionQuery);
 
   }
 
   }
Line 173: Line 197:
 
</pre>
 
</pre>
  
The selection query from those prepare methods will then be executed through hooks into the ObjectLevelReadQuery and ObjectLevelReadAllQuery.
+
The selection query from those prepare methods will then be executed through hooks from ReadObjectQuery and ReadAllQuery.
  
====ObjectLevelReadQuery====
+
====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 chain the execution of the prepared selection queries.
+
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.
  
 
<pre>
 
<pre>
if (row == null && getDescriptor().hasTablePerClassInheritance()) {
+
...
   Object returnValue = getDescriptor().getTablePerClassInheritancePolicy().selectOneChildObject(this);
+
 
   setExecutionTime(System.currentTimeMillis());
+
if (getDescriptor().isDescriptorForInterface()  || getDescriptor().hasTablePerClassPolicy()) {
  return returnValue;
+
   Object returnValue = getDescriptor().getInterfacePolicy().selectOneObjectUsingMultipleTableSubclassRead(this);
 +
           
 +
   if (getDescriptor().hasTablePerClassPolicy() && returnValue == null) {
 +
    // let it fall through to query the root.
 +
  } else {
 +
    setExecutionTime(System.currentTimeMillis());
 +
    return returnValue;
 +
  }
 
}
 
}
 +
 +
...
 
</pre>
 
</pre>
  
Where the meat of the policy select looks as follows:
+
Where the meat of the table per class 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).
  
 
<pre>
 
<pre>
 
/**
 
/**
* INTERNAL:
+
  * INTERNAL:
* Select one object of any concrete subclass.
+
  * Select one object of any concrete subclass.
* One to one mappings, we'll just clone the query, update the primary key
+
  */
* fields and re-execute as needed (until we find the object).
+
@Override
*/
+
public Object selectOneObjectUsingMultipleTableSubclassRead(ReadObjectQuery query) throws DatabaseException, QueryException {
public Object selectOneChildObject(ReadObjectQuery query) throws DescriptorException {
+
  for (ClassDescriptor childDescriptor : (Vector<ClassDescriptor>) getChildDescriptors()) {
  // Go through the list of child descriptors to look for the object. Stop as soon
+
    Object object = childDescriptor.getTablePerClassPolicy().selectOneObject(query);
  // as we find it. Otherwise null will be returned.
+
               
  for (ClassDescriptor childDescriptor : (Vector<ClassDescriptor>) getChildDescriptors()) {
+
    // Quit as soon as once child object has been found.
    Object object = childDescriptor.getTablePerClassInheritancePolicy().selectOneObject(query);
+
    if (object != null) {
           
+
      return object;
    if (object != null) {
+
    }
      return object;
+
  }
    }
+
  }
+
  
  return null;
+
  return null;
}
+
}
 
      
 
      
 
  /**
 
  /**
Line 225: Line 258:
 
     // We have to update the translation row to be to the correct field.
 
     // We have to update the translation row to be to the correct field.
 
     AbstractRecord translationRow = (AbstractRecord) query.getTranslationRow().clone();
 
     AbstractRecord translationRow = (AbstractRecord) query.getTranslationRow().clone();
     Vector primaryKeyFields = new Vector();
+
     Vector allFields = new Vector();
 
          
 
          
 
     // If we don't find a pk field for our descriptor then obviously there  
 
     // If we don't find a pk field for our descriptor then obviously there  
Line 234: Line 267:
 
     for (DatabaseField field : (Vector<DatabaseField>) translationRow.getFields()) {
 
     for (DatabaseField field : (Vector<DatabaseField>) translationRow.getFields()) {
 
       if (isPrimaryKeyField(field)) {
 
       if (isPrimaryKeyField(field)) {
         DatabaseField newPKField = new DatabaseField(field.getName());
+
         // Remove the table and let the descriptor figure it out.
         newPKField.setTableName(getDescriptor().getTableName());
+
         allFields.add(new DatabaseField(field.getName()));
        primaryKeyFields.add(newPKField);
+
 
         pkFound = true;
 
         pkFound = true;
 
       } else {
 
       } else {
Line 245: Line 277:
 
     if (pkFound) {
 
     if (pkFound) {
 
       translationRow.getFields().clear();
 
       translationRow.getFields().clear();
       translationRow.getFields().addAll(primaryKeyFields);
+
       translationRow.getFields().addAll(allFields);
 
       result = query.getSession().executeQuery(getDescriptor().getQueryManager().getReadObjectQuery(), translationRow);
 
       result = query.getSession().executeQuery(getDescriptor().getQueryManager().getReadObjectQuery(), translationRow);
 
     }
 
     }
Line 253: Line 285:
 
</pre>
 
</pre>
  
====ObjectLevelReadAllQuery====
+
==== 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 for the source mapping.
+
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 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 call up to InterfacePolicy to execute a query for us.
  
 
<pre>
 
<pre>
 +
...
 +
 
// Add the other (already registered) results and return them.
 
// Add the other (already registered) results and return them.
if (getDescriptor().hasTablePerClassInheritance()) {
+
if (getDescriptor().hasTablePerClassPolicy()) {
   result = containerPolicy.concatenateContainers(result, getDescriptor().getTablePerClassInheritancePolicy().selectAllChildObjects(this));  
+
   result = containerPolicy.concatenateContainers(result, getDescriptor().getTablePerClassPolicy().selectAllObjectsUsingMultipleTableSubclassRead(this));
 
}
 
}
 +
 +
...
 +
 
</pre>
 
</pre>
  
Where the meat of the policy select looks as follows:
+
Where the meat of the table per class policy code looks as follows:
 +
 
 +
Note: The selectAllObjects call is made from selectAllObjectsUsingMultipleTableSubclassRead which is defined on InterfacePolicy. The TablePerClassPolicy overrides that method.
  
 
<pre>
 
<pre>
 +
 
/**
 
/**
* INTERNAL:
+
  * INTERNAL:
* Select all objects from this descriptors immediate children in a
+
  * Select all objects for a concrete descriptor.
* table per class hierarchy. This is accomplished by selecting all of
+
  */
* the sub classes and then merging the objects.
+
@Override
*/
+
protected Object selectAllObjects(ReadAllQuery query) {  
public Object selectAllChildObjects(ReadAllQuery query) throws DatabaseException {
+
  // If we came from a source mapping the execute the selection query
  ContainerPolicy containerPolicy = query.getContainerPolicy();
+
  // we prepared from it.
  Object objects = containerPolicy.containerInstance(1);
+
  if (selectionQueriesForAllObjects.containsKey(query.getSourceMapping())) {
       
+
    return query.getExecutionSession().executeQuery(selectionQueriesForAllObjects.get(query.getSourceMapping()), query.getTranslationRow()); 
  for (ClassDescriptor childDescriptor : (Vector<ClassDescriptor>) getChildDescriptors()) {
+
  } else {
    objects = containerPolicy.concatenateContainers(objects, childDescriptor.getTablePerClassInheritancePolicy().selectAllObjects(query));
+
    return super.selectAllObjects(query);
  }
+
  }
 +
}
  
  return objects;
+
// InterfacePolicy - selectAllObjects method
}
+
 
   
+
/**
/**
+
 
   * INTERNAL:
 
   * INTERNAL:
 +
  * Select all objects for a concrete descriptor.
 
   */
 
   */
  protected Object selectAllObjects(ReadAllQuery query) {
+
protected Object selectAllObjects(ReadAllQuery query) {
    Object results;  
+
  ReadAllQuery concreteQuery = (ReadAllQuery) query.deepClone();
 +
  concreteQuery.setReferenceClass(descriptor.getJavaClass());
 +
  concreteQuery.setDescriptor(descriptor);
 
          
 
          
    // If we didn't come from a buildAttributeIntoObject call we will not have a selection query  
+
  // Avoid cloning the query again ...
    // to issue. That is, a custom find all query (NamedQuery) has been executed. Let's build
+
  concreteQuery.setIsExecutionClone(true);
    // based on what we have.
+
  concreteQuery.getExpressionBuilder().setQueryClassAndDescriptor(descriptor.getJavaClass(), descriptor);
    if (selectionQueriesForAllObjects.containsKey(query.getSourceMapping())) {
+
      return query.getExecutionSession().executeQuery(selectionQueriesForAllObjects.get(query.getSourceMapping()), query.getTranslationRow());
+
      } else {
+
        ReadAllQuery readAllQuery = (ReadAllQuery) query.clone();
+
        readAllQuery.setDescriptor(getDescriptor());
+
        readAllQuery.setReferenceClass(getDescriptor().getJavaClass());
+
 
              
 
              
        results = query.getExecutionSession().executeQuery(readAllQuery, query.getTranslationRow());
+
  // Update the selection criteria if needed as well and don't lose the translation row.
      }
+
  if (concreteQuery.getQueryMechanism().getSelectionCriteria() != null) {
       
+
    concreteQuery.getQueryMechanism().getSelectionCriteria().getBuilder().setQueryClassAndDescriptor(descriptor.getJavaClass(), descriptor);
      return results;
+
    return query.getSession().executeQuery(concreteQuery, query.getTranslationRow());
 
   }
 
   }
 +
       
 +
  return query.getSession().executeQuery(concreteQuery);
 +
}
 +
   
 
</pre>
 
</pre>
  
==Work Required==
+
==Work schedule==
 
# Update core
 
# Update core
#: approx 15 days - create new table per class inhertiance policy with core hooks
+
#: approx 15 days - create new table per class policy with core hooks
 
# Update Metadata processing
 
# Update Metadata processing
 
#: approx 5 days - processing of inherited mappings and updates to target key fields
 
#: approx 5 days - processing of inherited mappings and updates to target key fields
Line 317: Line 358:
 
==Future==
 
==Future==
  
*Multiple tables
+
No future work is planned for this feature. What is included in this implementation is currently final.
*Cursors
+
*Min and max result
+
*ProjectClassGenerator
+
*XMLWriter
+
*OXM mappings
+
*EntityResult
+
 
+
==Things we get for free==
+
 
+
*Optimistic/Pessimistic Locking
+
*Caching
+

Latest revision as of 07:00, 14 January 2009

Table Per Class

JPA 2.0 Root | bug 249860

Discussion

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 and provide a testcase(s) for each:

  • One to one mapping (both target and source foreign key)
  • Many to one mapping
  • One to many mapping
  • Many to many mapping
  • Named query support

Other items that the implementation should get for free or partially free are as follows (with minimal testing):

  • Multiple tables
  • Batch reading
  • Optimistic/Pessimistic locking
  • Caching

The following items will NOT be supported with this feature:

  • Joins
  • Join fetch
  • Update-all
  • Delete-all
  • Polymorphic queries across mappings.
  • Function operators, min, max etc.
  • Cursors
  • Report query
  • OXM Mappings
  • XML Writer
  • Project class generator

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 TablePerClassPolicy, 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. Entity level metadata is currently not inherited and processed for subclasses. Internally if specific items are needed or required to be set on a subclass descriptor, it should be addressed in the TablePerClassInheritancePolicy initialize.
    1. @OptimisticLocking
    2. @Cache - cache can only be set on the root of the inheritance hierarchy.
    3. @IdClass - Should only be specified on the root.
    4. @ChangeTracking - process per class and applies only to the class it is specified on. It is not inherited.
    5. @CopyPolicy - processed per class and applies only to the class it is specified on. It is not inherited.
    6. @ReadOnly - Currently ignored on inheritance subclasses. Can only be specified on the root.
    7. @ExistenceChecking - processed per class and applies only to the class it is specified on. It is not inherited.
    8. @ExcludeDefaultListeners - processed per class and applies only to the class it is specified on. It is not inherited.
    9. @ExcludeSuperclassListeners - process per class and applies only to the class it is specified on. It is not inherited.
    10. etc.
  3. For each relationship 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.

Core changes

The TablePerClassPolicy will be created and will subclass a the existing InterfacePolicy where we will re-use as much code as possible to achieve the desired functionality. The InheritancePolicy 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.

The following API will be added to ClassDescriptor:

Method Behavior
hasTablePerClassPolicy() Returns true is a TablePerClassPolicy has been set and should always be called before a getTablePerClass policy is made.
getTablePerClassPolicy() Assumes hasTablePerClassPolicy() has been called before hand otherwise, there are two possible outcomes.
  1. If no policy has been set, an TablePerClassPolicy will be lazily initialized and returned.
  2. If an InterfacePolicy has been set, a ClassCastException will occur. In short EclipseLink, should never call this method before calling hasTablePerClassPolicy()

Note: That setting the TablePerClassPolicy will be done through the existing setInterfacePolicy() method.

TablePerClassPolicy core functionality

Essentially, the main purpose of the TablePerClassPolicy 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 TablePerClassPolicy set which contains a list of its immediate children classes and a reference to its parent descriptor.

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

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.

...

if (getDescriptor().isDescriptorForInterface()  || getDescriptor().hasTablePerClassPolicy()) {
  Object returnValue = getDescriptor().getInterfacePolicy().selectOneObjectUsingMultipleTableSubclassRead(this);
            
  if (getDescriptor().hasTablePerClassPolicy() && returnValue == null) {
    // let it fall through to query the root.
  } else {
    setExecutionTime(System.currentTimeMillis());
    return returnValue;
  }
}

...

Where the meat of the table per class 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 of any concrete subclass.
  */
@Override
public Object selectOneObjectUsingMultipleTableSubclassRead(ReadObjectQuery query) throws DatabaseException, QueryException {
  for (ClassDescriptor childDescriptor : (Vector<ClassDescriptor>) getChildDescriptors()) {
    Object object = childDescriptor.getTablePerClassPolicy().selectOneObject(query);
                
    // Quit as soon as once child object has been found.
    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 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 call up to InterfacePolicy to execute a query for us.

...

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

...

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

Note: The selectAllObjects call is made from selectAllObjectsUsingMultipleTableSubclassRead which is defined on InterfacePolicy. The TablePerClassPolicy overrides that method.


/**
  * INTERNAL:
  * Select all objects for a concrete descriptor.
  */
@Override
protected Object selectAllObjects(ReadAllQuery query) { 
  // 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 {
    return super.selectAllObjects(query);
  }
}

// InterfacePolicy - selectAllObjects method

/**
  * INTERNAL:
  * Select all objects for a concrete descriptor.
  */
protected Object selectAllObjects(ReadAllQuery query) {
  ReadAllQuery concreteQuery = (ReadAllQuery) query.deepClone();
  concreteQuery.setReferenceClass(descriptor.getJavaClass());
  concreteQuery.setDescriptor(descriptor);
        
  // Avoid cloning the query again ...
  concreteQuery.setIsExecutionClone(true);
  concreteQuery.getExpressionBuilder().setQueryClassAndDescriptor(descriptor.getJavaClass(), descriptor);
            
  // Update the selection criteria if needed as well and don't lose the translation row.
  if (concreteQuery.getQueryMechanism().getSelectionCriteria() != null) {
    concreteQuery.getQueryMechanism().getSelectionCriteria().getBuilder().setQueryClassAndDescriptor(descriptor.getJavaClass(), descriptor);
    return query.getSession().executeQuery(concreteQuery, query.getTranslationRow());
  }
        
  return query.getSession().executeQuery(concreteQuery);
}
    

Work schedule

  1. Update core
    approx 15 days - create new table per class 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

No future work is planned for this feature. What is included in this implementation is currently final.

Back to the top