/******************************************************************************* * Copyright (c) 1998, 2015 Oracle and/or its affiliates. All rights reserved. * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0 * which accompanies this distribution. * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html * and the Eclipse Distribution License is available at * http://www.eclipse.org/org/documents/edl-v10.php. * * Contributors: * Oracle - initial API and implementation from Oracle TopLink * 05/16/2008-1.0M8 Guy Pelletier * - 218084: Implement metadata merging functionality between mapping files * 09/23/2008-1.1 Guy Pelletier * - 241651: JPA 2.0 Access Type support * 12/12/2008-1.1 Guy Pelletier * - 249860: Implement table per class inheritance support. * 02/06/2009-2.0 Guy Pelletier * - 248293: JPA 2.0 Element Collections (part 2) * 03/27/2009-2.0 Guy Pelletier * - 241413: JPA 2.0 Add EclipseLink support for Map type attributes * 06/02/2009-2.0 Guy Pelletier * - 278768: JPA 2.0 Association Override Join Table * 09/29/2009-2.0 Guy Pelletier * - 282553: JPA 2.0 JoinTable support for OneToOne and ManyToOne * 11/02/2009-2.0 Michael O'Brien * - 266912: JPA 2.0 Metamodel support for 1:m as 1:1 in DI 96 * 04/27/2010-2.1 Guy Pelletier * - 309856: MappedSuperclasses from XML are not being initialized properly * 06/14/2010-2.2 Guy Pelletier * - 264417: Table generation is incorrect for JoinTables in AssociationOverrides * 09/03/2010-2.2 Guy Pelletier * - 317286: DB column lenght not in sync between @Column and @JoinColumn * 03/24/2011-2.3 Guy Pelletier * - 337323: Multi-tenant with shared schema support (part 1) * 05/17/2012-2.3.3 Arron Ferguson * - 379829: NPE Thrown with OneToOne Relationship * 11/19/2012-2.5 Guy Pelletier * - 389090: JPA 2.1 DDL Generation Support (foreign key metadata support) * 11/28/2012-2.5 Guy Pelletier * - 374688: JPA 2.1 Converter support ******************************************************************************/ package org.eclipse.persistence.internal.jpa.metadata.accessors.mappings; import java.util.List; import java.util.Map; import org.eclipse.persistence.eis.mappings.EISOneToManyMapping; import org.eclipse.persistence.exceptions.ValidationException; import org.eclipse.persistence.internal.helper.DatabaseField; import org.eclipse.persistence.internal.jpa.metadata.MetadataDescriptor; import org.eclipse.persistence.internal.jpa.metadata.MetadataLogger; import org.eclipse.persistence.internal.jpa.metadata.accessors.classes.ClassAccessor; import org.eclipse.persistence.internal.jpa.metadata.accessors.objects.MetadataAnnotatedElement; import org.eclipse.persistence.internal.jpa.metadata.accessors.objects.MetadataAnnotation; import org.eclipse.persistence.internal.jpa.metadata.columns.AssociationOverrideMetadata; import org.eclipse.persistence.internal.jpa.metadata.columns.JoinColumnMetadata; import org.eclipse.persistence.mappings.CollectionMapping; import org.eclipse.persistence.mappings.DatabaseMapping; import org.eclipse.persistence.mappings.EmbeddableMapping; import org.eclipse.persistence.mappings.ManyToManyMapping; import org.eclipse.persistence.mappings.OneToManyMapping; import org.eclipse.persistence.mappings.OneToOneMapping; import org.eclipse.persistence.mappings.UnidirectionalOneToManyMapping; /** * INTERNAL: * A OneToMany relationship accessor. A OneToMany annotation currently is not * required to be on the accessible object, that is, a 1-M can default. * * Key notes: * - any metadata mapped from XML to this class must be compared in the * equals method. * - any metadata mapped from XML to this class must be handled in the merge * method. (merging is done at the accessor/mapping level) * - any metadata mapped from XML to this class must be initialized in the * initXMLObject method. * - methods should be preserved in alphabetical order. * * @author Guy Pelletier * @since TopLink EJB 3.0 Reference Implementation */ public class OneToManyAccessor extends CollectionAccessor { /** * INTERNAL: * Used for OX mapping. */ public OneToManyAccessor() { super("<one-to-many>"); } /** * INTERNAL: */ public OneToManyAccessor(MetadataAnnotation oneToMany, MetadataAnnotatedElement annotatedElement, ClassAccessor classAccessor) { super(oneToMany, annotatedElement, classAccessor); // A one to many mapping can default. if (oneToMany != null) { setOrphanRemoval(oneToMany.getAttributeBooleanDefaultFalse("orphanRemoval")); } } /** * INTERNAL: */ @Override public boolean equals(Object objectToCompare) { return super.equals(objectToCompare) && objectToCompare instanceof OneToManyAccessor; } @Override public int hashCode() { return super.hashCode(); } /** * INTERNAL: * * Return the logging context for this accessor. */ protected String getLoggingContext() { return MetadataLogger.ONE_TO_MANY_MAPPING_REFERENCE_CLASS; } /** * INTERNAL: */ @Override public boolean isOneToMany() { return true; } /** * INTERNAL: * Process a OneToMany accessor into an EclipseLink OneToManyMapping. If a * JoinTable is found however, we must create a ManyToManyMapping. */ @Override public void process() { super.process(); if (getDescriptor().getClassDescriptor().isEISDescriptor()) { // EIS 1-m is always a m-m relation. processManyToManyMapping(); } else if (hasMappedBy()) { // Process a 1-M using the mapped by mapping values. processOneToManyMapping(); } else if (getJoinColumns().isEmpty()) { // No join columns and no mapped by value, default to // unidirectional 1-M using a M-M mapping and a join table. processManyToManyMapping(); } else { // If we find join column(s) then process a uni-directional 1-M. processUnidirectionalOneToManyMapping(); } } /** * INTERNAL: * Process an association override for either an embedded object mapping, * or a map mapping (element-collection, 1-M and M-M) containing an * embeddable object as the value or key. */ @Override protected void processAssociationOverride(AssociationOverrideMetadata associationOverride, EmbeddableMapping embeddableMapping, MetadataDescriptor owningDescriptor) { if (getMapping().isUnidirectionalOneToManyMapping()) { // Create an override mapping and process the join columns to it. UnidirectionalOneToManyMapping overrideMapping = new UnidirectionalOneToManyMapping(); overrideMapping.setAttributeName(getAttributeName()); processUnidirectionalOneToManyTargetForeignKeyRelationship(overrideMapping, associationOverride.getJoinColumns(), owningDescriptor); // The override mapping will have the correct source, sourceRelation, // target and targetRelation keys. Along with the correct relation table. embeddableMapping.addOverrideUnidirectionalOneToManyMapping(overrideMapping); // Set the override mapping which will have the correct metadata // set. This is the metadata any non-owning relationship accessor // referring to this accessor will need. setOverrideMapping(overrideMapping); } else { super.processAssociationOverride(associationOverride, embeddableMapping, owningDescriptor); } } /** * INTERNAL: * Process an many to many mapping for this accessor since a join table * was specified. */ protected void processManyToManyMapping() { // Create a M-M mapping and process common collection mapping metadata // first followed by specific metadata. // Allow for different descriptor types (EIS) to create different mapping types. CollectionMapping mapping = getDescriptor().getClassDescriptor().newManyToManyMapping(); process(mapping); if (mapping instanceof ManyToManyMapping) { // 266912: If this 1:n accessor is different than the n:n mapping - track this ((ManyToManyMapping) mapping).setDefinedAsOneToManyMapping(true); // Process the JoinTable metadata. processJoinTable(mapping, ((ManyToManyMapping) mapping).getRelationTableMechanism(), getJoinTableMetadata()); } else if (mapping instanceof EISOneToManyMapping) { processEISOneToManyMapping((EISOneToManyMapping) mapping); } } /** * INTERNAL: * Process an one to many mapping for this accessor. */ protected void processOneToManyMapping() { // Non-owning side, process the foreign keys from the owner. DatabaseMapping owningMapping = getOwningMapping(); if (owningMapping.isOneToOneMapping()){ OneToOneMapping ownerMapping = (OneToOneMapping) owningMapping; // If the owner uses a relation table mechanism we must map a M-M. if (ownerMapping.hasRelationTableMechanism()) { ManyToManyMapping mapping = new ManyToManyMapping(); // Process the common collection mapping. process(mapping); // Process the mapped by relation table metadata. processMappedByRelationTable(ownerMapping.getRelationTableMechanism(), mapping.getRelationTableMechanism()); // Set the mapping to read only mapping.setIsReadOnly(true); mapping.setMappedBy(getMappedBy()); } else { // Create a 1-M mapping and process common collection mapping // metadata first followed by specific metadata. OneToManyMapping mapping = new OneToManyMapping(); process(mapping); Map<DatabaseField, DatabaseField> keys = ownerMapping.getSourceToTargetKeyFields(); for (DatabaseField fkField : keys.keySet()) { DatabaseField pkField = keys.get(fkField); // If we are within a table per class strategy we have to update // the primary key field to point to our own database table. // The extra table check is if the mapping is actually defined // on our java class (meaning we have the right table at this // point and can avoid the cloning) if (getDescriptor().usesTablePerClassInheritanceStrategy() && ! pkField.getTable().equals(getDescriptor().getPrimaryTable())) { // We need to update the pk field to be to our table. pkField = pkField.clone(); pkField.setTable(getDescriptor().getPrimaryTable()); } mapping.addTargetForeignKeyField(fkField, pkField); } mapping.setMappedBy(getMappedBy()); } } else { // If improper mapping encountered, throw an exception. throw ValidationException.invalidMapping(getJavaClass(), getReferenceClass()); } } /** * INTERNAL: * Process an unidirectional one to many mapping for this accessor since * join columns were specified and no mapped by value. */ protected void processUnidirectionalOneToManyMapping() { // Create a 1-M unidirectional mapping and process common collection // mapping metadata first followed by specific metadata. UnidirectionalOneToManyMapping mapping = new UnidirectionalOneToManyMapping(); process(mapping); // Process the JoinColumn metadata. processUnidirectionalOneToManyTargetForeignKeyRelationship(mapping, getJoinColumns(getJoinColumns(), getOwningDescriptor()), getOwningDescriptor()); } /** * INTERNAL: * Process the join column(s) metadata for the owning side of a * unidirectional one to many mapping. The default pk used only with single * primary key entities. The processor should never get as far as to use * them with entities that have a composite primary key (validation * exception will be thrown). */ protected void processUnidirectionalOneToManyTargetForeignKeyRelationship(UnidirectionalOneToManyMapping mapping, List<JoinColumnMetadata> joinColumns, MetadataDescriptor owningDescriptor) { // If the fk field (name) is not specified, it defaults to the // concatenation of the following: the name of the referencing // relationship property or field of the referencing entity; "_"; // the name of the referenced primary key column. String defaultFKFieldName = getDefaultAttributeName() + "_" + owningDescriptor.getPrimaryKeyFieldName(); // Join columns will come from a @JoinColumn(s). // Add the source foreign key fields to the mapping. for (JoinColumnMetadata joinColumn : joinColumns) { // Look up the primary key field from the referenced column name. DatabaseField pkField = getReferencedField(joinColumn.getReferencedColumnName(), owningDescriptor, MetadataLogger.PK_COLUMN); DatabaseField fkField = joinColumn.getForeignKeyField(pkField); setFieldName(fkField, defaultFKFieldName, MetadataLogger.FK_COLUMN); // Set the table name if one is not already set. if (!fkField.hasTableName()) { fkField.setTable(getReferenceDescriptor().getPrimaryTable()); } // Uni-directional 12M mapping would like a type on the foreign key // field. If one is not set, a unidirectional one to many mapping // will try to set one itself in its postInitialize. There is // currently a bug against this since the postInitiaze does not // handle the fact that the descriptor may be an aggregate, hence // not be able to look up a primary key mapping correctly. // From a metadata processing standpoint, we'll attempt to make sure // one is set. Meaning in some cases we won't be able to if we don't // have an associated mapping accessor for the pkField. So why // wouldn't we? One, it could be a bogus field specified only for // testing purposes to ensure an override is correctly applied and // two, the field could be part of a derived id (which at this point // we don't have the mapping accessor readily accessible. (may be // able to fix this if it becomes a problem). And thirdly, there is // the 'off' chance we've screwed up metadata processing somewhere // ( yeah right! ) so instead of show casing our mistakes, let's // hide them! :-) Anyway, long story short, if there is no // mappingAccessor for the pkField, don't do anything and silently // continue. Best we can do right now ... MappingAccessor mappingAccessor = owningDescriptor.getPrimaryKeyAccessorForField(pkField); if (mappingAccessor != null) { // If the mapping specified a converter then the field // classification may be set so check it first. Class fieldClassification = mappingAccessor.getMapping().getFieldClassification(mappingAccessor.getMapping().getField()); String typeName; if (fieldClassification == null) { // No fieldClassification, use the raw class from the // mapping accessor. typeName = mappingAccessor.getRawClass().getName(); } else { typeName = fieldClassification.getName(); } fkField.setTypeName(typeName); } // Add target foreign key to the mapping. mapping.addTargetForeignKeyField(fkField, pkField); // If any of the join columns is marked read-only then set the // mapping to be read only. if (fkField.isReadOnly()) { mapping.setIsReadOnly(true); } } } }