ManyToManyRelationConfigurer.java
package org.codefilarete.stalactite.engine.configurer.manytomany;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.Collection;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
import org.codefilarete.reflection.Accessor;
import org.codefilarete.reflection.AccessorByMethod;
import org.codefilarete.reflection.AccessorDefinition;
import org.codefilarete.reflection.Accessors;
import org.codefilarete.reflection.MutatorByMethod;
import org.codefilarete.reflection.PropertyAccessor;
import org.codefilarete.stalactite.dsl.entity.EntityMappingConfiguration;
import org.codefilarete.stalactite.dsl.naming.AssociationTableNamingStrategy;
import org.codefilarete.stalactite.dsl.naming.AssociationTableNamingStrategy.ReferencedColumnNames;
import org.codefilarete.stalactite.dsl.naming.ColumnNamingStrategy;
import org.codefilarete.stalactite.dsl.naming.ForeignKeyNamingStrategy;
import org.codefilarete.stalactite.dsl.naming.JoinColumnNamingStrategy;
import org.codefilarete.stalactite.dsl.naming.TableNamingStrategy;
import org.codefilarete.stalactite.dsl.property.CascadeOptions.RelationMode;
import org.codefilarete.stalactite.engine.configurer.AbstractRelationConfigurer;
import org.codefilarete.stalactite.engine.configurer.AssociationRecordMapping;
import org.codefilarete.stalactite.engine.configurer.CascadeConfigurationResult;
import org.codefilarete.stalactite.engine.configurer.EntityMappingConfigurationWithTable;
import org.codefilarete.stalactite.engine.configurer.IndexedAssociationRecordMapping;
import org.codefilarete.stalactite.engine.configurer.builder.PersisterBuilderContext;
import org.codefilarete.stalactite.engine.configurer.manytomany.ManyToManyRelation.MappedByConfiguration;
import org.codefilarete.stalactite.engine.configurer.manytomany.ManyToManyRelation.ShiftedMappedByConfiguration;
import org.codefilarete.stalactite.engine.configurer.onetomany.FirstPhaseCycleLoadListener;
import org.codefilarete.stalactite.engine.runtime.AssociationRecord;
import org.codefilarete.stalactite.engine.runtime.AssociationRecordPersister;
import org.codefilarete.stalactite.engine.runtime.AssociationTable;
import org.codefilarete.stalactite.engine.runtime.ConfiguredRelationalPersister;
import org.codefilarete.stalactite.engine.runtime.IndexedAssociationRecord;
import org.codefilarete.stalactite.engine.runtime.IndexedAssociationTable;
import org.codefilarete.stalactite.engine.runtime.onetomany.AbstractOneToManyWithAssociationTableEngine;
import org.codefilarete.stalactite.engine.runtime.onetomany.IndexedAssociationTableManyRelationDescriptor;
import org.codefilarete.stalactite.engine.runtime.onetomany.ManyRelationDescriptor;
import org.codefilarete.stalactite.engine.runtime.onetomany.OneToManyWithAssociationTableEngine;
import org.codefilarete.stalactite.engine.runtime.onetomany.OneToManyWithIndexedAssociationTableEngine;
import org.codefilarete.stalactite.sql.ConnectionConfiguration;
import org.codefilarete.stalactite.sql.Dialect;
import org.codefilarete.stalactite.sql.ddl.structure.PrimaryKey;
import org.codefilarete.stalactite.sql.ddl.structure.Table;
import org.codefilarete.tool.Nullable;
import org.codefilarete.tool.Reflections;
import org.codefilarete.tool.bean.FieldIterator;
import org.codefilarete.tool.bean.InstanceFieldIterator;
import org.codefilarete.tool.collection.Iterables;
import org.danekja.java.util.function.serializable.SerializableBiConsumer;
import static org.codefilarete.tool.Nullable.nullable;
/**
* @param <SRC> type of input (left/source entities)
* @param <TRGT> type of output (right/target entities)
* @param <SRCID> identifier type of source entities
* @param <TRGTID> identifier type of target entities
* @param <C1> collection type of the relation
* @author Guillaume Mary
*/
public class ManyToManyRelationConfigurer<SRC, TRGT, SRCID, TRGTID, C1 extends Collection<TRGT>, C2 extends Collection<SRC>>
extends AbstractRelationConfigurer<SRC, SRCID, TRGT, TRGTID> {
private final ForeignKeyNamingStrategy foreignKeyNamingStrategy;
private final JoinColumnNamingStrategy joinColumnNamingStrategy;
private final ColumnNamingStrategy indexColumnNamingStrategy;
private final AssociationTableNamingStrategy associationTableNamingStrategy;
private final PrimaryKey<?, SRCID> leftPrimaryKey;
public ManyToManyRelationConfigurer(ConfiguredRelationalPersister<SRC, SRCID> sourcePersister,
Dialect dialect,
ConnectionConfiguration connectionConfiguration,
TableNamingStrategy tableNamingStrategy,
ForeignKeyNamingStrategy foreignKeyNamingStrategy,
JoinColumnNamingStrategy joinColumnNamingStrategy,
ColumnNamingStrategy indexColumnNamingStrategy,
AssociationTableNamingStrategy associationTableNamingStrategy,
PersisterBuilderContext currentBuilderContext) {
super(dialect, connectionConfiguration, sourcePersister, tableNamingStrategy, currentBuilderContext);
this.foreignKeyNamingStrategy = foreignKeyNamingStrategy;
this.joinColumnNamingStrategy = joinColumnNamingStrategy;
this.indexColumnNamingStrategy = indexColumnNamingStrategy;
this.associationTableNamingStrategy = associationTableNamingStrategy;
this.leftPrimaryKey = sourcePersister.getMapping().getTargetTable().getPrimaryKey();
}
public void configure(ManyToManyRelation<SRC, TRGT, TRGTID, C1, C2> manyToManyRelation) {
RelationMode maintenanceMode = manyToManyRelation.getRelationMode();
// selection is always present (else configuration is nonsense !)
boolean orphanRemoval = maintenanceMode == RelationMode.ALL_ORPHAN_REMOVAL;
boolean writeAuthorized = maintenanceMode != RelationMode.READ_ONLY;
ManyToManyAssociationConfiguration<SRC, TRGT, SRCID, TRGTID, C1, C2, ?, ?> associationConfiguration = new ManyToManyAssociationConfiguration<>(manyToManyRelation,
sourcePersister,
leftPrimaryKey,
foreignKeyNamingStrategy,
indexColumnNamingStrategy,
orphanRemoval, writeAuthorized);
ManyToManyWithAssociationTableConfigurer<SRC, TRGT, SRCID, TRGTID, C1, C2, ?, ?> configurer = new ManyToManyWithAssociationTableConfigurer<>(associationConfiguration,
associationTableNamingStrategy,
dialect,
maintenanceMode == RelationMode.ASSOCIATION_ONLY,
connectionConfiguration);
String relationName = AccessorDefinition.giveDefinition(manyToManyRelation.getCollectionAccessor()).getName();
EntityMappingConfiguration<TRGT, TRGTID> targetMappingConfiguration = manyToManyRelation.getTargetMappingConfiguration();
if (currentBuilderContext.isCycling(targetMappingConfiguration)) {
// cycle detected
// we had a second phase load because cycle can hardly be supported by simply joining things together because at one time we will
// fall into infinite loop (think to SQL generation of a cycling graph ...)
Class<TRGT> targetEntityType = targetMappingConfiguration.getEntityType();
// adding the relation to an eventually already existing cycle configurer for the entity
ManyToManyCycleConfigurer<TRGT> cycleSolver = (ManyToManyCycleConfigurer<TRGT>)
Iterables.find(currentBuilderContext.getBuildLifeCycleListeners(), p -> p instanceof ManyToManyCycleConfigurer && ((ManyToManyCycleConfigurer<?>) p).getEntityType() == targetEntityType);
if (cycleSolver == null) {
cycleSolver = new ManyToManyCycleConfigurer<>(targetEntityType);
currentBuilderContext.addBuildLifeCycleListener(cycleSolver);
}
cycleSolver.addCycleSolver(relationName, configurer);
} else {
// NB: even if no table is found in configuration, build(..) will create one
Table targetTable = determineTargetTable(associationConfiguration.getManyToManyRelation());
ConfiguredRelationalPersister<TRGT, TRGTID> targetPersister = persisterBuilder.buildOrGiveExisting(new EntityMappingConfigurationWithTable<>(targetMappingConfiguration, targetTable));
configurer.configure(targetPersister, associationConfiguration.getManyToManyRelation().isFetchSeparately());
}
}
private Table determineTargetTable(ManyToManyRelation<SRC, TRGT, TRGTID, C1, C2> manyToManyRelation) {
Table targetTable = manyToManyRelation.getTargetMappingConfiguration().getTable();
if (targetTable == null) {
targetTable = lookupTableInRegisteredPersisters(manyToManyRelation.getTargetMappingConfiguration().getEntityType());
}
return targetTable;
}
/**
* Configurer
*/
public static class ManyToManyWithAssociationTableConfigurer<SRC, TRGT, SRCID, TRGTID, C1 extends Collection<TRGT>, C2 extends Collection<SRC>,
LEFTTABLE extends Table<LEFTTABLE>, RIGHTTABLE extends Table<RIGHTTABLE>> {
private final ManyToManyAssociationConfiguration<SRC, TRGT, SRCID, TRGTID, C1, C2, LEFTTABLE, RIGHTTABLE> associationConfiguration;
private final AssociationTableNamingStrategy associationTableNamingStrategy;
private final Dialect dialect;
private final boolean maintainAssociationOnly;
private final ConnectionConfiguration connectionConfiguration;
private AbstractOneToManyWithAssociationTableEngine<SRC, TRGT, SRCID, TRGTID, C1, ? extends AssociationRecord, ?> associationTableEngine;
private ManyToManyWithAssociationTableConfigurer(ManyToManyAssociationConfiguration<SRC, TRGT, SRCID, TRGTID, C1, C2, LEFTTABLE, RIGHTTABLE> associationConfiguration,
AssociationTableNamingStrategy associationTableNamingStrategy,
Dialect dialect,
boolean maintainAssociationOnly,
ConnectionConfiguration connectionConfiguration) {
this.associationConfiguration = associationConfiguration;
this.associationTableNamingStrategy = associationTableNamingStrategy;
this.dialect = dialect;
this.maintainAssociationOnly = maintainAssociationOnly;
this.connectionConfiguration = connectionConfiguration;
}
private void prepare(ConfiguredRelationalPersister<TRGT, TRGTID> targetPersister) {
// case : Collection mapping without reverse property: an association table is needed
RIGHTTABLE rightTable = targetPersister.<RIGHTTABLE>getMapping().getTargetTable();
PrimaryKey<RIGHTTABLE, TRGTID> rightPrimaryKey = rightTable.getPrimaryKey();
String associationTableName = nullable(associationConfiguration.getManyToManyRelation().getAssociationTableName()).getOr(() -> associationTableNamingStrategy.giveName(associationConfiguration.getAccessorDefinition(),
associationConfiguration.getLeftPrimaryKey(), rightPrimaryKey));
if (associationConfiguration.getManyToManyRelation().isOrdered()) {
assignEngineForIndexedAssociation(rightPrimaryKey, associationTableName, targetPersister);
} else {
assignEngineForNonIndexedAssociation(rightPrimaryKey, associationTableName, targetPersister);
}
}
/**
* Build the combiner between target entities and source ones.
*
* @param targetClass target entity type, provided to look up for reverse property if no sufficient info was given
* @return null if no information was provided about the reverse side (no bidirectionality)
*/
private BiConsumer<TRGT, SRC> buildReverseCombiner(Class<TRGT> targetClass) {
MappedByConfiguration<SRC, TRGT, C2> mappedByConfiguration = associationConfiguration.getManyToManyRelation().getMappedByConfiguration();
if (mappedByConfiguration.isEmpty()) {
// relation is not bidirectional, and not even set by the reverse link, there's nothing to do
return null;
} else {
PropertyAccessor<TRGT, C2> collectionAccessor = associationConfiguration.buildReversePropertyAccessor();
if (collectionAccessor == null) {
// since some reverse info has been done but not the collection accessor, we try to find the matching property by type
FieldIterator targetFields = new InstanceFieldIterator(targetClass);
Class<SRC> sourceEntityType = associationConfiguration.getSrcPersister().getClassToPersist();
Field reverseField = Iterables.find(targetFields, field -> Collection.class.isAssignableFrom(field.getType())
&& ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0].equals(sourceEntityType));
if (reverseField != null) {
Nullable<AccessorByMethod<TRGT, C2>> reverseGetterMethod = nullable(Accessors.accessorByMethod(reverseField));
if (reverseGetterMethod.isPresent()) {
collectionAccessor = new PropertyAccessor<>(reverseGetterMethod.get());
} else {
Nullable<MutatorByMethod<TRGT, C2>> reverseSetterMethod = nullable(Accessors.mutatorByMethod(reverseField));
if (reverseSetterMethod.isPresent()) {
collectionAccessor = new PropertyAccessor<>(reverseSetterMethod.get());
}
}
} // else : relation is not bidirectional, or not a usual one, may be set by reverse link
}
Nullable<SerializableBiConsumer<TRGT, SRC>> configuredCombiner = nullable(mappedByConfiguration.getReverseCombiner());
BiConsumer<TRGT, SRC> result;
if (collectionAccessor == null) {
result = configuredCombiner.get();
} else {
// collection factory is in priority the one configured
Supplier<C2> reverseCollectionFactory = mappedByConfiguration.getReverseCollectionFactory();
if (reverseCollectionFactory == null) {
Class<C2> collectionType = AccessorDefinition.giveDefinition(collectionAccessor).getMemberType();
reverseCollectionFactory = Reflections.giveCollectionFactory(collectionType);
}
PropertyAccessor<TRGT, C2> finalCollectionAccessor = collectionAccessor;
SerializableBiConsumer<TRGT, SRC> combiner = configuredCombiner.getOr((TRGT trgt, SRC src) -> {
// collectionAccessor can't be null due to nullable check
finalCollectionAccessor.get(trgt).add(src);
});
Supplier<C2> effectiveCollectionFactory = reverseCollectionFactory;
result = (TRGT trgt, SRC src) -> {
// we call the collection factory to ensure that property is initialized
if (finalCollectionAccessor.get(trgt) == null) {
finalCollectionAccessor.set(trgt, effectiveCollectionFactory.get());
}
// Note that combiner can't be null here thanks to nullable(..) check
combiner.accept(trgt, src);
};
}
if (mappedByConfiguration instanceof ManyToManyRelation.ShiftedMappedByConfiguration) {
Accessor shifter = ((ShiftedMappedByConfiguration) mappedByConfiguration).getShifter();
return (trgt, src) -> {
SRC src1 = (SRC) shifter.get(src);
result.accept(trgt, src1);
};
} else {
return result;
}
}
}
String configure(ConfiguredRelationalPersister<TRGT, TRGTID> targetPersister, boolean loadSeparately) {
prepare(targetPersister);
String relationJoinNodeName = associationTableEngine.addSelectCascade(associationConfiguration.getSrcPersister(), loadSeparately);
addWriteCascades(associationTableEngine, targetPersister);
return relationJoinNodeName;
}
public CascadeConfigurationResult<SRC,TRGT> configureWithSelectIn2Phases(ConfiguredRelationalPersister<TRGT, TRGTID> targetPersister,
FirstPhaseCycleLoadListener<SRC, TRGTID> firstPhaseCycleLoadListener) {
prepare(targetPersister);
associationTableEngine.addSelectCascadeIn2Phases(firstPhaseCycleLoadListener);
addWriteCascades(associationTableEngine, targetPersister);
return new CascadeConfigurationResult<>(associationTableEngine.getManyRelationDescriptor().getRelationFixer(), associationConfiguration.getSrcPersister());
}
private void addWriteCascades(AbstractOneToManyWithAssociationTableEngine<SRC, TRGT, SRCID, TRGTID, C1, ? extends AssociationRecord, ? extends AssociationTable> oneToManyWithAssociationTableEngine, ConfiguredRelationalPersister<TRGT, TRGTID> targetPersister) {
if (associationConfiguration.isWriteAuthorized()) {
oneToManyWithAssociationTableEngine.addInsertCascade(maintainAssociationOnly, targetPersister);
oneToManyWithAssociationTableEngine.addUpdateCascade(associationConfiguration.isOrphanRemoval(), maintainAssociationOnly, targetPersister);
oneToManyWithAssociationTableEngine.addDeleteCascade(associationConfiguration.isOrphanRemoval(), dialect, targetPersister);
}
}
private <ASSOCIATIONTABLE extends AssociationTable<ASSOCIATIONTABLE, LEFTTABLE, RIGHTTABLE, SRCID, TRGTID>>
void assignEngineForNonIndexedAssociation(
PrimaryKey<RIGHTTABLE, TRGTID> rightPrimaryKey,
String associationTableName,
ConfiguredRelationalPersister<TRGT, TRGTID> targetPersister) {
// we don't create foreign key for table-per-class because source columns should reference different tables (the one
// per entity) which databases do not allow
boolean createOneSideForeignKey = !(associationConfiguration.getManyToManyRelation().isSourceTablePerClassPolymorphic());
boolean createManySideForeignKey = !associationConfiguration.getManyToManyRelation().isTargetTablePerClassPolymorphic();
ReferencedColumnNames<LEFTTABLE, RIGHTTABLE> columnNames = associationTableNamingStrategy.giveColumnNames(
associationConfiguration.getAccessorDefinition(),
associationConfiguration.getLeftPrimaryKey(),
rightPrimaryKey);
if (associationConfiguration.getManyToManyRelation().getSourceJoinColumnName() != null) {
columnNames.setLeftColumnName(Iterables.first(associationConfiguration.getLeftPrimaryKey().getColumns()), associationConfiguration.getManyToManyRelation().getSourceJoinColumnName());
}
if (associationConfiguration.getManyToManyRelation().getSourceJoinColumnName() != null) {
columnNames.setRightColumnName(Iterables.first(rightPrimaryKey.getColumns()), associationConfiguration.getManyToManyRelation().getTargetJoinColumnName());
}
ASSOCIATIONTABLE intermediaryTable = (ASSOCIATIONTABLE) new AssociationTable<ASSOCIATIONTABLE, LEFTTABLE, RIGHTTABLE, SRCID, TRGTID>(
associationConfiguration.getLeftPrimaryKey().getTable().getSchema(),
associationTableName,
associationConfiguration.getLeftPrimaryKey(),
rightPrimaryKey,
columnNames,
associationConfiguration.getForeignKeyNamingStrategy(),
createOneSideForeignKey,
createManySideForeignKey
);
AssociationRecordPersister<AssociationRecord, ASSOCIATIONTABLE> associationPersister = new AssociationRecordPersister<>(
new AssociationRecordMapping<>(intermediaryTable,
associationConfiguration.getSrcPersister().getMapping().getIdMapping().getIdentifierAssembler(),
targetPersister.getMapping().getIdMapping().getIdentifierAssembler()),
dialect,
connectionConfiguration);
ManyRelationDescriptor<SRC, TRGT, C1> manyRelationDescriptor = new ManyRelationDescriptor<>(
associationConfiguration.getCollectionGetter(),
associationConfiguration.getSetter()::set,
associationConfiguration.getCollectionFactory(),
buildReverseCombiner(targetPersister.getClassToPersist()));
associationTableEngine = new OneToManyWithAssociationTableEngine<>(
associationConfiguration.getSrcPersister(),
targetPersister,
manyRelationDescriptor,
associationPersister,
dialect.getWriteOperationFactory());
}
private <ASSOCIATIONTABLE extends IndexedAssociationTable<ASSOCIATIONTABLE, LEFTTABLE, RIGHTTABLE, SRCID, TRGTID>>
void assignEngineForIndexedAssociation(
PrimaryKey<RIGHTTABLE, TRGTID> rightPrimaryKey,
String associationTableName,
ConfiguredRelationalPersister<TRGT, TRGTID> targetPersister) {
ManyToManyRelation<SRC, TRGT, TRGTID, C1, C2> relation = associationConfiguration.getManyToManyRelation();
// we don't create foreign key for table-per-class because source columns should reference different tables (the one
// per entity) which databases do not allow
boolean createOneSideForeignKey = !relation.isSourceTablePerClassPolymorphic();
boolean createManySideForeignKey = !relation.isTargetTablePerClassPolymorphic();
ReferencedColumnNames<LEFTTABLE, RIGHTTABLE> columnNames = associationTableNamingStrategy.giveColumnNames(
associationConfiguration.getAccessorDefinition(),
associationConfiguration.getLeftPrimaryKey(),
rightPrimaryKey);
String indexingColumnName = nullable(relation.getIndexingColumnName()).getOr(() -> associationConfiguration.getIndexColumnNamingStrategy().giveName(associationConfiguration.getAccessorDefinition()));
// NB: index column is part of the primary key
ASSOCIATIONTABLE intermediaryTable = (ASSOCIATIONTABLE) new IndexedAssociationTable<ASSOCIATIONTABLE, LEFTTABLE, RIGHTTABLE, SRCID, TRGTID>(
associationConfiguration.getLeftPrimaryKey().getTable().getSchema(),
associationTableName,
associationConfiguration.getLeftPrimaryKey(),
rightPrimaryKey,
columnNames,
associationConfiguration.getForeignKeyNamingStrategy(),
createOneSideForeignKey,
createManySideForeignKey,
indexingColumnName);
intermediaryTable.addForeignKey(associationConfiguration.getForeignKeyNamingStrategy()::giveName,
intermediaryTable.getOneSideForeignKey(), associationConfiguration.getLeftPrimaryKey());
AssociationRecordPersister<IndexedAssociationRecord, ASSOCIATIONTABLE> indexedAssociationPersister =
new AssociationRecordPersister<>(
new IndexedAssociationRecordMapping<>(intermediaryTable,
associationConfiguration.getSrcPersister().getMapping().getIdMapping().getIdentifierAssembler(),
targetPersister.getMapping().getIdMapping().getIdentifierAssembler(),
intermediaryTable.getLeftIdentifierColumnMapping(),
intermediaryTable.getRightIdentifierColumnMapping()),
dialect,
connectionConfiguration);
IndexedAssociationTableManyRelationDescriptor<SRC, TRGT, C1, SRCID> manyRelationDescriptor = new IndexedAssociationTableManyRelationDescriptor<>(
associationConfiguration.getCollectionGetter(),
associationConfiguration.getSetter()::set,
associationConfiguration.getCollectionFactory(),
buildReverseCombiner(targetPersister.getClassToPersist()),
associationConfiguration.getSrcPersister()::getId
);
associationTableEngine = new OneToManyWithIndexedAssociationTableEngine<>(
associationConfiguration.getSrcPersister(),
targetPersister,
manyRelationDescriptor,
indexedAssociationPersister,
intermediaryTable.getIndexColumn(),
dialect.getWriteOperationFactory());
}
}
}