diff --git a/src/lazy-loading/LazyRelationsWrapper.ts b/src/lazy-loading/LazyRelationsWrapper.ts index bf807d636..3749611a1 100644 --- a/src/lazy-loading/LazyRelationsWrapper.ts +++ b/src/lazy-loading/LazyRelationsWrapper.ts @@ -35,7 +35,7 @@ export class LazyRelationsWrapper { if (relation.isManyToOne || relation.isOneToOneOwner) { - const joinColumns = relation.isOwning ? relation.foreignKey.columns : relation.inverseRelation.foreignKey.columns; + const joinColumns = relation.isOwning ? relation.joinColumns : relation.inverseRelation.joinColumns; const conditions = joinColumns.map(joinColumn => { return `${relation.entityMetadata.name}.${relation.propertyName} = ${relation.propertyName}.${joinColumn.referencedColumn.propertyName}`; }).join(" AND "); @@ -74,7 +74,7 @@ export class LazyRelationsWrapper { qb.select(relation.propertyName) .from(relation.inverseRelation.entityMetadata.target, relation.propertyName); - relation.inverseRelation.foreignKey.columns.forEach(joinColumn => { + relation.inverseRelation.joinColumns.forEach(joinColumn => { qb.andWhere(`${relation.propertyName}.${joinColumn.propertyName} = :${joinColumn.referencedColumn.propertyName}`) .setParameter(`${joinColumn.referencedColumn.propertyName}`, this[joinColumn.referencedColumn.propertyName]); }); diff --git a/src/metadata-builder/EntityMetadataBuilder.ts b/src/metadata-builder/EntityMetadataBuilder.ts index f9f5bae96..1357b5778 100644 --- a/src/metadata-builder/EntityMetadataBuilder.ts +++ b/src/metadata-builder/EntityMetadataBuilder.ts @@ -269,7 +269,7 @@ export class EntityMetadataBuilder { ); foreignKey.entityMetadata = relation.entityMetadata; - relation.foreignKey = foreignKey; + relation.foreignKeys = [foreignKey]; relation.entityMetadata.foreignKeys.push(foreignKey); }); }); diff --git a/src/metadata/EntityMetadata.ts b/src/metadata/EntityMetadata.ts index 862524c09..ef1b247f0 100644 --- a/src/metadata/EntityMetadata.ts +++ b/src/metadata/EntityMetadata.ts @@ -603,12 +603,12 @@ export class EntityMetadata { // if entity id is a relation, then extract referenced column from that relation const columnRelation = this.relations.find(relation => relation.propertyName === column.propertyName); - if (columnRelation && columnRelation.foreignKey.columns.length) { - const ids = columnRelation.foreignKey.columns.map(joinColumn => entityValue[joinColumn.referencedColumn.propertyName]); + if (columnRelation && columnRelation.joinColumns.length) { + const ids = columnRelation.joinColumns.map(joinColumn => entityValue[joinColumn.referencedColumn.propertyName]); map[column.propertyName] = ids.length === 1 ? ids[0] : ids; - } else if (columnRelation && columnRelation.inverseRelation.foreignKey.columns.length) { - const ids = columnRelation.inverseRelation.foreignKey.columns.map(joinColumn => entityValue[joinColumn.referencedColumn.propertyName]); + } else if (columnRelation && columnRelation.inverseRelation.joinColumns.length) { + const ids = columnRelation.inverseRelation.joinColumns.map(joinColumn => entityValue[joinColumn.referencedColumn.propertyName]); map[column.propertyName] = ids.length === 1 ? ids[0] : ids; } else { @@ -626,12 +626,12 @@ export class EntityMetadata { // if entity id is a relation, then extract referenced column from that relation const columnRelation = this.relations.find(relation => relation.propertyName === column.propertyName); - if (columnRelation && columnRelation.foreignKey.columns.length) { - const ids = columnRelation.foreignKey.columns.map(joinColumn => entityValue[joinColumn.referencedColumn.propertyName]); + if (columnRelation && columnRelation.joinColumns.length) { + const ids = columnRelation.joinColumns.map(joinColumn => entityValue[joinColumn.referencedColumn.propertyName]); map[column.propertyName] = ids.length === 1 ? ids[0] : ids; - } else if (columnRelation && columnRelation.inverseRelation.foreignKey.columns.length) { - const ids = columnRelation.inverseRelation.foreignKey.columns.map(joinColumn => entityValue[joinColumn.referencedColumn.propertyName]); + } else if (columnRelation && columnRelation.inverseRelation.joinColumns.length) { + const ids = columnRelation.inverseRelation.joinColumns.map(joinColumn => entityValue[joinColumn.referencedColumn.propertyName]); map[column.propertyName] = ids.length === 1 ? ids[0] : ids; } else { @@ -656,12 +656,12 @@ export class EntityMetadata { // if entity id is a relation, then extract referenced column from that relation const columnRelation = this.relations.find(relation => relation.propertyName === column.propertyName); - if (columnRelation && columnRelation.foreignKey.columns.length) { - const ids = columnRelation.foreignKey.columns.map(joinColumn => entityValue[joinColumn.referencedColumn.propertyName]); + if (columnRelation && columnRelation.joinColumns.length) { + const ids = columnRelation.joinColumns.map(joinColumn => entityValue[joinColumn.referencedColumn.propertyName]); map[column.fullName] = ids.length === 1 ? ids[0] : ids; - } else if (columnRelation && columnRelation.inverseRelation.foreignKey.columns.length) { - const ids = columnRelation.inverseRelation.foreignKey.columns.map(joinColumn => entityValue[joinColumn.referencedColumn.propertyName]); + } else if (columnRelation && columnRelation.inverseRelation.joinColumns.length) { + const ids = columnRelation.inverseRelation.joinColumns.map(joinColumn => entityValue[joinColumn.referencedColumn.propertyName]); map[column.fullName] = ids.length === 1 ? ids[0] : ids; } else { @@ -678,12 +678,12 @@ export class EntityMetadata { // if entity id is a relation, then extract referenced column from that relation const columnRelation = this.relations.find(relation => relation.propertyName === column.propertyName); - if (columnRelation && columnRelation.foreignKey.columns.length) { - const ids = columnRelation.foreignKey.columns.map(joinColumn => entityValue[joinColumn.referencedColumn.propertyName]); + if (columnRelation && columnRelation.joinColumns.length) { + const ids = columnRelation.joinColumns.map(joinColumn => entityValue[joinColumn.referencedColumn.propertyName]); map[column.fullName] = ids.length === 1 ? ids[0] : ids; - } else if (columnRelation && columnRelation.inverseRelation.foreignKey.columns.length) { - const ids = columnRelation.inverseRelation.foreignKey.columns.map(joinColumn => entityValue[joinColumn.referencedColumn.propertyName]); + } else if (columnRelation && columnRelation.inverseRelation.joinColumns.length) { + const ids = columnRelation.inverseRelation.joinColumns.map(joinColumn => entityValue[joinColumn.referencedColumn.propertyName]); map[column.fullName] = ids.length === 1 ? ids[0] : ids; } else { diff --git a/src/metadata/RelationMetadata.ts b/src/metadata/RelationMetadata.ts index 8e87f48ca..c5425ccfa 100644 --- a/src/metadata/RelationMetadata.ts +++ b/src/metadata/RelationMetadata.ts @@ -4,6 +4,7 @@ import {OnDeleteType, ForeignKeyMetadata} from "./ForeignKeyMetadata"; import {JoinTableMetadata} from "./JoinTableMetadata"; import {RelationMetadataArgs} from "../metadata-args/RelationMetadataArgs"; import {ObjectLiteral} from "../common/ObjectLiteral"; +import {ColumnMetadata} from "./ColumnMetadata"; /** * Function that returns a type of the field. Returned value must be a class used on the relation. @@ -46,7 +47,7 @@ export class RelationMetadata { */ joinTable: JoinTableMetadata; - foreignKey: ForeignKeyMetadata; + foreignKeys: ForeignKeyMetadata[] = []; // --------------------------------------------------------------------- // Readonly Properties @@ -195,21 +196,52 @@ export class RelationMetadata { if (this.isOwning) { if (this.joinTable) { return this.joinTable.joinColumns[0].name; - } else if (this.foreignKey && this.foreignKey.columns) { - return this.foreignKey.columns[0].name; + } else if (this.foreignKeys[0] && this.foreignKeys[0].columns) { + return this.foreignKeys[0].columns[0].name; } } else if (this.hasInverseSide) { if (this.inverseRelation.joinTable) { return this.inverseRelation.joinTable.inverseJoinColumns[0].name; - } else if (this.inverseRelation.foreignKey && this.inverseRelation.foreignKey.columns && this.inverseRelation.foreignKey.columns[0].referencedColumn) { - return this.inverseRelation.foreignKey.columns[0].referencedColumn.fullName; // todo: [0] is temporary!! + } else if (this.inverseRelation.foreignKeys[0] && this.inverseRelation.foreignKeys[0].columns && this.inverseRelation.foreignKeys[0].columns[0].referencedColumn) { + return this.inverseRelation.foreignKeys[0].columns[0].referencedColumn.fullName; // todo: [0] is temporary!! } } throw new Error(`Relation name cannot be retrieved.`); } + /** + * Join table name. + */ + get joinTableName(): string { + return this.junctionEntityMetadata.table.name; + } + + + /** + * Join table columns. + */ + get joinColumns(): ColumnMetadata[] { + if (!this.isOwning) + throw new Error(`Inverse join columns are only supported from owning side`); + + return this.foreignKeys[0].columns; + } + + /** + * Join table columns. + */ + get inverseJoinColumns(): ColumnMetadata[] { + if (!this.isOwning) + throw new Error(`Inverse join columns are only supported from owning side`); + + if (!this.isManyToMany) + throw new Error(`Inverse join columns are not supported by non-many-to-many relations`); + + return this.foreignKeys[1].columns; + } + /** * Gets the name of column to which this relation is referenced. * //Cannot be used with many-to-many relations since all referenced are in the junction table. @@ -278,7 +310,7 @@ export class RelationMetadata { get isOwning() { return !!(this.isManyToOne || (this.isManyToMany && this.joinTable) || - (this.isOneToOne && this.foreignKey)); + (this.isOneToOne && this.foreignKeys.length > 0)); } /** diff --git a/src/persistence/SubjectBuilder.ts b/src/persistence/SubjectBuilder.ts index bd0eff05d..42cc1ee75 100644 --- a/src/persistence/SubjectBuilder.ts +++ b/src/persistence/SubjectBuilder.ts @@ -349,7 +349,7 @@ export class SubjectBuilder { if (subject.hasEntity) { const persistValue = relation.getEntityValue(subject.entity); if (persistValue === null) persistValueRelationId = null; - if (persistValue) persistValueRelationId = persistValue[relation.foreignKey.columns[0].referencedColumn.propertyName]; + if (persistValue) persistValueRelationId = persistValue[relation.joinColumns[0].referencedColumn.propertyName]; if (persistValueRelationId === undefined) return; // skip undefined properties } @@ -367,7 +367,7 @@ export class SubjectBuilder { // (example) here we seek a Details loaded from the database in the subjects // (example) here relatedSubject.databaseEntity is a Details // (example) and we need to compare details.id === post.detailsId - return relatedSubject.databaseEntity[relation.foreignKey.columns[0].referencedColumn.propertyName] === relationIdInDatabaseEntity; + return relatedSubject.databaseEntity[relation.joinColumns[0].referencedColumn.propertyName] === relationIdInDatabaseEntity; }); // if not loaded yet then load it from the database @@ -377,7 +377,7 @@ export class SubjectBuilder { const databaseEntity = await this.connection .getRepository(valueMetadata.target) .createQueryBuilder(qbAlias, this.queryRunnerProvider) // todo: this wont work for mongodb. implement this in some method and call it here instead? - .where(qbAlias + "." + relation.foreignKey.columns[0].referencedColumn.propertyName + "=:id") + .where(qbAlias + "." + relation.joinColumns[0].referencedColumn.propertyName + "=:id") .setParameter("id", relationIdInDatabaseEntity) // (example) subject.entity is a post here .enableAutoRelationIdsLoad() .getOne(); @@ -428,7 +428,7 @@ export class SubjectBuilder { } // (example) returns us referenced column (detail's id) - const relationIdInDatabaseEntity = subject.databaseEntity[relation.inverseRelation.foreignKey.columns[0].referencedColumn.propertyName]; + const relationIdInDatabaseEntity = subject.databaseEntity[relation.inverseRelation.joinColumns[0].referencedColumn.propertyName]; // if database relation id does not exist then nothing to remove (but can this be possible?) if (relationIdInDatabaseEntity === null || relationIdInDatabaseEntity === undefined) @@ -594,7 +594,7 @@ export class SubjectBuilder { } else { // this case can only be a oneToMany relation // todo: fix issues with joinColumn[0] // (example) returns us referenced column (detail's id) - const relationIdInDatabaseEntity = subject.databaseEntity[relation.inverseRelation.foreignKey.columns[0].referencedColumn.propertyName]; + const relationIdInDatabaseEntity = subject.databaseEntity[relation.inverseRelation.joinColumns[0].referencedColumn.propertyName]; // in this case we need inverse entities not only because of cascade removes // because we also need inverse entities to be able to perform update of entities diff --git a/src/persistence/SubjectOperationExecutor.ts b/src/persistence/SubjectOperationExecutor.ts index 801a5eaed..f8a63f3ff 100644 --- a/src/persistence/SubjectOperationExecutor.ts +++ b/src/persistence/SubjectOperationExecutor.ts @@ -206,7 +206,7 @@ export class SubjectOperationExecutor { // first update relations with join columns (one-to-one owner and many-to-one relations) const updateOptions: ObjectLiteral = {}; subject.metadata.relationsWithJoinColumns.forEach(relation => { - relation.foreignKey.columns.forEach(joinColumn => { + relation.joinColumns.forEach(joinColumn => { const referencedColumn = joinColumn.referencedColumn; const relatedEntity = relation.getEntityValue(subject.entity); @@ -283,7 +283,7 @@ export class SubjectOperationExecutor { const columnRelation = subject.metadata.relations.find(relation => relation.propertyName === column.propertyName); if (entityValue && columnRelation) { // not sure if we need handle join column from inverse side - columnRelation.foreignKey.columns.forEach(joinColumn => { + columnRelation.joinColumns.forEach(joinColumn => { let relationIdOfEntityValue = entityValue[joinColumn.referencedColumn.propertyName]; if (!relationIdOfEntityValue) { const entityValueInsertSubject = this.insertSubjects.find(subject => subject.entity === entityValue); @@ -327,7 +327,7 @@ export class SubjectOperationExecutor { const oneToManyAndOneToOneNonOwnerRelations = subject.metadata.oneToManyRelations.concat(subject.metadata.oneToOneRelations.filter(relation => !relation.isOwning)); subject.metadata.extractRelationValuesFromEntity(subject.entity, oneToManyAndOneToOneNonOwnerRelations) .forEach(([relation, subRelatedEntity, inverseEntityMetadata]) => { - relation.inverseRelation.foreignKey.columns.forEach(joinColumn => { + relation.inverseRelation.joinColumns.forEach(joinColumn => { const referencedColumn = joinColumn.referencedColumn; const columns = inverseEntityMetadata.parentEntityMetadata ? inverseEntityMetadata.primaryColumnsWithParentIdColumns : inverseEntityMetadata.primaryColumns; @@ -340,7 +340,7 @@ export class SubjectOperationExecutor { const columnRelation = inverseEntityMetadata.relations.find(relation => relation.propertyName === column.propertyName); if (entityValue && columnRelation) { // not sure if we need handle join column from inverse side - columnRelation.foreignKey.columns.forEach(columnRelationJoinColumn => { + columnRelation.joinColumns.forEach(columnRelationJoinColumn => { let relationIdOfEntityValue = entityValue[columnRelationJoinColumn.referencedColumn.propertyName]; if (!relationIdOfEntityValue) { const entityValueInsertSubject = this.insertSubjects.find(subject => subject.entity === entityValue); @@ -507,7 +507,7 @@ export class SubjectOperationExecutor { collectFromEmbeddeds(entity, columnsAndValuesMap, metadata.embeddeds); metadata.relationsWithJoinColumns.forEach(relation => { - relation.foreignKey.columns.forEach(joinColumn => { + relation.joinColumns.forEach(joinColumn => { let relationValue: any; const value = relation.getEntityValue(entity); @@ -653,7 +653,7 @@ export class SubjectOperationExecutor { // todo: since closure tables do not support compose primary keys - throw an exception? // todo: what if parent entity or parentEntityId is empty?! const tableName = subject.metadata.closureJunctionTable.table.name; - const referencedColumn = subject.metadata.treeParentRelation.foreignKey.columns[0].referencedColumn; // todo: check if joinColumn works + const referencedColumn = subject.metadata.treeParentRelation.joinColumns[0].referencedColumn; // todo: check if joinColumn works // todo: fix joinColumns[0] usage let newEntityId = subject.entity[referencedColumn.propertyName]; @@ -792,7 +792,7 @@ export class SubjectOperationExecutor { } const value = relation.getEntityValue(entity); - relation.foreignKey.columns.forEach(joinColumn => { + relation.joinColumns.forEach(joinColumn => { valueMap!.values[joinColumn.name] = value !== null && value !== undefined ? value[joinColumn.referencedColumn.propertyName] : null; // todo: should not have a call to primaryColumn, instead join column metadata should be used }); }); @@ -877,7 +877,7 @@ export class SubjectOperationExecutor { private async updateRelations(subject: Subject) { const values: ObjectLiteral = {}; subject.relationUpdates.forEach(setRelation => { - setRelation.relation.foreignKey.columns.forEach(joinColumn => { + setRelation.relation.joinColumns.forEach(joinColumn => { const value = setRelation.value ? setRelation.value[joinColumn.referencedColumn.propertyName] : null; values[joinColumn.fullName] = value; // todo: || fromInsertedSubjects ?? }); diff --git a/src/query-builder/QueryBuilder.ts b/src/query-builder/QueryBuilder.ts index 410802b0a..da14b655d 100644 --- a/src/query-builder/QueryBuilder.ts +++ b/src/query-builder/QueryBuilder.ts @@ -1454,7 +1454,7 @@ export class QueryBuilder { if (relation.isManyToOne || relation.isOneToOneOwner) { // JOIN `category` `category` ON `category`.`id` = `post`.`categoryId` - const condition = relation.foreignKey.columns.map(joinColumn => { + const condition = relation.joinColumns.map(joinColumn => { return ea(destinationTableAlias) + "." + ec(joinColumn.referencedColumn.fullName) + "=" + ea(parentAlias) + "." + ec(joinColumn.propertyName); }).join(" AND "); @@ -1463,7 +1463,7 @@ export class QueryBuilder { } else if (relation.isOneToMany || relation.isOneToOneNotOwner) { // JOIN `post` `post` ON `post`.`categoryId` = `category`.`id` - const condition = relation.inverseRelation.foreignKey.columns.map(joinColumn => { + const condition = relation.inverseRelation.joinColumns.map(joinColumn => { return ea(destinationTableAlias!) + "." + ec(joinColumn.propertyName) + "=" + ea(parentAlias) + "." + ec(joinColumn.referencedColumn.fullName); }).join(" AND "); diff --git a/src/query-builder/relation-count/RelationCountLoader.ts b/src/query-builder/relation-count/RelationCountLoader.ts index 1ab0b1ec8..6a6fc79d3 100644 --- a/src/query-builder/relation-count/RelationCountLoader.ts +++ b/src/query-builder/relation-count/RelationCountLoader.ts @@ -32,7 +32,7 @@ export class RelationCountLoader { // todo(dima): fix issues wit multiple primary keys and remove joinColumns[0] const relation = relationCountAttr.relation; // "category.posts" const inverseRelation = relation.inverseRelation; // "post.category" - const referenceColumnName = inverseRelation.foreignKey.columns[0].referencedColumn.propertyName; // post id + const referenceColumnName = inverseRelation.joinColumns[0].referencedColumn.propertyName; // post id const inverseSideTable = relation.inverseEntityMetadata.table.target; // Post const inverseSideTableName = relation.inverseEntityMetadata.table.name; // post const inverseSideTableAlias = relationCountAttr.alias || inverseSideTableName; // if condition (custom query builder factory) is set then relationIdAttr.alias defined diff --git a/src/query-builder/relation-id/RelationIdLoader.ts b/src/query-builder/relation-id/RelationIdLoader.ts index 633f868a8..d26b1160d 100644 --- a/src/query-builder/relation-id/RelationIdLoader.ts +++ b/src/query-builder/relation-id/RelationIdLoader.ts @@ -55,7 +55,7 @@ export class RelationIdLoader { const relation = relationIdAttr.relation; // "category.posts" const inverseRelation = relation.inverseRelation; // "post.category" - const referenceColumnName = inverseRelation.foreignKey.columns[0].referencedColumn.propertyName; // post id + const referenceColumnName = inverseRelation.joinColumns[0].referencedColumn.propertyName; // post id const inverseSideTable = relation.inverseEntityMetadata.table.target; // Post const inverseSideTableName = relation.inverseEntityMetadata.table.name; // post const inverseSideTableAlias = relationIdAttr.alias || inverseSideTableName; // if condition (custom query builder factory) is set then relationIdAttr.alias defined diff --git a/src/query-builder/transformer/RawSqlResultsToEntityTransformer.ts b/src/query-builder/transformer/RawSqlResultsToEntityTransformer.ts index 576b0b175..a39d9158b 100644 --- a/src/query-builder/transformer/RawSqlResultsToEntityTransformer.ts +++ b/src/query-builder/transformer/RawSqlResultsToEntityTransformer.ts @@ -171,7 +171,7 @@ export class RawSqlResultsToEntityTransformer { } else { let referenceColumnName: string; if (relation.isOneToMany || relation.isOneToOneNotOwner) { // todo: fix joinColumns[0] - referenceColumnName = relation.inverseRelation.foreignKey.columns[0].referencedColumn.fullName; + referenceColumnName = relation.inverseRelation.joinColumns[0].referencedColumn.fullName; } else { referenceColumnName = relation.isOwning ? relation.joinTable.joinColumns[0].referencedColumn.fullName : relation.inverseRelation.joinTable.joinColumns[0].referencedColumn.fullName; } @@ -202,7 +202,7 @@ export class RawSqlResultsToEntityTransformer { let referenceColumnName: string; if (relation.isOneToMany) { - referenceColumnName = relation.inverseRelation.foreignKey.columns[0].referencedColumn.fullName; // todo: fix joinColumns[0] + referenceColumnName = relation.inverseRelation.joinColumns[0].referencedColumn.fullName; // todo: fix joinColumns[0] } else { referenceColumnName = relation.isOwning ? relation.joinTable.joinColumns[0].referencedColumn.fullName : relation.inverseRelation.joinTable.joinColumns[0].referencedColumn.fullName; diff --git a/src/repository/SpecificRepository.ts b/src/repository/SpecificRepository.ts index 8d0829878..e2f3ce72f 100644 --- a/src/repository/SpecificRepository.ts +++ b/src/repository/SpecificRepository.ts @@ -63,11 +63,11 @@ export class SpecificRepository { if (relation.isOwning) { table = relation.entityMetadata.table.name; values[relation.name] = relatedEntityId; - conditions[relation.foreignKey.columns[0].referencedColumn.fullName] = entityId; + conditions[relation.joinColumns[0].referencedColumn.fullName] = entityId; } else { table = relation.inverseEntityMetadata.table.name; values[relation.inverseRelation.name] = relatedEntityId; - conditions[relation.inverseRelation.foreignKey.columns[0].referencedColumn.fullName] = entityId; + conditions[relation.inverseRelation.joinColumns[0].referencedColumn.fullName] = entityId; } @@ -114,11 +114,11 @@ export class SpecificRepository { if (relation.isOwning) { table = relation.inverseEntityMetadata.table.name; values[relation.inverseRelation.name] = relatedEntityId; - conditions[relation.inverseRelation.foreignKey.columns[0].referencedColumn.fullName] = entityId; + conditions[relation.inverseRelation.joinColumns[0].referencedColumn.fullName] = entityId; } else { table = relation.entityMetadata.table.name; values[relation.name] = relatedEntityId; - conditions[relation.foreignKey.columns[0].referencedColumn.fullName] = entityId; + conditions[relation.joinColumns[0].referencedColumn.fullName] = entityId; } const queryRunnerProvider = this.queryRunnerProvider ? this.queryRunnerProvider : new QueryRunnerProvider(this.connection.driver);