EclipseLink/Development/JPA 1.0/table per class

From Eclipsepedia

Jump to: navigation, search

Contents

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.