All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.persistence.internal.expressions.QueryKeyExpression Maven / Gradle / Ivy

There is a newer version: 5.0.0-B03
Show newest version
/*
 * Copyright (c) 1998, 2024 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
//     11/10/2011-2.4 Guy Pelletier
//       - 357474: Address primaryKey option from tenant discriminator column
package org.eclipse.persistence.internal.expressions;

import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.descriptors.FetchGroupManager;
import org.eclipse.persistence.exceptions.QueryException;
import org.eclipse.persistence.expressions.Expression;
import org.eclipse.persistence.internal.helper.DatabaseField;
import org.eclipse.persistence.internal.helper.DatabaseTable;
import org.eclipse.persistence.internal.identitymaps.CacheId;
import org.eclipse.persistence.internal.queries.MappedKeyMapContainerPolicy;
import org.eclipse.persistence.internal.sessions.AbstractRecord;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.internal.sessions.UnitOfWorkImpl;
import org.eclipse.persistence.mappings.AggregateObjectMapping;
import org.eclipse.persistence.mappings.CollectionMapping;
import org.eclipse.persistence.mappings.DatabaseMapping;
import org.eclipse.persistence.mappings.DirectCollectionMapping;
import org.eclipse.persistence.mappings.ForeignReferenceMapping;
import org.eclipse.persistence.mappings.ManyToManyMapping;
import org.eclipse.persistence.mappings.ObjectReferenceMapping;
import org.eclipse.persistence.mappings.OneToOneMapping;
import org.eclipse.persistence.mappings.foundation.AbstractColumnMapping;
import org.eclipse.persistence.mappings.querykeys.DirectQueryKey;
import org.eclipse.persistence.mappings.querykeys.ForeignReferenceQueryKey;
import org.eclipse.persistence.mappings.querykeys.QueryKey;
import org.eclipse.persistence.queries.DatabaseQuery;
import org.eclipse.persistence.queries.InMemoryQueryIndirectionPolicy;
import org.eclipse.persistence.queries.ReadQuery;

import java.io.BufferedWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * Represents expression on query keys or mappings.
 * This includes direct, relationships query keys and mappings.
 */
public class QueryKeyExpression extends ObjectExpression {

    /** The name of the query key. */
    protected String name;

    /** Cache the aliased field. Only applies to attributes. */
    protected DatabaseField aliasedField;

    /** Is this a query across a 1:many or many:many relationship. Does not apply to attributes. */
    protected boolean shouldQueryToManyRelationship;

    /** Cache the query key for performance. Store a boolean so we don't repeat the search if there isn't one. */
    transient protected QueryKey queryKey;
    protected boolean hasQueryKey;

    /** Cache the mapping for performance. Store a boolean so we don't repeat the search if there isn't one. */
    transient protected DatabaseMapping mapping;
    protected boolean hasMapping;

    /** PERF: Cache if the expression is an attribute expression. */
    protected Boolean isAttributeExpression;

    protected IndexExpression index;

    protected boolean isClonedForSubQuery = false;

    public QueryKeyExpression() {
        this.shouldQueryToManyRelationship = false;
        this.hasQueryKey = true;
        this.hasMapping = true;
    }

    public QueryKeyExpression(String aName, Expression base) {
        super();
        name = aName;
        baseExpression = base;
        shouldUseOuterJoin = false;
        shouldQueryToManyRelationship = false;
        hasQueryKey = true;
        hasMapping = true;
    }

    /**
     * INTERNAL:
     * Return if the expression is equal to the other.
     * This is used to allow dynamic expression's SQL to be cached.
     */
    @Override
    public boolean equals(Object object) {
        if (this == object) {
            return true;
        }
        if (!super.equals(object)) {
            return false;
        }
        QueryKeyExpression expression = (QueryKeyExpression) object;
        // Return false for anyOf expressions, as equality is unknown.
        if (shouldQueryToManyRelationship() || expression.shouldQueryToManyRelationship()) {
            return false;
        }
        return ((getName() == expression.getName()) || ((getName() != null) && getName().equals(expression.getName())));
    }

    /**
     * INTERNAL:
     * Compute a consistent hash-code for the expression.
     * This is used to allow dynamic expression's SQL to be cached.
     */
    @Override
    public int computeHashCode() {
        int hashCode = super.computeHashCode();
        if (getName() != null) {
            hashCode = hashCode + getName().hashCode();
        }
        return hashCode;
    }

    /**
     * INTERNAL:
     * Return the expression to join the main table of this node to any auxiliary tables.
     */
    @Override
    public Expression additionalExpressionCriteria() {
        if (getDescriptor() == null) {
            return null;
        }

        Expression criteria = getDescriptor().getQueryManager().getAdditionalJoinExpression();
        if (criteria != null) {
            criteria = this.baseExpression.twist(criteria, this);
            if (shouldUseOuterJoin() && getSession().getPlatform().shouldPrintOuterJoinInWhereClause()) {
                criteria.convertToUseOuterJoin();
            }
        }
        if(getSession().getPlatform().shouldPrintOuterJoinInWhereClause()) {
            if(isUsingOuterJoinForMultitableInheritance()) {
                Expression childrenCriteria = getDescriptor().getInheritancePolicy().getChildrenJoinExpression();
                childrenCriteria = this.baseExpression.twist(childrenCriteria, this);
                childrenCriteria.convertToUseOuterJoin();
                if(criteria == null) {
                    criteria = childrenCriteria;
                } else {
                    criteria = criteria.and(childrenCriteria);
                }
            }
        }
        if (getDescriptor().getHistoryPolicy() != null) {
            Expression historyCriteria = getDescriptor().getHistoryPolicy().additionalHistoryExpression(this, this);
            if (criteria != null) {
                criteria = criteria.and(historyCriteria);
            } else {
                criteria = historyCriteria;
            }
        }
        return criteria;
    }

    /**
     * INTERNAL:
     * Used in case outer joins should be printed in FROM clause.
     * Each of the additional tables mapped to expressions that joins it.
     */
    @Override
    public Map additionalExpressionCriteriaMap() {
        if (getDescriptor() == null) {
            return null;
        }

        Map tablesJoinExpressions = new HashMap<>();
        List tables = getDescriptor().getTables();
        // skip the main table - start with i=1
        int tablesSize = tables.size();
        if (shouldUseOuterJoin() || (!getSession().getPlatform().shouldPrintInnerJoinInWhereClause(getDescriptor().getQueryManager().getReadAllQuery()))) {
            for (int i=1; i < tablesSize; i++) {
                DatabaseTable table = tables.get(i);
                Expression joinExpression = getDescriptor().getQueryManager().getTablesJoinExpressions().get(table);
                joinExpression = this.baseExpression.twist(joinExpression, this);
                if (getDescriptor().getHistoryPolicy() != null) {
                    joinExpression = joinExpression.and(getDescriptor().getHistoryPolicy().additionalHistoryExpression(this, this, i));
                }
                tablesJoinExpressions.put(table, joinExpression);
            }
        }
        if (isUsingOuterJoinForMultitableInheritance()) {
            List childrenTables = getDescriptor().getInheritancePolicy().getChildrenTables();
            tablesSize = childrenTables.size();
            for (int i=0; i < tablesSize; i++) {
                DatabaseTable table = childrenTables.get(i);
                Expression joinExpression = getDescriptor().getInheritancePolicy().getChildrenTablesJoinExpressions().get(table);
                joinExpression = this.baseExpression.twist(joinExpression, this);
                tablesJoinExpressions.put(table, joinExpression);
            }
        }

        return tablesJoinExpressions;
    }

    /**
     * INTERNAL:
     * Find the alias for a given table
     */
    @Override
    public DatabaseTable aliasForTable(DatabaseTable table) {
        DatabaseMapping mapping = getMapping();
        if (isAttribute() || ((mapping != null) && (mapping.isAggregateObjectMapping() || mapping.isTransformationMapping()))) {
            return this.baseExpression.aliasForTable(table);
        }

        //"ref" and "structure" mappings, no table printed in the FROM clause, need to get the table alias form the parent table
        if ((mapping != null) && (mapping.isReferenceMapping() || mapping.isStructureMapping())) {
            DatabaseTable alias = this.baseExpression.aliasForTable(mapping.getDescriptor().getTables().get(0));
            alias.setName(alias.getName() + "." + mapping.getField().getName());
            return alias;
        }

        // For direct-collection mappings the alias is store on the table expression.
        if ((mapping != null) && (mapping.isDirectCollectionMapping())) {
            if (tableAliases != null){
                DatabaseTable aliasedTable = tableAliases.keyAtValue(table);
                if (aliasedTable != null){
                    return aliasedTable;
                }
            }
            return getTable(table).aliasForTable(table);
        }

        return super.aliasForTable(table);
    }

    /**
     * ADVANCED:
     * Return an expression that allows you to treat its base as if it were a subclass of the class returned by the base
     * This can only be called on an ExpressionBuilder, the result of expression.get(String), expression.getAllowingNull(String),
     * the result of expression.anyOf("String") or the result of expression.anyOfAllowingNull("String")
     * 

* downcast uses Expression.type() internally to guarantee the results are of the specified class. *

Example: *

     *     EclipseLink: employee.get("project").treat(LargeProject.class).get("budget").equal(1000)
     *     Java: ((LargeProject)employee.getProjects().get(0)).getBudget() == 1000
     *     SQL: LPROJ.PROJ_ID (+)= PROJ.PROJ_ID AND L_PROJ.BUDGET = 1000 AND PROJ.TYPE = "L"
     * 
*/ @Override public Expression treat(Class castClass){ //to be used in 'where treat(t as PerformanceTireInfo).speedRating > 100' QueryKeyExpression clonedExpression = new TreatAsExpression(castClass, this); clonedExpression.shouldQueryToManyRelationship = this.shouldQueryToManyRelationship; //using shouldUseOuterJoin to indicate the join to use between the t and PerformanceTireInfo subclass. clonedExpression.hasQueryKey = this.hasQueryKey; clonedExpression.hasMapping = this.hasMapping; this.addDerivedExpression(clonedExpression); return clonedExpression; } /** * INTERNAL: * Used for cloning. */ @Override protected void postCopyIn(Map alreadyDone) { super.postCopyIn(alreadyDone); if (this.index != null) { this.index = (IndexExpression)this.index.copiedVersionFrom(alreadyDone); } } /** * INTERNAL: * Used for debug printing. */ @Override public String descriptionOfNodeType() { return "Query Key"; } /** * INTERNAL: */ public void doQueryToManyRelationship() { shouldQueryToManyRelationship = true; } /** * INTERNAL: * Return any additional tables that belong to this expression * An example of how this method is used is to return any tables that belong to the map key * when this expression traverses a mapping that uses a Map */ @Override public List getAdditionalTables() { if (mapping != null && mapping.isCollectionMapping()){ return mapping.getContainerPolicy().getAdditionalTablesForJoinQuery(); } return null; } /** * INTERNAL: * Return the field appropriately aliased */ @Override public DatabaseField getAliasedField() { if (aliasedField == null) { initializeAliasedField(); } return aliasedField; } /** * Return the alias for our table */ protected DatabaseTable getAliasedTable() { DataExpression base = (DataExpression)this.baseExpression; DatabaseTable alias = base.aliasForTable(getField().getTable()); if (alias == null) { return getField().getTable(); } else { return alias; } } /** * INTERNAL: */ @Override public DatabaseField getField() { if (!isAttribute()) { return null; } QueryKey key = getQueryKeyOrNull(); if ((key != null) && key.isDirectQueryKey()) { return ((DirectQueryKey)key).getField(); } DatabaseMapping mapping = getMapping(); if ((mapping == null) || mapping.getFields().isEmpty()) { return null; } return mapping.getFields().get(0); } /** * INTERNAL: * Return all the fields */ @Override public List getFields() { if (isAttribute()) { List result = new ArrayList<>(1); DatabaseField field = getField(); if (field != null) { result.add(field); } return result; } else { List result = new ArrayList<>(); result.addAll(super.getFields()); if ((this.mapping != null) && this.mapping.isCollectionMapping()){ List fields = this.mapping.getContainerPolicy().getAdditionalFieldsForJoin((CollectionMapping)this.mapping); if (fields != null){ result.addAll(fields); } } return result; } } /** * INTERNAL: */ @Override public List getSelectionFields(ReadQuery query) { if (isAttribute()) { List result = new ArrayList<>(1); DatabaseField field = getField(); if (field != null) { result.add(field); } return result; } else { List result = new ArrayList<>(); result.addAll(super.getSelectionFields(query)); if ((this.mapping != null) && this.mapping.isCollectionMapping()){ List fields = this.mapping.getContainerPolicy().getAdditionalFieldsForJoin((CollectionMapping)this.mapping); if (fields != null){ result.addAll(fields); } } return result; } } /** * INTERNAL: * Transform the object-level value into a database-level value */ @Override public Object getFieldValue(Object objectValue, AbstractSession session) { DatabaseMapping mapping = getMapping(); Object fieldValue = objectValue; if (mapping != null) { if (mapping.isAbstractDirectMapping() || mapping.isDirectCollectionMapping()) { // CR#3623207, check for IN Collection here not in mapping. if (objectValue instanceof Collection values) { // This can actually be a collection for IN within expressions... however it would be better for expressions to handle this. List fieldValues = new ArrayList<>(values.size()); for (Iterator iterator = values.iterator(); iterator.hasNext();) { Object value = iterator.next(); if (!(value instanceof Expression)){ value = getFieldValue(value, session); } fieldValues.add(value); } fieldValue = fieldValues; } else { if (mapping.isAbstractColumnMapping()) { fieldValue = ((AbstractColumnMapping)mapping).getFieldValue(objectValue, session); } else if (mapping.isDirectCollectionMapping()) { fieldValue = ((DirectCollectionMapping)mapping).getFieldValue(objectValue, session); } } } else if ((objectValue instanceof Collection values) && (mapping.isForeignReferenceMapping())) { // Was an IN with a collection of objects, extract their ids. List ids = new ArrayList<>(); for (Object object : values) { if ((mapping.getReferenceDescriptor() != null) && (mapping.getReferenceDescriptor().getJavaClass().isInstance(object))) { Object id = mapping.getReferenceDescriptor().getObjectBuilder().extractPrimaryKeyFromObject(object, session); if (id instanceof CacheId cId) { id = Arrays.asList(cId.getPrimaryKey()); } ids.add(id); } else { ids.add(object); } } fieldValue = ids; } } return fieldValue; } @Override public DatabaseMapping getMapping() { if (!hasMapping) { return null; } if (mapping == null) { mapping = super.getMapping(); if (mapping == null) { hasMapping = false; } } return mapping; } public DatabaseMapping getMappingFromQueryKey() { QueryKey queryKey = getQueryKeyOrNull(); if ((queryKey == null) || (!(queryKey instanceof DirectQueryKey))) { throw QueryException.cannotConformExpression(); } mapping = queryKey.getDescriptor().getObjectBuilder().getMappingForField(((DirectQueryKey)queryKey).getField()); if (mapping == null) { throw QueryException.cannotConformExpression(); } return mapping; } @Override public String getName() { return name; } /** * INTERNAL: * Returns nested attribute name or null */ public String getNestedAttributeName() { if(getMapping() != null) { String attributeName = getMapping().getAttributeName(); if(this.baseExpression.isExpressionBuilder()) { return attributeName; } else if (this.baseExpression.isQueryKeyExpression()) { String nestedAttributeName = ((QueryKeyExpression)this.baseExpression).getNestedAttributeName(); if(nestedAttributeName == null) { return null; } else { return nestedAttributeName + '.' + attributeName; } } else { return null; } } else { return null; } } /** * INTERNAL: */ @Override public List getOwnedTables() { if ((getMapping() != null) && getMapping().isNestedTableMapping()) { List nestedTable = null; if (shouldQueryToManyRelationship()) { nestedTable = new ArrayList<>(super.getOwnedTables()); } else { nestedTable = new ArrayList<>(1); } nestedTable.add(new NestedTable(this)); return nestedTable; } if ((getMapping() != null) && (getMapping().isReferenceMapping() || getMapping().isStructureMapping())) { return null; } return super.getOwnedTables(); } @Override public QueryKey getQueryKeyOrNull() { if (!hasQueryKey) { return null; } // Oct 19, 2000 JED // Added try/catch. This was throwing a NPE in the following case // expresssionBuilder.get("firstName").get("bob") //moved by Gordon Yorke to cover validate and normalize if (getContainingDescriptor() == null) { throw QueryException.invalidQueryKeyInExpression(getName()); } if (queryKey == null) { queryKey = getContainingDescriptor().getQueryKeyNamed(getName()); if (queryKey == null) { hasQueryKey = false; } } return queryKey; } /* * PUBLIC: * Index method could be applied to QueryKeyExpression corresponding to CollectionMapping * that has non-null listOrderField (the field holding the index values). *

Example: *

* ReportQuery query = new ReportQuery(); * query.setReferenceClass(Employee.class); * ExpressionBuilder builder = query.getExpressionBuilder(); * Expression firstNameJohn = builder.get("firstName").equal("John"); * Expression anyOfProjects = builder.anyOf("projects"); * Expression exp = firstNameJohn.and(anyOfProjects.index().between(2, 4)); * query.setSelectionCriteria(exp); * query.addAttribute("projects", anyOfProjects); * * SELECT DISTINCT t0.PROJ_ID, t0.PROJ_TYPE, t0.DESCRIP, t0.PROJ_NAME, t0.LEADER_ID, t0.VERSION, t1.PROJ_ID, t1.BUDGET, t1.MILESTONE * FROM OL_PROJ_EMP t4, OL_SALARY t3, OL_EMPLOYEE t2, OL_LPROJECT t1, OL_PROJECT t0 * WHERE ((((t2.F_NAME = 'John') AND (t4.PROJ_ORDER BETWEEN 2 AND 4)) AND (t3.OWNER_EMP_ID = t2.EMP_ID)) AND * (((t4.EMP_ID = t2.EMP_ID) AND (t0.PROJ_ID = t4.PROJ_ID)) AND (t1.PROJ_ID (+) = t0.PROJ_ID))) *
*/ @Override public Expression index() { if(index == null) { index = new IndexExpression(this); } return index; } /** * INTERNAL: * Alias the database field for our current environment */ protected void initializeAliasedField() { DatabaseField tempField = getField().clone(); DatabaseTable aliasedTable = getAliasedTable(); // Put in a special check here so that if the aliasing does nothing we don't cache the // result because it's invalid. This saves us from caching premature data if e.g. debugging // causes us to print too early" // if (aliasedTable.equals(getField().getTable())) { // return; // } else { aliasedField = tempField; aliasedField.setTable(aliasedTable); // } } /** * INTERNAL: * Return if the expression is for a direct mapped attribute. */ @Override public boolean isAttribute() { if (isAttributeExpression == null) { if (getSession() == null) { // We can't tell, so say no. return false; } QueryKey queryKey = getQueryKeyOrNull(); if (queryKey != null) { isAttributeExpression = queryKey.isDirectQueryKey(); } else { DatabaseMapping mapping = getMapping(); if (mapping != null) { if (mapping.isVariableOneToOneMapping()) { throw QueryException.cannotQueryAcrossAVariableOneToOneMapping(mapping, mapping.getDescriptor()); } else { isAttributeExpression = mapping.isDirectToFieldMapping(); } } else { isAttributeExpression = Boolean.FALSE; } } } return isAttributeExpression; } @Override public boolean isQueryKeyExpression() { return true; } /* * INTERNAL: * If this query key represents a foreign reference answer the * base expression -> foreign reference join criteria. */ public Expression mappingCriteria(Expression base) { Expression selectionCriteria; // First look for a query key, then a mapping if (getQueryKeyOrNull() == null) { if ((getMapping() == null) || (!getMapping().isForeignReferenceMapping())) { return null; } else { // The join criteria is now twisted by the mappings. selectionCriteria = ((ForeignReferenceMapping)getMapping()).getJoinCriteria(this, base); } } else { if (!getQueryKeyOrNull().isForeignReferenceQueryKey()) { return null; } else { selectionCriteria = ((ForeignReferenceQueryKey)getQueryKeyOrNull()).getJoinCriteria(); selectionCriteria = this.baseExpression.twist(selectionCriteria, base); } } if (shouldUseOuterJoin() && getSession().getPlatform().shouldPrintOuterJoinInWhereClause()) { selectionCriteria = selectionCriteria.convertToUseOuterJoin(); } return selectionCriteria; } /** * INTERNAL: * Normalize the expression into a printable structure. * Any joins must be added to form a new root. */ @Override public Expression normalize(ExpressionNormalizer normalizer) { return normalize(normalizer, this, null); } /** * INTERNAL: * Check if new expression need to be created for sub queries and re-normalized. */ protected Expression checkJoinForSubSelectWithParent(ExpressionNormalizer normalizer, Expression base, List foreignKeyJoinPointer) { SQLSelectStatement statement = normalizer.getStatement(); if(!isClonedForSubQuery && statement.isSubSelect() && statement.getParentStatement().getBuilder().equals(getBuilder())) { if (baseExpression.isQueryKeyExpression()) { QueryKeyExpression baseQueryKeyExpression = (QueryKeyExpression) baseExpression; DatabaseMapping mapping = baseQueryKeyExpression.getMapping(); if (mapping != null && mapping.isOneToOneMapping()) { if (statement.getOptimizedClonedExpressions().containsKey(this)) { return statement.getOptimizedClonedExpressions().get(this); } QueryKeyExpression clonedBaseExpression = null; if (baseQueryKeyExpression.hasBeenNormalized()) { // Call normalize again to get same expression. clonedBaseExpression = (QueryKeyExpression) baseQueryKeyExpression.normalize(normalizer); } if (clonedBaseExpression == null && baseQueryKeyExpression.getBaseExpression().isQueryKeyExpression()) { DatabaseMapping basebaseExprMapping = ((QueryKeyExpression)baseQueryKeyExpression.getBaseExpression()).getMapping(); if (basebaseExprMapping != null && basebaseExprMapping.isOneToOneMapping()) { // Let base expression normalization re-create base base expression and normalize it if needed. clonedBaseExpression = (QueryKeyExpression) baseQueryKeyExpression.normalize(normalizer); } } if (clonedBaseExpression == null) { // Clone base expression & normalize clonedBaseExpression = new QueryKeyExpression(baseQueryKeyExpression.getName(), baseQueryKeyExpression.getBaseExpression()); clonedBaseExpression.shouldQueryToManyRelationship = baseQueryKeyExpression.shouldQueryToManyRelationship; clonedBaseExpression.shouldUseOuterJoin = baseQueryKeyExpression.shouldUseOuterJoin; clonedBaseExpression.hasQueryKey = baseQueryKeyExpression.hasQueryKey; clonedBaseExpression.hasMapping = baseQueryKeyExpression.hasMapping; clonedBaseExpression.isAttributeExpression = baseQueryKeyExpression.isAttributeExpression; clonedBaseExpression.isClonedForSubQuery = true; clonedBaseExpression = (QueryKeyExpression) clonedBaseExpression.normalize(normalizer); statement.addOptimizedClonedExpressions(baseQueryKeyExpression, clonedBaseExpression); } // Clone expression, normalize & return. QueryKeyExpression clonedExpression = new QueryKeyExpression(name, clonedBaseExpression); clonedExpression.shouldQueryToManyRelationship = this.shouldQueryToManyRelationship; clonedExpression.shouldUseOuterJoin = this.shouldUseOuterJoin; clonedExpression.hasQueryKey = this.hasQueryKey; clonedExpression.hasMapping = this.hasMapping; clonedExpression.isAttributeExpression = this.isAttributeExpression; clonedExpression.isClonedForSubQuery = true; if (base == this) { clonedExpression = (QueryKeyExpression) clonedExpression.normalize(normalizer, clonedExpression, foreignKeyJoinPointer); } else { // Caller invoked overloaded method with different base, RelationExpression in this case. clonedExpression = (QueryKeyExpression) clonedExpression.normalize(normalizer, base, foreignKeyJoinPointer); } statement.addOptimizedClonedExpressions(this, clonedExpression); return clonedExpression; } } } return null; } /** * INTERNAL: * For CR#2456 if this is part of an objExp.equal(objExp), do not need to add * additional expressions to normalizer both times, and the foreign key join * replaces the equal expression. */ public Expression normalize(ExpressionNormalizer normalizer, Expression base, List foreignKeyJoinPointer) { if (this.hasBeenNormalized) { return this; } // Bug 397619 - It should only be normalized by parent. // If subselect & not normalized, always clone and normalize // if it has parent builder. Expression clonedExpression = checkJoinForSubSelectWithParent(normalizer, base, foreignKeyJoinPointer); if (clonedExpression != null) { return clonedExpression; } super.normalize(normalizer); DatabaseMapping mapping = getMapping(); SQLSelectStatement statement = normalizer.getStatement(); if ((mapping != null) && mapping.isDirectToXMLTypeMapping()) { statement.setRequiresAliases(true); } // Check if any joins need to be added. if (isAttribute()) { return this; } ReadQuery query = normalizer.getStatement().getQuery(); // Record any class used in a join to invalidate query results cache. if ((query != null) && query.shouldCacheQueryResults()) { if ((mapping != null) && (mapping.getReferenceDescriptor() != null) && (mapping.getReferenceDescriptor().getJavaClass() != null)) { query.getQueryResultsCachePolicy().getInvalidationClasses().add(mapping.getReferenceDescriptor().getJavaClass()); } else { QueryKey queryKey = getQueryKeyOrNull(); if ((queryKey != null) && queryKey.isForeignReferenceQueryKey()) { query.getQueryResultsCachePolicy().getInvalidationClasses().add(((ForeignReferenceQueryKey)queryKey).getReferenceClass()); } } } // If the mapping is 'ref' or 'structure', no join needed. if ((mapping != null) && (mapping.isReferenceMapping() || mapping.isStructureMapping())) { statement.setRequiresAliases(true); return this; } // Compute if a distinct is required during normalization. if (shouldQueryToManyRelationship() && (!statement.isDistinctComputed()) && (!statement.isAggregateSelect())) { statement.useDistinct(); } // Turn off DISTINCT if nestedTableMapping is used (not supported by Oracle 8.1.5). if ((mapping != null) && mapping.isNestedTableMapping()) { // There are two types of nested tables, one used by clients, one used by mappings, do nothing in the mapping case. if (!shouldQueryToManyRelationship()) { return this; } statement.dontUseDistinct(); } // Normalize the ON clause if present. Need to use rebuild, not twist as parameters are real parameters. if (this.onClause != null) { this.onClause = this.onClause.normalize(normalizer); } Expression mappingExpression = mappingCriteria(base); if (mappingExpression != null) { mappingExpression = mappingExpression.normalize(normalizer); } if (mappingExpression != null) { // If the join was an outer join we must not add the join criteria to the where clause, // if the platform prints the join in the from clause. if (shouldUseOuterJoin() && (getSession().getPlatform().isInformixOuterJoin())) { setOuterJoinExpIndex(statement.addOuterJoinExpressionsHolders(this, mappingExpression, null, null)); normalizer.addAdditionalExpression(mappingExpression.and(additionalExpressionCriteria())); return this; } else if ((shouldUseOuterJoin() && (!getSession().getPlatform().shouldPrintOuterJoinInWhereClause())) || (!getSession().getPlatform().shouldPrintInnerJoinInWhereClause((normalizer.getStatement().getParentStatement() != null ? normalizer.getStatement().getParentStatement().getQuery() : normalizer.getStatement().getQuery())))) { setOuterJoinExpIndex(statement.addOuterJoinExpressionsHolders(this, mappingExpression, additionalExpressionCriteriaMap(), null)); if ((getDescriptor() != null) && (getDescriptor().getHistoryPolicy() != null)) { Expression historyOnClause = getDescriptor().getHistoryPolicy().additionalHistoryExpression(this, this, 0); if (getOnClause() != null) { setOnClause(getOnClause().and(historyOnClause)); } else { setOnClause(historyOnClause); } } return this; } else if (isUsingOuterJoinForMultitableInheritance() && (!getSession().getPlatform().shouldPrintOuterJoinInWhereClause())) { setOuterJoinExpIndex(statement.addOuterJoinExpressionsHolders(null, null, additionalExpressionCriteriaMap(), mapping.getReferenceDescriptor())); // fall through to the main case } // This must be added even if outer. Actually it should be converted to use a right outer join, but that gets complex // so we do not support this current which is a limitation in some cases. if (foreignKeyJoinPointer != null) { // If this expression is right side of an objExp.equal(objExp), one // need not add additionalExpressionCriteria twice. // Also the join will replace the original objExp.equal(objExp). // For CR#2456. foreignKeyJoinPointer.add(mappingExpression.and(this.onClause)); } else { normalizer.addAdditionalExpression(mappingExpression.and(additionalExpressionCriteria()).and(this.onClause)); } } // For bug 2900974 special code for DirectCollectionMappings moved to printSQL. return this; } /** * INTERNAL: * Print SQL onto the stream, using the ExpressionPrinter for context */ @Override public void printSQL(ExpressionSQLPrinter printer) { if (isAttribute()) { printer.printField(getAliasedField()); } // If the mapping is a direct collection then this falls into a gray area. // It must be treated as an attribute at this moment for it has a direct field. // However it is not an attribute in the sense that it also represents a foreign // reference and a mapping criteria has been added. // For bug 2900974 these are now handled as non-attributes during normalize but // as attributes when printing SQL. // if ((!isAttribute()) && (getMapping() != null) && getMapping().isDirectCollectionMapping()) { DirectCollectionMapping directCollectionMapping = (DirectCollectionMapping)getMapping(); // The aliased table comes for free as it was a required part of the join criteria. TableExpression table = (TableExpression)getTable(directCollectionMapping.getReferenceTable()); DatabaseTable aliasedTable = table.aliasForTable(table.getTable()); DatabaseField aliasedField = directCollectionMapping.getDirectField().clone(); aliasedField.setTable(aliasedTable); printer.printField(aliasedField); } if ((getMapping() != null) && getMapping().isNestedTableMapping()) { DatabaseTable tableAlias = aliasForTable(new NestedTable(this)); printer.printString(tableAlias.getName()); } } /** * INTERNAL: * Print java for project class generation */ @Override public void printJava(ExpressionJavaPrinter printer) { this.baseExpression.printJava(printer); if (!shouldUseOuterJoin()) { if (!shouldQueryToManyRelationship()) { printer.printString(".get("); } else { printer.printString(".anyOf("); } } else { if (!shouldQueryToManyRelationship()) { printer.printString(".getAllowingNull("); } else { printer.printString(".anyOfAllowingNone("); } } printer.printString("\"" + getName() + "\")"); } /** * INTERNAL: * This expression is built on a different base than the one we want. Rebuild it and * return the root of the new tree */ @Override public Expression rebuildOn(Expression newBase) { Expression newLocalBase = this.baseExpression.rebuildOn(newBase); QueryKeyExpression result = null; // For bug 3096634 rebuild outer joins correctly from the start. if (shouldUseOuterJoin) { result = (QueryKeyExpression)newLocalBase.getAllowingNull(getName()); } else { result = (QueryKeyExpression)newLocalBase.get(getName()); } if (shouldQueryToManyRelationship) { result.doQueryToManyRelationship(); } result.setSelectIfOrderedBy(selectIfOrderedBy()); if (castClass != null){ result.setCastClass(castClass); } return result; } /** * INTERNAL: * A special version of rebuildOn where the newBase need not be a new * ExpressionBuilder but any expression. *

* For nested joined attributes, the joined attribute query must have * its joined attributes rebuilt relative to it. */ public Expression rebuildOn(Expression oldBase, Expression newBase) { if (this == oldBase) { return newBase; } Expression newLocalBase = ((QueryKeyExpression)this.baseExpression).rebuildOn(oldBase, newBase); QueryKeyExpression result = null; // For bug 3096634 rebuild outer joins correctly from the start. if (shouldUseOuterJoin) { result = (QueryKeyExpression)newLocalBase.getAllowingNull(getName()); } else { result = (QueryKeyExpression)newLocalBase.get(getName()); } if (shouldQueryToManyRelationship) { result.doQueryToManyRelationship(); } result.setSelectIfOrderedBy(selectIfOrderedBy()); return result; } /** * Reset cached information here so that we can be sure we're accurate. */ @Override protected void resetCache() { hasMapping = true; mapping = null; hasQueryKey = true; queryKey = null; } public boolean shouldQueryToManyRelationship() { return shouldQueryToManyRelationship; } /** * INTERNAL: * Rebuild myself against the base, with the values of parameters supplied by the context * expression. This is used for transforming a standalone expression (e.g. the join criteria of a mapping) * into part of some larger expression. You normally would not call this directly, instead calling twist * See the comment there for more details" */ @Override public Expression twistedForBaseAndContext(Expression newBase, Expression context, Expression oldBase) { if (oldBase == null || this.baseExpression == oldBase) { Expression twistedBase = this.baseExpression.twistedForBaseAndContext(newBase, context, oldBase); QueryKeyExpression result = (QueryKeyExpression)twistedBase.get(getName()); if (shouldUseOuterJoin) { result.doUseOuterJoin(); } if (shouldQueryToManyRelationship) { result.doQueryToManyRelationship(); } return result; } return this; } /** * Do any required validation for this node. Throw an exception if it's incorrect. */ @Override public void validateNode() { QueryKey queryKey = getQueryKeyOrNull(); DatabaseMapping mapping = getMapping(); if ((queryKey == null) && (mapping == null)) { throw QueryException.invalidQueryKeyInExpression(getName()); } Object theOneThatsNotNull = null; boolean qkIsToMany = false; if (queryKey != null) { theOneThatsNotNull = queryKey; qkIsToMany = queryKey.isManyToManyQueryKey() || queryKey.isOneToManyQueryKey(); } boolean isNestedMapping = false; if (mapping != null) { // Bug 2847621 - Add Aggregate Collection to the list of valid items for outer join. if (this.shouldUseOuterJoin && (!(mapping.isOneToOneMapping() || mapping.isOneToManyMapping() || mapping.isManyToManyMapping() || mapping.isAggregateCollectionMapping() || mapping.isDirectCollectionMapping()))) { throw QueryException.outerJoinIsOnlyValidForOneToOneMappings(mapping); } qkIsToMany = mapping.isCollectionMapping(); if (this.index != null) { if (qkIsToMany) { CollectionMapping collectionMapping = (CollectionMapping)mapping; if(collectionMapping.getListOrderField() != null) { this.index.setField(collectionMapping.getListOrderField()); addDerivedField(this.index); } else { throw QueryException.indexRequiresCollectionMappingWithListOrderField(this, collectionMapping); } } else { throw QueryException.indexRequiresCollectionMappingWithListOrderField(this, mapping); } } isNestedMapping = mapping.isNestedTableMapping(); theOneThatsNotNull = mapping; } else { if (this.index != null) { throw QueryException.indexRequiresCollectionMappingWithListOrderField(this, null); } } if ((!shouldQueryToManyRelationship()) && qkIsToMany && (!isNestedMapping)) { throw QueryException.invalidUseOfToManyQueryKeyInExpression(theOneThatsNotNull); } if (shouldQueryToManyRelationship() && !qkIsToMany) { throw QueryException.invalidUseOfAnyOfInExpression(theOneThatsNotNull); } } /** * INTERNAL: * Return the value for in memory comparison. * This is only valid for valueable expressions. */ @Override public Object valueFromObject(Object object, AbstractSession session, AbstractRecord translationRow, int valueHolderPolicy, boolean isObjectUnregistered) { // The expression may be across a relationship, in which case it must be traversed. if ((!this.baseExpression.isExpressionBuilder()) && this.baseExpression.isQueryKeyExpression()) { object = this.baseExpression.valueFromObject(object, session, translationRow, valueHolderPolicy, isObjectUnregistered); // toDo: Null means the join filters out the row, returning null is not correct if an inner join, // outer/inner joins need to be fixed to filter correctly. if (object == null) { return null; } // If from an anyof the object will be a collection of values, // A new vector must union the object values and the values extracted from it. if (object instanceof List v) { List comparisonVector = new ArrayList<>(v.size() + 2); for (Iterator iterator1 = v.iterator(); iterator1.hasNext();) { Object vectorObject = iterator1.next(); if (vectorObject == null) { comparisonVector.add(null); } else { Object valueOrValues = valuesFromCollection(vectorObject, session, valueHolderPolicy, isObjectUnregistered); // If a collection of values were extracted union them. if (valueOrValues instanceof List vv) { comparisonVector.addAll(vv); } else { comparisonVector.add(valueOrValues); } } } return comparisonVector; } } return valuesFromCollection(object, session, valueHolderPolicy, isObjectUnregistered); } /** * INTERNAL * This method iterates through a collection and gets the values from the objects to conform in an in-memory query. */ public Object valuesFromCollection(Object object, AbstractSession session, int valueHolderPolicy, boolean isObjectUnregistered) { // in case the mapping is null - this can happen if a query key is being used // In this case, check for the query key and find it's mapping. boolean readMappingFromQueryKey = false; if (getMapping() == null) { getMappingFromQueryKey(); readMappingFromQueryKey = true; } // For bug 2780817 get the mapping directly from the object. In EJB 2.0 // inheritance, each child must override mappings defined in an abstract // class with its own. DatabaseMapping mapping = this.mapping; ClassDescriptor descriptor = mapping.getDescriptor(); if (descriptor.hasInheritance() && (descriptor.getJavaClass() != object.getClass())) { descriptor = descriptor.getInheritancePolicy().getDescriptor(object.getClass()); mapping = descriptor.getObjectBuilder().getMappingForAttributeName(mapping.getAttributeName()); } //fetch group support if (descriptor.hasFetchGroupManager()) { FetchGroupManager fetchGroupManager = descriptor.getFetchGroupManager(); if (fetchGroupManager.isPartialObject(object) && (!fetchGroupManager.isAttributeFetched(object, mapping.getAttributeName()))) { //the conforming attribute is not fetched, simply throw exception throw QueryException.cannotConformUnfetchedAttribute(mapping.getAttributeName()); } } if (mapping.isAbstractColumnMapping()) { return mapping.valueFromObject(object, mapping.getField(), session); } else if (mapping.isForeignReferenceMapping()) { //CR 3677 integration of a ValueHolderPolicy Object valueFromMapping = mapping.getAttributeValueFromObject(object); if (!((ForeignReferenceMapping)mapping).getIndirectionPolicy().objectIsInstantiated(valueFromMapping)) { if (valueHolderPolicy != InMemoryQueryIndirectionPolicy.SHOULD_TRIGGER_INDIRECTION) { //If the client wishes us to trigger the indirection then we should do so, //Other wise throw the exception throw QueryException.mustInstantiateValueholders();// you should instantiate the valueholder for this to work } // maybe we should throw this exception from the start, to save time } Object valueToIterate = mapping.getRealAttributeValueFromObject(object, session); UnitOfWorkImpl uow = isObjectUnregistered ? (UnitOfWorkImpl)session : null; // First check that object in fact is unregistered. // toDo: ?? Why is this commented out? Why are we supporting the unregistered thing at all? // Does not seem to be any public API for this, nor every used internally? //if (isObjectUnregistered) { // isObjectUnregistered = !uow.getCloneMapping().containsKey(object); //} if (mapping.isCollectionMapping() && (valueToIterate != null)) { // For bug 2766379 must use the correct version of vectorFor to // unwrap the result same time. valueToIterate = mapping.getContainerPolicy().vectorFor(valueToIterate, session); // toDo: If the value is empty, need to support correct inner/outer join filtering symantics. // For CR 2612601, try to partially replace the result with already // registered objects. if (isObjectUnregistered && (uow.getCloneMapping().get(object) == null)) { @SuppressWarnings({"unchecked"}) List objectValues = (List) valueToIterate; for (int i = 0; i < objectValues.size(); i++) { Object original = objectValues.get(i); Object clone = uow.getIdentityMapAccessorInstance().getIdentityMapManager().getFromIdentityMap(original); if (clone != null) { objectValues.set(i, clone); } } } // For CR 2612601, conforming without registering, a query could be // bob.get("address").get("city").equal("Ottawa"); where the address // has been registered and modified in the UOW, but bob has not. Thus // even though bob does not point to the modified address now, it will // as soon as it is registered, so should point to it here. } else if (isObjectUnregistered && (uow.getCloneMapping().get(object) == null)) { Object clone = uow.getIdentityMapAccessorInstance().getIdentityMapManager().getFromIdentityMap(valueToIterate); if (clone != null) { valueToIterate = clone; } } return valueToIterate; } else if (mapping.isAggregateMapping()) { Object aggregateValue = mapping.getAttributeValueFromObject(object); // Bug 3995468 - if this query key is to a mapping in an aggregate object, get the object from actual mapping rather than the aggregate mapping while (readMappingFromQueryKey && mapping.isAggregateObjectMapping() && !((AggregateObjectMapping)mapping).getReferenceClass().equals(queryKey.getDescriptor().getJavaClass())) { mapping = mapping.getReferenceDescriptor().getObjectBuilder().getMappingForField(((DirectQueryKey)queryKey).getField()); aggregateValue = mapping.getRealAttributeValueFromObject(aggregateValue, session); } return aggregateValue; } else { throw QueryException.cannotConformExpression(); } } /** * INTERNAL: * Lookup the descriptor for this item by traversing its expression recursively. */ @Override public ClassDescriptor getLeafDescriptor(DatabaseQuery query, ClassDescriptor rootDescriptor, AbstractSession session) { Expression baseExpression = getBaseExpression(); ClassDescriptor baseDescriptor = baseExpression.getLeafDescriptor(query, rootDescriptor, session); if (isMapEntryExpression()) { // get the expression that owns the mapping for the table entry Expression owningExpression = ((QueryKeyExpression)baseExpression).getBaseExpression(); ClassDescriptor owningDescriptor = owningExpression.getLeafDescriptor(query, rootDescriptor, session); // Get the mapping that owns the table CollectionMapping mapping = (CollectionMapping)owningDescriptor.getObjectBuilder().getMappingForAttributeName(baseExpression.getName()); return mapping.getContainerPolicy().getDescriptorForMapKey(); } ClassDescriptor descriptor = null; String attributeName = getName(); DatabaseMapping mapping = baseDescriptor.getObjectBuilder().getMappingForAttributeName(attributeName); if (mapping == null) { QueryKey queryKey = baseDescriptor.getQueryKeyNamed(attributeName); if (queryKey != null) { if (queryKey.isForeignReferenceQueryKey()) { descriptor = session.getDescriptor(((ForeignReferenceQueryKey)queryKey).getReferenceClass()); } else { // if (queryKey.isDirectQueryKey()) descriptor = queryKey.getDescriptor(); } } if (descriptor == null) { throw QueryException.invalidExpressionForQueryItem(this, query); } } else if (mapping.isAggregateMapping()) { descriptor = mapping.getReferenceDescriptor(); } else if (mapping.isForeignReferenceMapping()) { descriptor = mapping.getReferenceDescriptor(); } return descriptor; } /** * INTERNAL: * Lookup the mapping for this item by traversing its expression recursively. * If an aggregate of foreign mapping is found it is traversed. */ @Override public DatabaseMapping getLeafMapping(DatabaseQuery query, ClassDescriptor rootDescriptor, AbstractSession session) { if (isMapEntryExpression()){ MapEntryExpression mapEntryExpression = (MapEntryExpression)this; // get the expression that we want the table entry for QueryKeyExpression baseExpression = (QueryKeyExpression)mapEntryExpression.getBaseExpression(); // get the expression that owns the mapping for the table entry Expression owningExpression = baseExpression.getBaseExpression(); ClassDescriptor owningDescriptor = owningExpression.getLeafDescriptor(query, rootDescriptor, session); // Get the mapping that owns the table CollectionMapping mapping = (CollectionMapping)owningDescriptor.getObjectBuilder().getMappingForAttributeName(baseExpression.getName()); if (mapEntryExpression.shouldReturnMapEntry()) { return mapping; } if (mapping.getContainerPolicy().isMappedKeyMapPolicy()){ MappedKeyMapContainerPolicy policy = (MappedKeyMapContainerPolicy)mapping.getContainerPolicy(); return (DatabaseMapping)policy.getKeyMapping(); } return mapping; } Expression baseExpression = getBaseExpression(); ClassDescriptor descriptor = baseExpression.getLeafDescriptor(query, rootDescriptor, session); if (descriptor == null){ return null; } return descriptor.getObjectBuilder().getMappingForAttributeName(getName()); } /** * INTERNAL: * Used to print a debug form of the expression tree. */ @Override public void writeDescriptionOn(BufferedWriter writer) throws IOException { writer.write(getName()); if (castClass != null){ writer.write(" (" + castClass.getName() + ") "); } writer.write(tableAliasesDescription()); } /** * INTERNAL: * Indicates whether this expression corresponds to DirectCollection. */ @Override public boolean isDirectCollection() { if(getMapping() != null) { return getMapping().isDirectCollectionMapping(); } else { if(getQueryKeyOrNull() != null) { return this.queryKey.isDirectCollectionQueryKey(); } else { return false; } } } /** * INTERNAL: * Indicates whether this expression corresponds to OneToOne. */ public boolean isOneToOne() { if(getMapping() != null) { return getMapping().isOneToOneMapping(); } else { if(getQueryKeyOrNull() != null) { return this.queryKey.isOneToOneQueryKey(); } else { return false; } } } /** * INTERNAL: * Indicates whether this expression corresponds to OneToMany. */ public boolean isOneToMany() { if(getMapping() != null) { return getMapping().isOneToManyMapping(); } else { if(getQueryKeyOrNull() != null) { return this.queryKey.isOneToManyQueryKey(); } else { return false; } } } /** * INTERNAL: * Indicates whether this expression corresponds to ManyToMany. */ public boolean isManyToMany() { if(getMapping() != null) { return getMapping().isManyToManyMapping(); } else { if(getQueryKeyOrNull() != null) { return this.queryKey.isManyToManyQueryKey(); } else { return false; } } } /** * INTERNAL: * Return if the expression if for a map key mapping where the key is a OneToOne. */ public boolean isMapKeyObjectRelationship() { if (getMapping() != null) { return getMapping().isCollectionMapping() && ((CollectionMapping)getMapping()).isMapKeyObjectRelationship(); } else { return false; } } /** * INTERNAL: * Return if descriptor for the map key mapping where the key is a OneToOne. */ public ClassDescriptor getMapKeyDescriptor() { return getMapping().getContainerPolicy().getDescriptorForMapKey(); } /** * Calculate the reference table for based on the various QueryKeyExpression * usages (join query keys, custom defined query keys, or query keys for * mappings). *

* Called from {@link SQLSelectStatement#appendFromClauseForOuterJoin}. * * @return DatabaseTable */ public DatabaseTable getReferenceTable() { if(getMapping() != null) { if (getMapping().isDirectCollectionMapping()) { return ((DirectCollectionMapping)getMapping()).getReferenceTable(); } else { return getMapping().getReferenceDescriptor().getTables().get(0); } } else { return ((ForeignReferenceQueryKey)getQueryKeyOrNull()).getReferenceTable(getDescriptor()); } } /** * Calculate the source table for based on the various QueryKeyExpression * usages (join query keys, custom defined query keys, or query keys for * mappings). *

* Called from {@link SQLSelectStatement#appendFromClauseForOuterJoin}. * * @return DatabaseTable */ public DatabaseTable getSourceTable() { if (getBaseExpression().isExpressionBuilder() && getBuilder().hasViewTable()) { return getBuilder().getViewTable(); } if (getMapping() != null) { // Grab the source table from the mapping not just the first table // from the descriptor. In an joined inheritance hierarchy, the // fk used in the outer join may be from a subclasses's table. if (getMapping().isObjectReferenceMapping() && ((ObjectReferenceMapping) getMapping()).isForeignKeyRelationship()) { return getMapping().getFields().get(0).getTable(); } else { return ((ObjectExpression)this.baseExpression).getDescriptor().getTables().get(0); } } else { return ((ForeignReferenceQueryKey)getQueryKeyOrNull()).getSourceTable(); } } /** * Calculate the relation table for based on the various QueryKeyExpression * usages (join query keys, custom defined query keys, or query keys for * mappings). *

* Called from {@link SQLSelectStatement#appendFromClauseForOuterJoin}. * * @return DatabaseTable */ @Override public DatabaseTable getRelationTable() { if(getMapping() != null) { if(getMapping().isManyToManyMapping()) { return ((ManyToManyMapping)getMapping()).getRelationTable(); } else if(getMapping().isOneToOneMapping()) { return ((OneToOneMapping)getMapping()).getRelationTable(); } } else { if(getQueryKeyOrNull().isForeignReferenceQueryKey()) { return ((ForeignReferenceQueryKey)getQueryKeyOrNull()).getRelationTable(getDescriptor()); } } return null; } }