/******************************************************************************* | |
* Copyright (c) 1998, 2013 Oracle and/or its affiliates. All rights reserved. | |
* This program and the accompanying materials are made available under the | |
* terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0 | |
* which accompanies this distribution. | |
* The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html | |
* and the Eclipse Distribution License is available at | |
* http://www.eclipse.org/org/documents/edl-v10.php. | |
* | |
* Contributors: | |
* Oracle - initial API and implementation from Oracle TopLink | |
******************************************************************************/ | |
package org.eclipse.persistence.mappings.structures; | |
import java.util.*; | |
import org.eclipse.persistence.internal.helper.*; | |
import org.eclipse.persistence.internal.sessions.*; | |
import org.eclipse.persistence.internal.databaseaccess.*; | |
import org.eclipse.persistence.internal.expressions.ObjectExpression; | |
import org.eclipse.persistence.mappings.*; | |
import org.eclipse.persistence.queries.*; | |
import org.eclipse.persistence.exceptions.*; | |
import org.eclipse.persistence.expressions.*; | |
import org.eclipse.persistence.internal.queries.*; | |
/** | |
* <p><b>Purpose:</b> | |
* Nested tables are similar to <code>VARRAYs</code> except internally they store their information in a separate table | |
* from their parent structure's table. The advantage of nested tables is that they support querying and | |
* joining much better than varrays that are inlined into the parent table. A nested table is typically | |
* used to represent a one-to-many or many-to-many relationship of references to another independent | |
* structure. TopLink supports storing a nested table of values into a single field. | |
* | |
* <p>NOTE: Only Oracle8i supports nested tables type. | |
* | |
* @since TOPLink/Java 2.5 | |
*/ | |
public class NestedTableMapping extends CollectionMapping { | |
protected DatabaseMapping nestedMapping; | |
/** A ref is always stored in a single field. */ | |
protected DatabaseField field; | |
/** Arrays require a structure name, this is the ADT defined for the VARRAY. */ | |
protected String structureName; | |
/** | |
* PUBLIC: | |
* Default constructor. | |
*/ | |
public NestedTableMapping() { | |
super(); | |
} | |
/** | |
* INTERNAL: | |
* In case Query By Example is used, this method builds and returns an expression that | |
* corresponds to a single attribute and it's value. | |
*/ | |
public Expression buildExpression(Object queryObject, QueryByExamplePolicy policy, Expression expressionBuilder, Map processedObjects, AbstractSession session) { | |
if (policy.shouldValidateExample()){ | |
throw QueryException.unsupportedMappingQueryByExample(queryObject.getClass().getName(), this); | |
} | |
return null; | |
} | |
/** | |
* INTERNAL: | |
* The mapping clones itself to create deep copy | |
*/ | |
public Object clone() { | |
NestedTableMapping clone = (NestedTableMapping)super.clone(); | |
return clone; | |
} | |
/** | |
* Returns all the aggregate fields. | |
*/ | |
protected Vector collectFields() { | |
Vector fields = new Vector(1); | |
fields.addElement(getField()); | |
return fields; | |
} | |
/** | |
* INTERNAL: | |
* Returns the field which this mapping represents. | |
*/ | |
public DatabaseField getField() { | |
return field; | |
} | |
/** | |
* PUBLIC: | |
* Return the name of the field this mapping represents. | |
*/ | |
public String getFieldName() { | |
return getField().getName(); | |
} | |
/** | |
* INTERNAL: | |
* Join criteria is created to read target records (nested table) from the table. | |
*/ | |
@Override | |
public Expression getJoinCriteria(ObjectExpression context, Expression base) { | |
return context.ref().equal(base.value()); | |
} | |
/** | |
* PUBLIC: | |
* Return the structure name of the nestedTable. | |
* This is the name of the user defined data type as defined on the database. | |
*/ | |
public String getStructureName() { | |
return structureName; | |
} | |
/** | |
* INTERNAL: | |
* The returns if the mapping has any constraint dependencies, such as foreign keys and join tables. | |
*/ | |
@Override | |
public boolean hasConstraintDependency() { | |
return true; | |
} | |
/** | |
* INTERNAL: | |
* Initialize the mapping. | |
*/ | |
@Override | |
public void initialize(AbstractSession session) throws DescriptorException { | |
super.initialize(session); | |
if (getField() == null) { | |
throw DescriptorException.fieldNameNotSetInMapping(this); | |
} | |
// For bug 2730536 convert the field to be an ObjectRelationalDatabaseField. | |
ObjectRelationalDatabaseField field = (ObjectRelationalDatabaseField)getField(); | |
field.setSqlType(java.sql.Types.ARRAY); | |
field.setSqlTypeName(getStructureName()); | |
setField(getDescriptor().buildField(getField())); | |
} | |
/** | |
* INTERNAL: | |
* Selection criteria is created to read target records (nested table) from the table. | |
*/ | |
protected void initializeSelectionCriteria(AbstractSession session) { | |
Expression exp1; | |
Expression exp2; | |
ExpressionBuilder builder = new ExpressionBuilder(); | |
Expression queryKey = builder.getManualQueryKey(getAttributeName(), getDescriptor()); | |
exp1 = builder.ref().equal(queryKey.get(getAttributeName()).value()); | |
exp2 = getDescriptor().getObjectBuilder().getPrimaryKeyExpression().rebuildOn(queryKey); | |
setSelectionCriteria(exp1.and(exp2)); | |
} | |
/** | |
* INTERNAL: | |
*/ | |
@Override | |
public boolean isNestedTableMapping() { | |
return true; | |
} | |
/** | |
* INTERNAL: | |
* Post Initialize the mapping. | |
*/ | |
@Override | |
public void postInitialize(AbstractSession session) throws DescriptorException { | |
initializeSelectionCriteria(session); | |
} | |
/** | |
* INTERNAL: | |
* Delete privately owned parts | |
*/ | |
@Override | |
public void preDelete(DeleteObjectQuery query) throws DatabaseException, OptimisticLockException { | |
if (!shouldObjectModifyCascadeToParts(query)) { | |
return; | |
} | |
Object objects = getRealCollectionAttributeValueFromObject(query.getObject(), query.getSession()); | |
ContainerPolicy containerPolicy = getContainerPolicy(); | |
Object objectsIterator = containerPolicy.iteratorFor(objects); | |
// delete parts one by one | |
while (containerPolicy.hasNext(objectsIterator)) { | |
DeleteObjectQuery deleteQuery = new DeleteObjectQuery(); | |
deleteQuery.setIsExecutionClone(true); | |
deleteQuery.setObject(containerPolicy.next(objectsIterator, query.getSession())); | |
deleteQuery.setCascadePolicy(query.getCascadePolicy()); | |
query.getSession().executeQuery(deleteQuery); | |
} | |
if (!query.getSession().isUnitOfWork()) { | |
// This deletes any objects on the database, as the collection in memory may has been changed. | |
// This is not required for unit of work, as the update would have already deleted these objects, | |
// and the backup copy will include the same objects causing double deletes. | |
verifyDeleteForUpdate(query); | |
} | |
} | |
/** | |
* INTERNAL: | |
* Insert privately owned parts | |
*/ | |
@Override | |
public void preInsert(WriteObjectQuery query) throws DatabaseException, OptimisticLockException { | |
if (!shouldObjectModifyCascadeToParts(query)) { | |
return; | |
} | |
Object objects = getRealCollectionAttributeValueFromObject(query.getObject(), query.getSession()); | |
// insert each object one by one | |
ContainerPolicy cp = getContainerPolicy(); | |
for (Object iter = cp.iteratorFor(objects); cp.hasNext(iter);) { | |
Object object = cp.next(iter, query.getSession()); | |
if (isPrivateOwned()) { | |
InsertObjectQuery insertQuery = new InsertObjectQuery(); | |
insertQuery.setIsExecutionClone(true); | |
insertQuery.setObject(object); | |
insertQuery.setCascadePolicy(query.getCascadePolicy()); | |
query.getSession().executeQuery(insertQuery); | |
} else { | |
// Will happen in unit of work or cascaded query. | |
// This is done only for persistence by reachablility and it not require if the targets are in the queue anyway | |
// Avoid cycles by checking commit manager, this is allowed because there is no dependency. | |
if (!query.getSession().getCommitManager().isCommitInPreModify(object)) { | |
ObjectChangeSet changeSet = null; | |
UnitOfWorkChangeSet uowChangeSet = null; | |
if (query.getSession().isUnitOfWork() && (((UnitOfWorkImpl)query.getSession()).getUnitOfWorkChangeSet() != null)) { | |
uowChangeSet = (UnitOfWorkChangeSet)((UnitOfWorkImpl)query.getSession()).getUnitOfWorkChangeSet(); | |
changeSet = (ObjectChangeSet)uowChangeSet.getObjectChangeSetForClone(object); | |
} | |
WriteObjectQuery writeQuery = new WriteObjectQuery(); | |
writeQuery.setIsExecutionClone(true); | |
writeQuery.setObject(object); | |
writeQuery.setObjectChangeSet(changeSet); | |
writeQuery.setCascadePolicy(query.getCascadePolicy()); | |
query.getSession().executeQuery(writeQuery); | |
} | |
} | |
} | |
} | |
/** | |
* INTERNAL: | |
* Update the privately owned parts | |
*/ | |
@Override | |
public void preUpdate(WriteObjectQuery query) throws DatabaseException, OptimisticLockException { | |
if (!shouldObjectModifyCascadeToParts(query)) { | |
return; | |
} | |
// If objects are not instantiated that means they are not changed. | |
if (!isAttributeValueInstantiatedOrChanged(query.getObject())) { | |
return; | |
} | |
if (query.getObjectChangeSet() != null) { | |
// UnitOfWork | |
writeChanges(query.getObjectChangeSet(), query); | |
} else { | |
// OLD COMMIT | |
compareObjectsAndWrite(query); | |
} | |
} | |
/** | |
* Set the field in the mapping. | |
*/ | |
protected void setField(DatabaseField theField) { | |
field = theField; | |
} | |
/** | |
* PUBLIC: | |
* Set the field name in the mapping. | |
*/ | |
public void setFieldName(String FieldName) { | |
setField(new ObjectRelationalDatabaseField(FieldName)); | |
} | |
/** | |
* PUBLIC: | |
* Set the name of the structure. | |
* This is the name of the user defined nested table data type as defined on the database. | |
*/ | |
public void setStructureName(String structureName) { | |
this.structureName = structureName; | |
} | |
/** | |
* INTERNAL: | |
* Verifying deletes make sure that all the records privately owned by this mapping are | |
* actually removed. If such records are found then those are all read and removed one | |
* by one taking their privately owned parts into account. | |
*/ | |
protected void verifyDeleteForUpdate(DeleteObjectQuery query) throws DatabaseException, OptimisticLockException { | |
Object objects = readPrivateOwnedForObject(query); | |
// Delete all objects one by one. | |
ContainerPolicy cp = getContainerPolicy(); | |
for (Object iter = cp.iteratorFor(objects); cp.hasNext(iter);) { | |
query.getSession().deleteObject(cp.next(iter, query.getSession())); | |
} | |
} | |
/** | |
* INTERNAL: | |
* Get a value from the object and set that in the respective field of the row. | |
*/ | |
@Override | |
public void writeFromObjectIntoRow(Object object, AbstractRecord record, AbstractSession session, WriteType writeType) { | |
if (isReadOnly()) { | |
return; | |
} | |
Object values = getRealCollectionAttributeValueFromObject(object, session); | |
ContainerPolicy cp = getContainerPolicy(); | |
Object[] fields = new Object[cp.sizeFor(values)]; | |
Object valuesIterator = cp.iteratorFor(values); | |
for (int index = 0; index < cp.sizeFor(values); index++) { | |
Object value = cp.next(valuesIterator, session); | |
fields[index] = ((ObjectRelationalDataTypeDescriptor)getReferenceDescriptor()).getRef(value, session); | |
} | |
java.sql.Array array; | |
try { | |
((DatabaseAccessor)session.getAccessor()).incrementCallCount(session); | |
java.sql.Connection connection = ((DatabaseAccessor)session.getAccessor()).getConnection(); | |
array = session.getPlatform().createArray(getStructureName(), fields, session,connection); | |
} catch (java.sql.SQLException exception) { | |
throw DatabaseException.sqlException(exception, session.getAccessor(), session, false); | |
} finally { | |
((DatabaseAccessor)session.getAccessor()).decrementCallCount(); | |
} | |
record.put(getField(), array); | |
} | |
/** | |
* INTERNAL: | |
* Get a value from the object and set that in the respective field of the row. | |
*/ | |
@Override | |
public void writeFromObjectIntoRowWithChangeRecord(ChangeRecord changeRecord, AbstractRecord record, AbstractSession session, WriteType writeType) { | |
if (isReadOnly()) { | |
return; | |
} | |
Object object = ((ObjectChangeSet)changeRecord.getOwner()).getUnitOfWorkClone(); | |
Object values = getRealAttributeValueFromObject(object, session); | |
ContainerPolicy containterPolicy = getContainerPolicy(); | |
if (values == null) { | |
values = containterPolicy.containerInstance(1); | |
} | |
Object[] fields = new Object[containterPolicy.sizeFor(values)]; | |
Object valuesIterator = containterPolicy.iteratorFor(values); | |
for (int index = 0; index < containterPolicy.sizeFor(values); index++) { | |
Object value = containterPolicy.next(valuesIterator, session); | |
fields[index] = ((ObjectRelationalDataTypeDescriptor)getReferenceDescriptor()).getRef(value, session); | |
} | |
java.sql.Array array; | |
try { | |
((DatabaseAccessor)session.getAccessor()).incrementCallCount(session); | |
java.sql.Connection connection = ((DatabaseAccessor)session.getAccessor()).getConnection(); | |
array = session.getPlatform().createArray(getStructureName(), fields, session, connection); | |
} catch (java.sql.SQLException exception) { | |
throw DatabaseException.sqlException(exception, session.getAccessor(), session, false); | |
} finally { | |
((DatabaseAccessor)session.getAccessor()).decrementCallCount(); | |
} | |
record.put(getField(), array); | |
} | |
/** | |
* INTERNAL: | |
* This row is built for shallow insert which happens in case of bidirectional inserts. | |
* The foreign keys must be set to null to avoid constraints. | |
*/ | |
@Override | |
public void writeFromObjectIntoRowForShallowInsert(Object object, AbstractRecord record, AbstractSession session) { | |
if (isReadOnly()) { | |
return; | |
} | |
if (getField().isNullable()) { | |
record.put(getField(), null); | |
} else { | |
writeFromObjectIntoRow(object, record, session, WriteType.INSERT); | |
} | |
} | |
/** | |
* INTERNAL: | |
* This row is built for update after shallow insert which happens in case of bidirectional inserts. | |
* It contains the foreign keys with non null values that were set to null for shallow insert. | |
*/ | |
@Override | |
public void writeFromObjectIntoRowForUpdateAfterShallowInsert(Object object, AbstractRecord record, AbstractSession session, DatabaseTable table) { | |
if (!getField().getTable().equals(table) || !getField().isNullable()) { | |
return; | |
} | |
writeFromObjectIntoRow(object, record, session, WriteType.UPDATE); | |
} | |
/** | |
* INTERNAL: | |
* This row is built for shallow insert which happens in case of bidirectional inserts. | |
* The foreign keys must be set to null to avoid constraints. | |
*/ | |
@Override | |
public void writeFromObjectIntoRowForShallowInsertWithChangeRecord(ChangeRecord changeRecord, AbstractRecord record, AbstractSession session) { | |
if (isReadOnly()) { | |
return; | |
} | |
record.put(getField(), null); | |
} | |
/** | |
* INTERNAL: | |
* Write the entire structure into the row as a special type that prints as the constructor. | |
* If any part of the structure has changed the whole thing is written. | |
*/ | |
@Override | |
public void writeFromObjectIntoRowForUpdate(WriteObjectQuery writeQuery, AbstractRecord record) throws DescriptorException { | |
if (!isAttributeValueInstantiatedOrChanged(writeQuery.getObject())) { | |
return; | |
} | |
if (writeQuery.getSession().isUnitOfWork()) { | |
if (compareObjects(writeQuery.getObject(), writeQuery.getBackupClone(), writeQuery.getSession())) { | |
return;// Nothing has changed, no work required | |
} | |
} | |
writeFromObjectIntoRow(writeQuery.getObject(), record, writeQuery.getSession(), WriteType.UPDATE); | |
} | |
/** | |
* INTERNAL: | |
* Write fields needed for insert into the template for with null values. | |
*/ | |
@Override | |
public void writeInsertFieldsIntoRow(AbstractRecord record, AbstractSession session) { | |
if (isReadOnly()) { | |
return; | |
} | |
record.put(getField(), null); | |
} | |
} |