blob: 48a9b73d3652a11886d4f74eb4bfb4da2c0d654a [file] [log] [blame]
/*
* Copyright (c) 1998, 2021 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.eis.mappings;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.eclipse.persistence.annotations.CacheKeyType;
import org.eclipse.persistence.exceptions.DatabaseException;
import org.eclipse.persistence.exceptions.DescriptorException;
import org.eclipse.persistence.expressions.Expression;
import org.eclipse.persistence.expressions.ExpressionBuilder;
import org.eclipse.persistence.indirection.ValueHolder;
import org.eclipse.persistence.internal.helper.DatabaseField;
import org.eclipse.persistence.internal.identitymaps.CacheId;
import org.eclipse.persistence.internal.identitymaps.CacheKey;
import org.eclipse.persistence.internal.queries.JoinedAttributeManager;
import org.eclipse.persistence.internal.sessions.AbstractRecord;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.mappings.DatabaseMapping;
import org.eclipse.persistence.mappings.ObjectReferenceMapping;
import org.eclipse.persistence.queries.ObjectBuildingQuery;
import org.eclipse.persistence.queries.ObjectLevelModifyQuery;
import org.eclipse.persistence.queries.ObjectLevelReadQuery;
import org.eclipse.persistence.queries.ReadObjectQuery;
import org.eclipse.persistence.queries.ReadQuery;
/**
* <p>An EIS one-to-one mapping is a reference mapping that represents the relationship between
* a single source object and a single mapped persistent Java object. The source object usually
* contains a foreign key (pointer) to the target object (key on source); alternatively, the target
* object may contain a foreign key to the source object (key on target). Because both the source
* and target objects use interactions, they must both be configured as root object types.
*
* <table border="1">
* <caption>Record formats</caption>
* <tr>
* <th id="c1">Record Type</th>
* <th id="c2">Description</th>
* </tr>
* <tr>
* <td headers="c1">Indexed</td>
* <td headers="c2">Ordered collection of record elements. The indexed record EIS format
* enables Java class attribute values to be retrieved by position or index.</td>
* </tr>
* <tr>
* <td headers="c1">Mapped</td>
* <td headers="c2">Key-value map based representation of record elements. The mapped record
* EIS format enables Java class attribute values to be retrieved by an object key.</td>
* </tr>
* <tr>
* <td headers="c1">XML</td>
* <td headers="c2">Record/Map representation of an XML DOM element.</td>
* </tr>
* </table>
*
* @see org.eclipse.persistence.eis.EISDescriptor#useIndexedRecordFormat
* @see org.eclipse.persistence.eis.EISDescriptor#useMappedRecordFormat
* @see org.eclipse.persistence.eis.EISDescriptor#useXMLRecordFormat
*
* @since Oracle TopLink 10<i>g</i> Release 2 (10.1.3)
*/
public class EISOneToOneMapping extends ObjectReferenceMapping implements EISMapping {
/** Maps the source foreign/primary key fields to the target primary/foreign key fields. */
protected Map sourceToTargetKeyFields;
/** Maps the target primary/foreign key fields to the source foreign/primary key fields. */
protected Map<DatabaseField, DatabaseField> targetToSourceKeyFields;
/** These are used for non-unit of work modification to check if the value of the 1-1 was changed and a deletion is required. */
protected boolean shouldVerifyDelete;
protected transient Expression privateOwnedCriteria;
public EISOneToOneMapping() {
this.selectionQuery = new ReadObjectQuery();
this.foreignKeyFields = org.eclipse.persistence.internal.helper.NonSynchronizedVector.newInstance(1);
this.sourceToTargetKeyFields = new HashMap(2);
this.targetToSourceKeyFields = new HashMap(2);
}
/**
* INTERNAL:
*/
@Override
public boolean isEISMapping() {
return true;
}
/**
* INTERNAL:
*/
@Override
public boolean isOneToOneMapping() {
return true;
}
/**
* PUBLIC:
* Define the source foreign key relationship in the one-to-one mapping.
* This method is used to add foreign key relationships to the mapping.
* Both the source foreign key field name and the corresponding
* target primary key field name must be specified.
*/
@Override
public void addForeignKeyField(DatabaseField sourceForeignKeyField, DatabaseField targetKeyField) {
getSourceToTargetKeyFields().put(sourceForeignKeyField, targetKeyField);
getTargetToSourceKeyFields().put(targetKeyField, sourceForeignKeyField);
getForeignKeyFields().add(sourceForeignKeyField);
setIsForeignKeyRelationship(true);
}
/**
* PUBLIC:
* Define the source foreign key relationship in the one-to-one mapping.
* This method is used to add foreign key relationships to the mapping.
* Both the source foreign key field name and the corresponding
* target primary key field name must be specified.
*/
public void addForeignKeyFieldName(String sourceForeignKeyFieldName, String targetKeyFieldName) {
this.addForeignKeyField(new DatabaseField(sourceForeignKeyFieldName), new DatabaseField(targetKeyFieldName));
}
/**
* INTERNAL:
* This methods clones all the fields and ensures that each collection refers to
* the same clones.
*/
@Override
public Object clone() {
EISOneToOneMapping clone = (EISOneToOneMapping)super.clone();
clone.setForeignKeyFields(org.eclipse.persistence.internal.helper.NonSynchronizedVector.newInstance(getForeignKeyFields().size()));
clone.setSourceToTargetKeyFields(new HashMap(getSourceToTargetKeyFields().size()));
clone.setTargetToSourceKeyFields(new HashMap(getTargetToSourceKeyFields().size()));
Map setOfFields = new HashMap(getTargetToSourceKeyFields().size());
for (Enumeration<DatabaseField> enumtr = getForeignKeyFields().elements(); enumtr.hasMoreElements();) {
DatabaseField field = enumtr.nextElement();
DatabaseField fieldClone = field.clone();
setOfFields.put(field, fieldClone);
clone.getForeignKeyFields().addElement(fieldClone);
}
//get clones from set for source hashtable. If they do not exist, create a new one.
Iterator<DatabaseField> sourceKeyIterator = getSourceToTargetKeyFields().keySet().iterator();
while (sourceKeyIterator.hasNext()) {
DatabaseField sourceField = sourceKeyIterator.next();
DatabaseField targetField = getSourceToTargetKeyFields().get(sourceField);
DatabaseField targetClone = (DatabaseField)setOfFields.get(targetField);
if (targetClone == null) {
targetClone = targetField.clone();
setOfFields.put(targetField, targetClone);
}
DatabaseField sourceClone = sourceField.clone();
if (sourceClone == null) {
sourceClone = sourceField.clone();
setOfFields.put(sourceField, sourceClone);
}
clone.getSourceToTargetKeyFields().put(sourceClone, targetClone);
}
//get clones from set for target hashtable. If they do not exist, create a new one.
Iterator<DatabaseField> targetKeyIterator = getTargetToSourceKeyFields().keySet().iterator();
while (targetKeyIterator.hasNext()) {
DatabaseField targetField = targetKeyIterator.next();
DatabaseField sourceField = getTargetToSourceKeyFields().get(targetField);
DatabaseField targetClone = (DatabaseField)setOfFields.get(targetField);
if (targetClone == null) {
targetClone = targetField.clone();
setOfFields.put(targetField, targetClone);
}
DatabaseField sourceClone = (DatabaseField)setOfFields.get(sourceField);
if (sourceClone == null) {
sourceClone = sourceField.clone();
setOfFields.put(sourceField, sourceClone);
}
clone.getTargetToSourceKeyFields().put(targetClone, sourceClone);
}
return clone;
}
/**
* INTERNAL:
* Return the primary key for the reference object (i.e. the object
* object referenced by domainObject and specified by mapping).
* This key will be used by a RemoteValueHolder.
*/
@Override
public Object extractPrimaryKeysForReferenceObjectFromRow(AbstractRecord row) {
List<DatabaseField> primaryKeyFields = getReferenceDescriptor().getPrimaryKeyFields();
Object[] result = new Object[primaryKeyFields.size()];
for (int index = 0; index < primaryKeyFields.size(); index++) {
DatabaseField targetKeyField = primaryKeyFields.get(index);
DatabaseField sourceKeyField = getTargetToSourceKeyFields().get(targetKeyField);
if (sourceKeyField == null) {
return null;
}
result[index] = row.get(sourceKeyField);
if (getReferenceDescriptor().getCachePolicy().getCacheKeyType() == CacheKeyType.ID_VALUE) {
return result[index];
}
}
return new CacheId(result);
}
/**
* INTERNAL:
* Initialize the mapping.
*/
@Override
public void initialize(AbstractSession session) throws DescriptorException {
super.initialize(session);
// Must build foreign keys fields.
List<DatabaseField> foreignKeyFields = getForeignKeyFields();
int size = foreignKeyFields.size();
for (int index = 0; index < size; index++) {
DatabaseField foreignKeyField = foreignKeyFields.get(index);
foreignKeyField = getDescriptor().buildField(foreignKeyField);
foreignKeyFields.set(index, foreignKeyField);
}
initializeForeignKeys(session);
if (shouldInitializeSelectionCriteria()) {
initializeSelectionCriteria(session);
} else {
setShouldVerifyDelete(false);
}
setFields(collectFields());
}
/**
* INTERNAL:
* The foreign keys primary keys are stored as database fields in the hashtable.
*/
protected void initializeForeignKeys(AbstractSession session) {
HashMap newSourceToTargetKeyFields = new HashMap(getSourceToTargetKeyFields().size());
HashMap newTargetToSourceKeyFields = new HashMap(getTargetToSourceKeyFields().size());
Iterator<Map.Entry<DatabaseField, DatabaseField>> iterator = getSourceToTargetKeyFields().entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<DatabaseField, DatabaseField> entry = iterator.next();
DatabaseField sourceField = entry.getKey();
DatabaseField targetField = entry.getValue();
sourceField = getDescriptor().buildField(sourceField);
targetField = getReferenceDescriptor().buildField(targetField);
newSourceToTargetKeyFields.put(sourceField, targetField);
newTargetToSourceKeyFields.put(targetField, sourceField);
}
setSourceToTargetKeyFields(newSourceToTargetKeyFields);
setTargetToSourceKeyFields(newTargetToSourceKeyFields);
}
/**
* INTERNAL:
* Selection criteria is created with source foreign keys and target keys.
* This criteria is then used to read target records from the table.
*
* CR#3922 - This method is almost the same as buildSelectionCriteria() the difference
* is that getSelectionCriteria() is called
*/
protected void initializeSelectionCriteria(AbstractSession session) {
if (this.getSourceToTargetKeyFields().isEmpty()) {
throw DescriptorException.noForeignKeysAreSpecified(this);
}
Expression criteria;
Expression builder = new ExpressionBuilder();
Iterator<DatabaseField> keyIterator = getSourceToTargetKeyFields().keySet().iterator();
while (keyIterator.hasNext()) {
DatabaseField foreignKey = keyIterator.next();
DatabaseField targetKey = getSourceToTargetKeyFields().get(foreignKey);
Expression expression = builder.getField(targetKey).equal(builder.getParameter(foreignKey));
criteria = expression.and(getSelectionCriteria());
setSelectionCriteria(criteria);
}
}
/**
* INTERNAL:
* Reads the private owned object.
*/
@Override
protected Object readPrivateOwnedForObject(ObjectLevelModifyQuery modifyQuery) throws DatabaseException {
if (modifyQuery.getSession().isUnitOfWork()) {
return getRealAttributeValueFromObject(modifyQuery.getBackupClone(), modifyQuery.getSession());
} else {
if (!shouldVerifyDelete()) {
return null;
}
ReadObjectQuery readQuery = (ReadObjectQuery)getSelectionQuery().clone();
readQuery.setSelectionCriteria(getPrivateOwnedCriteria());
return modifyQuery.getSession().executeQuery(readQuery, modifyQuery.getTranslationRow());
}
}
/**
* INTERNAL:
* Selection criteria is created with source foreign keys and target keys.
*/
protected void initializePrivateOwnedCriteria() {
if (!isForeignKeyRelationship()) {
setPrivateOwnedCriteria(getSelectionCriteria());
} else {
Expression pkCriteria = getDescriptor().getObjectBuilder().getPrimaryKeyExpression();
ExpressionBuilder builder = new ExpressionBuilder();
Expression backRef = builder.getManualQueryKey(getAttributeName() + "-back-ref", getDescriptor());
Expression newPKCriteria = pkCriteria.rebuildOn(backRef);
Expression twistedSelection = backRef.twist(getSelectionCriteria(), builder);
if (getDescriptor().getQueryManager().getAdditionalJoinExpression() != null) {
// We don't have to twist the additional join because it's all against the same node, which is our base
// but we do have to rebuild it onto the manual query key
Expression rebuiltAdditional = getDescriptor().getQueryManager().getAdditionalJoinExpression().rebuildOn(backRef);
if (twistedSelection == null) {
twistedSelection = rebuiltAdditional;
} else {
twistedSelection = twistedSelection.and(rebuiltAdditional);
}
}
setPrivateOwnedCriteria(newPKCriteria.and(twistedSelection));
}
}
/**
* INTERNAL:
* Return the value of the field from the row or a value holder on the query to obtain the object.
* Check for batch + aggregation reading.
*/
@Override
public Object valueFromRow(AbstractRecord row, JoinedAttributeManager joinManager, ObjectBuildingQuery query, CacheKey cacheKey, AbstractSession session, boolean isTargetProtected, Boolean[] wasCacheUsed) throws DatabaseException {
if (this.descriptor.getCachePolicy().isProtectedIsolation()) {
if (this.isCacheable && isTargetProtected && cacheKey != null) {
//cachekey will be null when isolating to uow
//used cached collection
Object result = null;
Object cached = cacheKey.getObject();
if (cached != null) {
if (wasCacheUsed != null){
wasCacheUsed[0] = Boolean.TRUE;
}
return this.getAttributeValueFromObject(cached);
}
return result;
} else if (!this.isCacheable && !isTargetProtected && cacheKey != null) {
return this.indirectionPolicy.buildIndirectObject(new ValueHolder(null));
}
}
// If any field in the foreign key is null then it means there are no referenced objects
// Skip for partial objects as fk may not be present.
if (!query.hasPartialAttributeExpressions()) {
for (Enumeration<DatabaseField> enumeration = getFields().elements(); enumeration.hasMoreElements();) {
DatabaseField field = enumeration.nextElement();
if (row.get(field) == null) {
return getIndirectionPolicy().nullValueFromRow();
}
}
}
// Call the default which executes the selection query,
// or wraps the query with a value holder.
//return super.valueFromRow(row, query);
ReadQuery targetQuery = getSelectionQuery();
// if the source query is cascading then the target query must use the same settings
if (targetQuery.isObjectLevelReadQuery() && (query.shouldCascadeAllParts() || (query.shouldCascadePrivateParts() && isPrivateOwned()) || (query.shouldCascadeByMapping() && this.cascadeRefresh))) {
targetQuery = (ObjectLevelReadQuery)targetQuery.clone();
((ObjectLevelReadQuery)targetQuery).setShouldRefreshIdentityMapResult(query.shouldRefreshIdentityMapResult());
targetQuery.setCascadePolicy(query.getCascadePolicy());
//CR #4365
targetQuery.setQueryId(query.getQueryId());
// For queries that have turned caching off, such as aggregate collection, leave it off.
if (targetQuery.shouldMaintainCache()) {
targetQuery.setShouldMaintainCache(query.shouldMaintainCache());
}
}
return getIndirectionPolicy().valueFromQuery(targetQuery, row, 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() || (!isForeignKeyRelationship())) {
return;
}
AbstractRecord referenceRow = getIndirectionPolicy().extractReferenceRow(getAttributeValueFromObject(object));
if (referenceRow == null) {
// Extract from object.
Object referenceObject = getRealAttributeValueFromObject(object, session);
for (int i = 0; i < getForeignKeyFields().size(); i++) {
DatabaseField sourceKey = getForeignKeyFields().get(i);
DatabaseField targetKey = getSourceToTargetKeyFields().get(sourceKey);
Object referenceValue = null;
// If privately owned part is null then method cannot be invoked.
if (referenceObject != null) {
referenceValue = getReferenceDescriptor().getObjectBuilder().extractValueFromObjectForField(referenceObject, targetKey, session);
}
Record.add(sourceKey, referenceValue);
}
} else {
for (int i = 0; i < getForeignKeyFields().size(); i++) {
DatabaseField sourceKey = getForeignKeyFields().get(i);
Record.add(sourceKey, referenceRow.get(sourceKey));
}
}
}
/**
* INTERNAL:
* Return the classifiction for the field contained in the mapping.
* This is used to convert the row value to a consistent java value.
*/
@Override
public Class getFieldClassification(DatabaseField fieldToClassify) throws DescriptorException {
DatabaseField fieldInTarget = getSourceToTargetKeyFields().get(fieldToClassify);
if (fieldInTarget == null) {
return null;// Can be registered as multiple table secondary field mapping
}
DatabaseMapping mapping = getReferenceDescriptor().getObjectBuilder().getMappingForField(fieldInTarget);
if (mapping == null) {
return null;// Means that the mapping is read-only
}
return mapping.getFieldClassification(fieldInTarget);
}
/**
* INTERNAL:
* The private owned criteria is only used outside of the unit of work to compare the previous value of the reference.
*/
public Expression getPrivateOwnedCriteria() {
if (privateOwnedCriteria == null) {
initializePrivateOwnedCriteria();
}
return privateOwnedCriteria;
}
/**
* INTERNAL:
* Private owned criteria is used to verify the deletion of the target.
* It joins from the source table on the foreign key to the target table,
* with a parameterization of the primary key of the source object.
*/
protected void setPrivateOwnedCriteria(Expression expression) {
privateOwnedCriteria = expression;
}
/**
* PUBLIC: Verify delete is used during delete and update on private 1:1's
* outside of a unit of work only. It checks for the previous value of the
* target object through joining the source and target tables. By default it
* is always done, but may be disabled for performance on distributed
* database reasons. In the unit of work the previous value is obtained from
* the backup-clone so it is never used.
*
* @param shouldVerifyDelete
* Sets whether delete verification should be performed
*/
public void setShouldVerifyDelete(boolean shouldVerifyDelete) {
this.shouldVerifyDelete = shouldVerifyDelete;
}
/**
* PUBLIC: Verify delete is used during delete and update outside of a unit
* of work only. It checks for the previous value of the target object
* through joining the source and target tables.
*
* @return TRUE if verify delete has been enabled
*/
public boolean shouldVerifyDelete() {
return shouldVerifyDelete;
}
/**
* INTERNAL: Gets the foreign key fields.
*
* @return The mapping from source to target key fields
*/
public Map<DatabaseField, DatabaseField> getSourceToTargetKeyFields() {
return sourceToTargetKeyFields;
}
/**
* INTERNAL: Gets the target foreign key fields.
*
* @return The mapping from target to source key fields
*/
public Map<DatabaseField, DatabaseField> getTargetToSourceKeyFields() {
return targetToSourceKeyFields;
}
/**
* INTERNAL: Set the source keys to target keys fields association.
*
* @param sourceToTargetKeyFields
* The mapping from source keys to target keys
*/
public void setSourceToTargetKeyFields(Map sourceToTargetKeyFields) {
this.sourceToTargetKeyFields = sourceToTargetKeyFields;
}
/**
* INTERNAL: Set the target keys to source keys fields association.
*
* @param targetToSourceKeyFields
* The mapping from target keys to source keys
*/
public void setTargetToSourceKeyFields(Map targetToSourceKeyFields) {
this.targetToSourceKeyFields = targetToSourceKeyFields;
}
/**
* INTERNAL:
* This method is not supported in an EIS environment.
*/
@Override
public void setSelectionSQLString(String sqlString) {
throw DescriptorException.invalidMappingOperation(this, "setSelectionSQLString");
}
}