| /* |
| * 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); |
| } |
| } |