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"

(ManyToMany prepare)
(ManyToMany prepare)
Line 150: Line 150:
  
 
     // Force the selection criteria to be re-built.
 
     // 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);
 
     manyToMany.setForceInitializationOfSelectionCriteria(true);
  

Revision as of 11:28, 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

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.

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 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.

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).
  2. 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.
  3. 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 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 hasIndicatorInhertiance()
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 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.

The way we build and cache the 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 this 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) {
  // See if we already built a selection query for the given 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();

    // Create new foreign key fields. The targetForeignKeyFields are not cloned in the mapping
    // clone() call therefore we need to clone each one before modifying it.
    Vector<DatabaseField> targetForeignKeyFields = new Vector<DatabaseField>();
        
    for (DatabaseField fkField : oneToMany.getTargetForeignKeysToSourceKeys().keySet()) {
      DatabaseField newFKField = new DatabaseField(fkField.getName());
      newFKField.setTableName(getDescriptor().getTableName());
      targetForeignKeyFields.add(newFKField);
    }
                    
    // 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) {
  // See if we already built a selection query for the given 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.
    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.setTableName(getDescriptor().getTableName());
    }
        
    // 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);


    selectionQueriesForAllObjects.put(sourceMapping, selectionQuery);
  }
}

The selection query from those prepare methods will then be executed through hooks into the ObjectLevelReadQuery and ObjectLevelReadAllQuery.

ObjectLevelReadQuery

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.

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

Where the meat of the policy select looks as follows:

/**
 * INTERNAL:
 * 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). 
 */
 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);
            
     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 primaryKeyFields = 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)) {
        DatabaseField newPKField = new DatabaseField(field.getName());
        newPKField.setTableName(getDescriptor().getTableName());
        primaryKeyFields.add(newPKField);
        pkFound = true;
      } else {
        primaryKeyFields.add(field);
      }
    }
        
    if (pkFound) {
      translationRow.getFields().clear();
      translationRow.getFields().addAll(primaryKeyFields);
      result = query.getSession().executeQuery(getDescriptor().getQueryManager().getReadObjectQuery(), translationRow);
    }
        
    return result;
  }

ObjectLevelReadAllQuery

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.

// 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 policy select 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 didn't come from a buildAttributeIntoObject call we will not have a selection query 
    // to issue. That is, a custom find all query (NamedQuery) has been executed. Let's build 
    // based on what we have.
    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());
      }
        
      return results;
  }

Work Required

  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

  • Multiple tables
  • Cursors
  • Min and max result
  • ProjectClassGenerator
  • XMLWriter
  • OXM mappings
  • EntityResult

Things we get for free

  • Optimistic/Pessimistic Locking
  • Caching

Back to the top