blob: ee8c75f9542c0bbd162b8bb596849ead37a01649 [file] [log] [blame]
/*
* Copyright (c) 1998, 2019 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 v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
*/
// Contributors:
// Oracle - initial API and implementation from Oracle TopLink
package org.eclipse.persistence.mappings.structures;
import java.util.Map;
import java.util.Vector;
import org.eclipse.persistence.exceptions.DatabaseException;
import org.eclipse.persistence.exceptions.DescriptorException;
import org.eclipse.persistence.exceptions.OptimisticLockException;
import org.eclipse.persistence.exceptions.QueryException;
import org.eclipse.persistence.expressions.Expression;
import org.eclipse.persistence.expressions.ExpressionBuilder;
import org.eclipse.persistence.internal.expressions.ObjectExpression;
import org.eclipse.persistence.internal.helper.DatabaseField;
import org.eclipse.persistence.internal.helper.DatabaseTable;
import org.eclipse.persistence.internal.queries.ContainerPolicy;
import org.eclipse.persistence.internal.sessions.AbstractRecord;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.internal.sessions.ChangeRecord;
import org.eclipse.persistence.internal.sessions.ObjectChangeSet;
import org.eclipse.persistence.internal.sessions.UnitOfWorkChangeSet;
import org.eclipse.persistence.internal.sessions.UnitOfWorkImpl;
import org.eclipse.persistence.mappings.CollectionMapping;
import org.eclipse.persistence.mappings.DatabaseMapping;
import org.eclipse.persistence.queries.DeleteObjectQuery;
import org.eclipse.persistence.queries.InsertObjectQuery;
import org.eclipse.persistence.queries.QueryByExamplePolicy;
import org.eclipse.persistence.queries.WriteObjectQuery;
/**
* <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.
*/
@Override
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
*/
@Override
public Object clone() {
NestedTableMapping clone = (NestedTableMapping)super.clone();
return clone;
}
/**
* Returns all the aggregate fields.
*/
@Override
protected Vector collectFields() {
Vector fields = new Vector(1);
fields.addElement(getField());
return fields;
}
/**
* INTERNAL:
* Returns the field which this mapping represents.
*/
@Override
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 {
session.getAccessor().incrementCallCount(session);
java.sql.Connection connection = 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 {
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 {
session.getAccessor().incrementCallCount(session);
java.sql.Connection connection = 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 {
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);
}
}