mirror of
https://github.com/typeorm/typeorm.git
synced 2025-12-08 21:26:23 +00:00
fixes #58
This commit is contained in:
parent
543f5c3b75
commit
c93d77c36e
@ -11,6 +11,7 @@ import {EmbeddedMetadata} from "../metadata/EmbeddedMetadata";
|
||||
*/
|
||||
export interface EntityMetadataArgs {
|
||||
|
||||
readonly junction: boolean;
|
||||
readonly target: Function|string;
|
||||
readonly tablesPrefix?: string;
|
||||
readonly inheritanceType?: "single-table"|"class-table";
|
||||
|
||||
@ -71,6 +71,7 @@ export class ClosureJunctionEntityMetadataBuilder {
|
||||
});
|
||||
|
||||
return new EntityMetadata({
|
||||
junction: true,
|
||||
target: "__virtual__",
|
||||
tablesPrefix: driver.options.tablesPrefix,
|
||||
namingStrategy: args.namingStrategy,
|
||||
|
||||
@ -228,6 +228,7 @@ export class EntityMetadataBuilder {
|
||||
});
|
||||
// create a new entity metadata
|
||||
const entityMetadata = new EntityMetadata({
|
||||
junction: false,
|
||||
target: tableArgs.target,
|
||||
tablesPrefix: driver.options.tablesPrefix,
|
||||
namingStrategy: namingStrategy,
|
||||
@ -462,9 +463,6 @@ export class EntityMetadataBuilder {
|
||||
// check for errors in a built metadata schema (we need to check after relationEntityMetadata is set)
|
||||
getFromContainer(EntityMetadataValidator).validateMany(entityMetadatas);
|
||||
|
||||
// check for errors in a built metadata schema (we need to check after relationEntityMetadata is set)
|
||||
getFromContainer(EntityMetadataValidator).validateDependencies(entityMetadatas);
|
||||
|
||||
return entityMetadatas;
|
||||
}
|
||||
|
||||
|
||||
@ -25,31 +25,7 @@ export class EntityMetadataValidator {
|
||||
*/
|
||||
validateMany(entityMetadatas: EntityMetadata[]) {
|
||||
entityMetadatas.forEach(entityMetadata => this.validate(entityMetadata, entityMetadatas));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates dependencies of the entity metadatas.
|
||||
*/
|
||||
validateDependencies(entityMetadatas: EntityMetadata[]) {
|
||||
|
||||
const DepGraph = require("dependency-graph").DepGraph;
|
||||
const graph = new DepGraph();
|
||||
entityMetadatas.forEach(entityMetadata => {
|
||||
graph.addNode(entityMetadata.name);
|
||||
});
|
||||
entityMetadatas.forEach(entityMetadata => {
|
||||
entityMetadata.relationsWithJoinColumns
|
||||
.filter(relation => !relation.isNullable)
|
||||
.forEach(relation => {
|
||||
graph.addDependency(entityMetadata.name, relation.inverseEntityMetadata.name);
|
||||
});
|
||||
});
|
||||
try {
|
||||
graph.overallOrder();
|
||||
|
||||
} catch (err) {
|
||||
throw new CircularRelationsError(err.toString().replace("Error: Dependency Cycle Found: ", ""));
|
||||
}
|
||||
this.validateDependencies(entityMetadatas);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -58,7 +34,7 @@ export class EntityMetadataValidator {
|
||||
validate(entityMetadata: EntityMetadata, allEntityMetadatas: EntityMetadata[]) {
|
||||
|
||||
// check if table metadata has an id
|
||||
if (!entityMetadata.table.isClassTableChild && !entityMetadata.primaryColumns.length)
|
||||
if (!entityMetadata.table.isClassTableChild && !entityMetadata.primaryColumns.length && !entityMetadata.junction)
|
||||
throw new MissingPrimaryColumnError(entityMetadata);
|
||||
|
||||
// validate if table is using inheritance it has a discriminator
|
||||
@ -130,4 +106,30 @@ export class EntityMetadataValidator {
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates dependencies of the entity metadatas.
|
||||
*/
|
||||
protected validateDependencies(entityMetadatas: EntityMetadata[]) {
|
||||
|
||||
const DepGraph = require("dependency-graph").DepGraph;
|
||||
const graph = new DepGraph();
|
||||
entityMetadatas.forEach(entityMetadata => {
|
||||
graph.addNode(entityMetadata.name);
|
||||
});
|
||||
entityMetadatas.forEach(entityMetadata => {
|
||||
entityMetadata.relationsWithJoinColumns
|
||||
.filter(relation => !relation.isNullable)
|
||||
.forEach(relation => {
|
||||
graph.addDependency(entityMetadata.name, relation.inverseEntityMetadata.name);
|
||||
});
|
||||
});
|
||||
try {
|
||||
graph.overallOrder();
|
||||
|
||||
} catch (err) {
|
||||
throw new CircularRelationsError(err.toString().replace("Error: Dependency Cycle Found: ", ""));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -63,6 +63,7 @@ export class JunctionEntityMetadataBuilder {
|
||||
});
|
||||
|
||||
const entityMetadata = new EntityMetadata({
|
||||
junction: true,
|
||||
target: "__virtual__",
|
||||
tablesPrefix: driver.options.tablesPrefix,
|
||||
namingStrategy: args.namingStrategy,
|
||||
|
||||
@ -46,6 +46,11 @@ export class EntityMetadata {
|
||||
*/
|
||||
readonly target: Function|string;
|
||||
|
||||
/**
|
||||
* Indicates if this entity metadata of a junction table, or not.
|
||||
*/
|
||||
readonly junction: boolean;
|
||||
|
||||
/**
|
||||
* Entity's table metadata.
|
||||
*/
|
||||
@ -104,6 +109,7 @@ export class EntityMetadata {
|
||||
constructor(args: EntityMetadataArgs,
|
||||
private lazyRelationsWrapper: LazyRelationsWrapper) {
|
||||
this.target = args.target;
|
||||
this.junction = args.junction;
|
||||
this.tablesPrefix = args.tablesPrefix;
|
||||
this.namingStrategy = args.namingStrategy;
|
||||
this.table = args.tableMetadata;
|
||||
@ -547,18 +553,41 @@ export class EntityMetadata {
|
||||
const map: ObjectLiteral = {};
|
||||
if (this.parentEntityMetadata) {
|
||||
this.primaryColumnsWithParentIdColumns.forEach(column => {
|
||||
map[column.propertyName] = entity[column.propertyName];
|
||||
const entityValue = entity[column.propertyName];
|
||||
if (entityValue === null || entityValue === undefined)
|
||||
return;
|
||||
|
||||
// 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.joinColumn) {
|
||||
map[column.name] = entityValue[columnRelation.joinColumn.referencedColumn.propertyName];
|
||||
} else if (columnRelation && columnRelation.inverseRelation.joinColumn) {
|
||||
map[column.name] = entityValue[columnRelation.inverseRelation.joinColumn.referencedColumn.propertyName];
|
||||
} else {
|
||||
map[column.name] = entityValue;
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
this.primaryColumns.forEach(column => {
|
||||
map[column.propertyName] = entity[column.propertyName];
|
||||
const entityValue = entity[column.propertyName];
|
||||
if (entityValue === null || entityValue === undefined)
|
||||
return;
|
||||
|
||||
// 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.joinColumn) {
|
||||
map[column.name] = entityValue[columnRelation.joinColumn.referencedColumn.propertyName];
|
||||
} else if (columnRelation && columnRelation.inverseRelation.joinColumn) {
|
||||
map[column.name] = entityValue[columnRelation.inverseRelation.joinColumn.referencedColumn.propertyName];
|
||||
} else {
|
||||
map[column.name] = entityValue;
|
||||
}
|
||||
});
|
||||
}
|
||||
const hasAllIds = this.primaryColumns.every(primaryColumn => {
|
||||
return map[primaryColumn.propertyName] !== undefined && map[primaryColumn.propertyName] !== null;
|
||||
});
|
||||
return hasAllIds ? map : undefined;
|
||||
return Object.keys(map).length > 0 ? map : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -568,22 +597,48 @@ export class EntityMetadata {
|
||||
const map: ObjectLiteral = {};
|
||||
if (this.parentEntityMetadata) {
|
||||
this.primaryColumnsWithParentIdColumns.forEach(column => {
|
||||
map[column.name] = entity[column.propertyName];
|
||||
const entityValue = entity[column.propertyName];
|
||||
if (entityValue === null || entityValue === undefined)
|
||||
return;
|
||||
|
||||
// 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.joinColumn) {
|
||||
map[column.name] = entityValue[columnRelation.joinColumn.referencedColumn.propertyName];
|
||||
} else if (columnRelation && columnRelation.inverseRelation.joinColumn) {
|
||||
map[column.name] = entityValue[columnRelation.inverseRelation.joinColumn.referencedColumn.propertyName];
|
||||
} else {
|
||||
map[column.name] = entityValue;
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
this.primaryColumns.forEach(column => {
|
||||
map[column.name] = entity[column.propertyName];
|
||||
const entityValue = entity[column.propertyName];
|
||||
if (entityValue === null || entityValue === undefined)
|
||||
return;
|
||||
|
||||
// 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.joinColumn) {
|
||||
map[column.name] = entityValue[columnRelation.joinColumn.referencedColumn.propertyName];
|
||||
} else if (columnRelation && columnRelation.inverseRelation.joinColumn) {
|
||||
map[column.name] = entityValue[columnRelation.inverseRelation.joinColumn.referencedColumn.propertyName];
|
||||
} else {
|
||||
map[column.name] = entityValue;
|
||||
}
|
||||
});
|
||||
}
|
||||
const hasAllIds = this.primaryColumns.every(primaryColumn => {
|
||||
return map[primaryColumn.name] !== undefined && map[primaryColumn.name] !== null;
|
||||
const hasAllIds = Object.keys(map).every(key => {
|
||||
return map[key] !== undefined && map[key] !== null;
|
||||
});
|
||||
return hasAllIds ? map : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
|
||||
createSimpleIdMap(id: any): ObjectLiteral {
|
||||
const map: ObjectLiteral = {};
|
||||
if (this.parentEntityMetadata) {
|
||||
@ -597,11 +652,11 @@ export class EntityMetadata {
|
||||
});
|
||||
}
|
||||
return map;
|
||||
}
|
||||
} */
|
||||
|
||||
/**
|
||||
* Same as createSimpleIdMap, but instead of id column property names it returns database column names.
|
||||
*/
|
||||
|
||||
createSimpleDatabaseIdMap(id: any): ObjectLiteral {
|
||||
const map: ObjectLiteral = {};
|
||||
if (this.parentEntityMetadata) {
|
||||
@ -615,7 +670,7 @@ export class EntityMetadata {
|
||||
});
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}*/
|
||||
|
||||
/**
|
||||
* todo: undefined entities should not go there??
|
||||
@ -830,8 +885,8 @@ export class EntityMetadata {
|
||||
* Checks if there any non-nullable column exist in this entity.
|
||||
*/
|
||||
get hasNonNullableColumns(): boolean {
|
||||
return this.relationsWithJoinColumns.length === 0 ||
|
||||
this.relationsWithJoinColumns.every(relation => relation.isNullable);
|
||||
return this.relationsWithJoinColumns.some(relation => !relation.isNullable || relation.isPrimary);
|
||||
// return this.relationsWithJoinColumns.some(relation => relation.isNullable || relation.isPrimary);
|
||||
}
|
||||
|
||||
}
|
||||
@ -253,7 +253,7 @@ export class Subject {
|
||||
* If entity is not set then it returns undefined.
|
||||
* If entity itself has an id then it simply returns it.
|
||||
* If entity does not have an id then it returns newly generated id.
|
||||
*/
|
||||
|
||||
get getPersistedEntityIdMap(): any|undefined {
|
||||
if (!this.hasEntity)
|
||||
return undefined;
|
||||
@ -266,7 +266,7 @@ export class Subject {
|
||||
return this.metadata.createSimpleDatabaseIdMap(this.newlyGeneratedId);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}*/
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public Methods
|
||||
|
||||
@ -227,7 +227,12 @@ export class SubjectBuilder<Entity extends ObjectLiteral> {
|
||||
const allIds = subjectGroup.subjects
|
||||
.filter(subject => !subject.hasDatabaseEntity) // we don't load if subject already has a database entity loaded
|
||||
.map(subject => subject.metadata.getEntityIdMixedMap(subject.entity)) // we only need entity id
|
||||
.filter(mixedId => mixedId !== undefined && mixedId !== null && mixedId !== ""); // we don't need empty ids
|
||||
.filter(mixedId => { // we don't need empty ids
|
||||
if (mixedId instanceof Object)
|
||||
return Object.keys(mixedId).every(key => mixedId[key] !== undefined && mixedId[key] !== null && mixedId[key] !== "");
|
||||
|
||||
return mixedId !== undefined && mixedId !== null && mixedId !== "";
|
||||
});
|
||||
|
||||
// if there no ids found (which means all entities are new and have generated ids) - then nothing to load there
|
||||
if (!allIds.length)
|
||||
|
||||
@ -146,14 +146,16 @@ export class SubjectOperationExecutor {
|
||||
|
||||
// separate insert entities into groups:
|
||||
|
||||
// TODO: current ordering mechanism is bad. need to create a correct order in which entities should be persisted, need to build a dependency graph
|
||||
|
||||
// first group of subjects are subjects without any non-nullable column
|
||||
// we need to insert first such entities because second group entities may rely on those entities.
|
||||
const firstInsertSubjects = this.insertSubjects.filter(subject => subject.metadata.hasNonNullableColumns);
|
||||
const firstInsertSubjects = this.insertSubjects.filter(subject => !subject.metadata.hasNonNullableColumns);
|
||||
|
||||
// second group - are all other subjects
|
||||
// since in this group there are non nullable columns, some of them may depend on value of the
|
||||
// previously inserted entity (which only can be entity with all nullable columns)
|
||||
const secondInsertSubjects = this.insertSubjects.filter(subject => !subject.metadata.hasNonNullableColumns);
|
||||
const secondInsertSubjects = this.insertSubjects.filter(subject => subject.metadata.hasNonNullableColumns);
|
||||
|
||||
// note: these operations should be executed in sequence, not in parallel
|
||||
// because second group depend of obtained data from the first group
|
||||
@ -168,7 +170,7 @@ export class SubjectOperationExecutor {
|
||||
// but category in post is inserted with "null".
|
||||
// now we need to update post table - set category with a newly persisted category id.
|
||||
const updatePromises: Promise<any>[] = [];
|
||||
this.insertSubjects.forEach(subject => {
|
||||
firstInsertSubjects.forEach(subject => {
|
||||
|
||||
// first update relations with join columns (one-to-one owner and many-to-one relations)
|
||||
const updateOptions: ObjectLiteral = {};
|
||||
@ -180,81 +182,153 @@ export class SubjectOperationExecutor {
|
||||
if (!relatedEntity)
|
||||
return;
|
||||
|
||||
const insertSubject = this.insertSubjects.find(insertedSubject => relatedEntity === insertedSubject.entity);
|
||||
// check if relation reference column is a relation
|
||||
let relationId: any;
|
||||
const columnRelation = relation.inverseEntityMetadata.relations.find(rel => rel.propertyName === relation.joinColumn.referencedColumn.propertyName);
|
||||
if (columnRelation) { // if referenced column is a relation
|
||||
const insertSubject = this.insertSubjects.find(insertedSubject => insertedSubject.entity === relatedEntity[referencedColumn.propertyName]);
|
||||
|
||||
// if relation id exist exist in the related entity then simply use it
|
||||
const relationId = relatedEntity[referencedColumn.propertyName]; // todo: what about relationId got from relation column, not relation itself? todo: create relation.getEntityRelationId(entity)
|
||||
// if this relation was just inserted
|
||||
if (insertSubject) {
|
||||
|
||||
// otherwise check if relation id was just now inserted and we can use its generated values
|
||||
if (insertSubject) {
|
||||
if (relationId) {
|
||||
updateOptions[relation.name] = relationId;
|
||||
// check if we have this relation id already
|
||||
relationId = relatedEntity[referencedColumn.propertyName][columnRelation.propertyName];
|
||||
if (!relationId) {
|
||||
|
||||
} else if (referencedColumn.isGenerated) {
|
||||
updateOptions[relation.name] = insertSubject.newlyGeneratedId;
|
||||
// if we don't have relation id then use special values
|
||||
if (referencedColumn.isGenerated) {
|
||||
relationId = insertSubject.newlyGeneratedId;
|
||||
}
|
||||
// todo: handle other special types too
|
||||
}
|
||||
}
|
||||
// todo: handle other special types too
|
||||
|
||||
} else { // if referenced column is a simple non relational column
|
||||
const insertSubject = this.insertSubjects.find(insertedSubject => insertedSubject.entity === relatedEntity);
|
||||
|
||||
// if this relation was just inserted
|
||||
if (insertSubject) {
|
||||
|
||||
// check if we have this relation id already
|
||||
relationId = relatedEntity[referencedColumn.propertyName];
|
||||
if (!relationId) {
|
||||
|
||||
// if we don't have relation id then use special values
|
||||
if (referencedColumn.isGenerated) {
|
||||
relationId = insertSubject.newlyGeneratedId;
|
||||
}
|
||||
// todo: handle other special types too
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (relationId) {
|
||||
updateOptions[relation.name] = relationId;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// if we found relations which we can update - then update them
|
||||
if (Object.keys(updateOptions).length > 0) {
|
||||
const relatedEntityIdMap = subject.getPersistedEntityIdMap;
|
||||
const updatePromise = this.queryRunner.update(subject.metadata.table.name, updateOptions, relatedEntityIdMap);
|
||||
if (Object.keys(updateOptions).length > 0 /*&& subject.hasEntity*/) {
|
||||
// const relatedEntityIdMap = subject.getPersistedEntityIdMap; // todo: this works incorrectly
|
||||
|
||||
const columns = subject.metadata.parentEntityMetadata ? subject.metadata.primaryColumnsWithParentIdColumns : subject.metadata.primaryColumns;
|
||||
const conditions: ObjectLiteral = {};
|
||||
|
||||
columns.forEach(column => {
|
||||
const entityValue = subject.entity[column.propertyName];
|
||||
|
||||
// if entity id is a relation, then extract referenced column from that relation
|
||||
const columnRelation = subject.metadata.relations.find(relation => relation.propertyName === column.propertyName);
|
||||
|
||||
if (entityValue && columnRelation && columnRelation.joinColumn) { // not sure if we need handle join column from inverse side
|
||||
let relationIdOfEntityValue = entityValue[columnRelation.joinColumn.referencedColumn.propertyName];
|
||||
if (!relationIdOfEntityValue) {
|
||||
const entityValueInsertSubject = this.insertSubjects.find(subject => subject.entity === entityValue);
|
||||
if (entityValueInsertSubject && columnRelation.joinColumn.referencedColumn.isGenerated) {
|
||||
relationIdOfEntityValue = entityValueInsertSubject.newlyGeneratedId;
|
||||
}
|
||||
}
|
||||
if (relationIdOfEntityValue) {
|
||||
conditions[column.name] = relationIdOfEntityValue;
|
||||
}
|
||||
|
||||
} else {
|
||||
if (entityValue) {
|
||||
conditions[column.name] = entityValue;
|
||||
} else {
|
||||
if (subject.newlyGeneratedId) {
|
||||
conditions[column.name] = subject.newlyGeneratedId;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!Object.keys(conditions).length)
|
||||
return;
|
||||
|
||||
const updatePromise = this.queryRunner.update(subject.metadata.table.name, updateOptions, conditions);
|
||||
updatePromises.push(updatePromise);
|
||||
}
|
||||
|
||||
// we need to update relation ids if newly inserted objects are used from inverse side in one-to-many inverse relation
|
||||
subject.metadata.oneToManyRelations.forEach(relation => {
|
||||
const referencedColumn = relation.inverseRelation.joinColumn.referencedColumn;
|
||||
const relatedEntity = subject.entity[relation.propertyName];
|
||||
// we also need to update relation ids if newly inserted objects are used from inverse side in one-to-one inverse relation
|
||||
const oneToManyAndOneToOneNonOwnerRelations = subject.metadata.oneToManyRelations.concat(subject.metadata.oneToOneRelations.filter(relation => !relation.isOwning));
|
||||
subject.metadata.extractRelationValuesFromEntity(subject.entity, oneToManyAndOneToOneNonOwnerRelations)
|
||||
.forEach(([relation, subRelatedEntity, inverseEntityMetadata]) => {
|
||||
const referencedColumn = relation.inverseRelation.joinColumn.referencedColumn;
|
||||
const columns = inverseEntityMetadata.parentEntityMetadata ? inverseEntityMetadata.primaryColumnsWithParentIdColumns : inverseEntityMetadata.primaryColumns;
|
||||
const conditions: ObjectLiteral = {};
|
||||
|
||||
// if related entity is not an array then no need to proceed
|
||||
if (!(relatedEntity instanceof Array))
|
||||
return;
|
||||
columns.forEach(column => {
|
||||
const entityValue = subRelatedEntity[column.propertyName];
|
||||
|
||||
relatedEntity.forEach(subRelatedEntity => {
|
||||
// if entity id is a relation, then extract referenced column from that relation
|
||||
const columnRelation = inverseEntityMetadata.relations.find(relation => relation.propertyName === column.propertyName);
|
||||
|
||||
let relationId = subRelatedEntity[referencedColumn.propertyName];
|
||||
const insertSubject = this.insertSubjects.find(insertedSubject => subRelatedEntity === insertedSubject.entity);
|
||||
if (entityValue && columnRelation && columnRelation.joinColumn) { // not sure if we need handle join column from inverse side
|
||||
let relationIdOfEntityValue = entityValue[columnRelation.joinColumn.referencedColumn.propertyName];
|
||||
if (!relationIdOfEntityValue) {
|
||||
const entityValueInsertSubject = this.insertSubjects.find(subject => subject.entity === entityValue);
|
||||
if (entityValueInsertSubject && columnRelation.joinColumn.referencedColumn.isGenerated) {
|
||||
relationIdOfEntityValue = entityValueInsertSubject.newlyGeneratedId;
|
||||
}
|
||||
}
|
||||
if (relationIdOfEntityValue) {
|
||||
conditions[column.name] = relationIdOfEntityValue;
|
||||
}
|
||||
|
||||
if (insertSubject) {
|
||||
} else {
|
||||
const entityValueInsertSubject = this.insertSubjects.find(subject => subject.entity === subRelatedEntity);
|
||||
if (entityValue) {
|
||||
conditions[column.name] = entityValue;
|
||||
} else {
|
||||
if (entityValueInsertSubject && entityValueInsertSubject.newlyGeneratedId) {
|
||||
conditions[column.name] = entityValueInsertSubject.newlyGeneratedId;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!Object.keys(conditions).length)
|
||||
return;
|
||||
|
||||
if (!relationId && referencedColumn.isGenerated)
|
||||
relationId = insertSubject.newlyGeneratedId;
|
||||
|
||||
// todo: implement other special referenced column types (update date, create date, version, discriminator column, etc.)
|
||||
const updateOptions: ObjectLiteral = { };
|
||||
const columnRelation = relation.inverseEntityMetadata.relations.find(rel => rel.propertyName === referencedColumn.propertyName);
|
||||
if (columnRelation) {
|
||||
let id = subject.entity[referencedColumn.propertyName][columnRelation.propertyName];
|
||||
if (!id) {
|
||||
const insertSubject = this.insertSubjects.find(subject => subject.entity === subject.entity[referencedColumn.propertyName]);
|
||||
if (insertSubject) {
|
||||
id = insertSubject.newlyGeneratedId;
|
||||
}
|
||||
}
|
||||
updateOptions[relation.inverseRelation.joinColumn.name] = id;
|
||||
} else {
|
||||
updateOptions[relation.inverseRelation.joinColumn.name] = subject.entity[referencedColumn.propertyName] || subject.newlyGeneratedId;
|
||||
}
|
||||
|
||||
const id = subject.entity[referencedColumn.propertyName] || subject.newlyGeneratedId;
|
||||
const conditions = relation.inverseEntityMetadata.getDatabaseEntityIdMap(subRelatedEntity) || relation.inverseEntityMetadata.createSimpleDatabaseIdMap(relationId);
|
||||
const updateOptions = { [relation.inverseRelation.joinColumn.name]: id }; // todo: what if subject's id is not generated?
|
||||
const updatePromise = this.queryRunner.update(relation.inverseEntityMetadata.table.name, updateOptions, conditions);
|
||||
updatePromises.push(updatePromise);
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
// we also need to update relation ids if newly inserted objects are used from inverse side in one-to-one inverse relation
|
||||
subject.metadata.oneToOneRelations.filter(relation => !relation.isOwning).forEach(relation => {
|
||||
const referencedColumn = relation.inverseRelation.joinColumn.referencedColumn;
|
||||
const value = subject.entity[relation.propertyName];
|
||||
|
||||
this.insertSubjects.forEach(insertedSubject => {
|
||||
if (value === insertedSubject.entity) {
|
||||
|
||||
if (referencedColumn.isGenerated) {
|
||||
const conditions = insertedSubject.metadata.getDatabaseEntityIdMap(insertedSubject.entity) || insertedSubject.metadata.createSimpleDatabaseIdMap(insertedSubject.newlyGeneratedId);
|
||||
const updateOptions = { [relation.inverseRelation.joinColumn.name]: subject.newlyGeneratedId };
|
||||
const updatePromise = this.queryRunner.update(relation.inverseRelation.entityMetadata.table.name, updateOptions, conditions);
|
||||
updatePromises.push(updatePromise);
|
||||
}
|
||||
|
||||
// todo: implement other special referenced column types (update date, create date, version, discriminator column, etc.)
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
@ -1848,7 +1848,7 @@ export class QueryBuilder<Entity> {
|
||||
const whereSubStrings: string[] = [];
|
||||
if (metadata.hasMultiplePrimaryKeys) {
|
||||
metadata.primaryColumns.forEach((primaryColumn, secondIndex) => {
|
||||
whereSubStrings.push(id[primaryColumn.name] + "=:id_" + index + "_" + secondIndex);
|
||||
whereSubStrings.push(primaryColumn.name + "=:id_" + index + "_" + secondIndex);
|
||||
parameters["id_" + index + "_" + secondIndex] = id[primaryColumn.name];
|
||||
});
|
||||
metadata.parentIdColumns.forEach((primaryColumn, secondIndex) => {
|
||||
|
||||
@ -73,9 +73,7 @@ describe("persistence > one-to-many", function() {
|
||||
const loadedPost = await postRepository.findOneById(1, findOptions);
|
||||
expect(loadedPost).not.to.be.empty;
|
||||
expect(loadedPost.categories).not.to.be.empty;
|
||||
if (loadedPost.categories) {
|
||||
expect(loadedPost.categories[0]).not.to.be.empty;
|
||||
}
|
||||
expect(loadedPost.categories![0]).not.to.be.empty;
|
||||
})));
|
||||
|
||||
});
|
||||
|
||||
@ -20,7 +20,10 @@ describe("one-to-one", function() {
|
||||
|
||||
const options: ConnectionOptions = {
|
||||
driver: createTestingConnectionOptions("mysql"),
|
||||
entities: [Post, PostDetails, PostCategory, PostMetadata, PostImage, PostInformation, PostAuthor]
|
||||
entities: [Post, PostDetails, PostCategory, PostMetadata, PostImage, PostInformation, PostAuthor],
|
||||
// logging: {
|
||||
// logQueries: true
|
||||
// }
|
||||
};
|
||||
|
||||
// connect to db
|
||||
|
||||
@ -20,7 +20,10 @@ describe("many-to-one", function() {
|
||||
|
||||
const options: ConnectionOptions = {
|
||||
driver: createTestingConnectionOptions("mysql"),
|
||||
entities: [Post, PostDetails, PostCategory, PostMetadata, PostImage, PostInformation, PostAuthor]
|
||||
entities: [Post, PostDetails, PostCategory, PostMetadata, PostImage, PostInformation, PostAuthor],
|
||||
// logging: {
|
||||
// logQueries: true
|
||||
// }
|
||||
};
|
||||
|
||||
// connect to db
|
||||
|
||||
19
test/issues/58/entity/Category.ts
Normal file
19
test/issues/58/entity/Category.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import {Table} from "../../../../src/decorator/tables/Table";
|
||||
import {PrimaryGeneratedColumn} from "../../../../src/decorator/columns/PrimaryGeneratedColumn";
|
||||
import {Column} from "../../../../src/decorator/columns/Column";
|
||||
import {OneToMany} from "../../../../src/decorator/relations/OneToMany";
|
||||
import {PostCategory} from "./PostCategory";
|
||||
|
||||
@Table()
|
||||
export class Category {
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@OneToMany(type => PostCategory, postCategory => postCategory.category)
|
||||
posts: PostCategory[];
|
||||
|
||||
}
|
||||
19
test/issues/58/entity/Post.ts
Normal file
19
test/issues/58/entity/Post.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import {Table} from "../../../../src/decorator/tables/Table";
|
||||
import {PrimaryGeneratedColumn} from "../../../../src/decorator/columns/PrimaryGeneratedColumn";
|
||||
import {Column} from "../../../../src/decorator/columns/Column";
|
||||
import {OneToMany} from "../../../../src/decorator/relations/OneToMany";
|
||||
import {PostCategory} from "./PostCategory";
|
||||
|
||||
@Table()
|
||||
export class Post {
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
title: string;
|
||||
|
||||
@OneToMany(type => PostCategory, postCategoryRelation => postCategoryRelation.post)
|
||||
categories: PostCategory[];
|
||||
|
||||
}
|
||||
28
test/issues/58/entity/PostCategory.ts
Normal file
28
test/issues/58/entity/PostCategory.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import {Table} from "../../../../src/decorator/tables/Table";
|
||||
import {Column} from "../../../../src/decorator/columns/Column";
|
||||
import {Post} from "./Post";
|
||||
import {ManyToOne} from "../../../../src/decorator/relations/ManyToOne";
|
||||
import {Category} from "./Category";
|
||||
|
||||
@Table()
|
||||
export class PostCategory {
|
||||
|
||||
@ManyToOne(type => Post, post => post.categories, {
|
||||
primary: true,
|
||||
cascadeInsert: true
|
||||
})
|
||||
post: Post;
|
||||
|
||||
@ManyToOne(type => Category, category => category.posts, {
|
||||
primary: true,
|
||||
cascadeInsert: true
|
||||
})
|
||||
category: Category;
|
||||
|
||||
@Column()
|
||||
addedByAdmin: boolean;
|
||||
|
||||
@Column()
|
||||
addedByUser: boolean;
|
||||
|
||||
}
|
||||
77
test/issues/58/issue-58.ts
Normal file
77
test/issues/58/issue-58.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import "reflect-metadata";
|
||||
import {setupTestingConnections, closeConnections, reloadDatabases} from "../../utils/test-utils";
|
||||
import {Connection} from "../../../src/connection/Connection";
|
||||
import {Post} from "./entity/Post";
|
||||
import {Category} from "./entity/Category";
|
||||
import {PostCategory} from "./entity/PostCategory";
|
||||
import {expect} from "chai";
|
||||
|
||||
describe("github issues > #58 relations with multiple primary keys", () => {
|
||||
|
||||
let connections: Connection[];
|
||||
before(async () => connections = await setupTestingConnections({
|
||||
entities: [__dirname + "/entity/*{.js,.ts}"],
|
||||
schemaCreate: true,
|
||||
dropSchemaOnConnection: true,
|
||||
}));
|
||||
beforeEach(() => reloadDatabases(connections));
|
||||
after(() => closeConnections(connections));
|
||||
|
||||
it("should persist successfully and return persisted entity", () => Promise.all(connections.map(async connection => {
|
||||
|
||||
// create objects to save
|
||||
const category1 = new Category();
|
||||
category1.name = "category #1";
|
||||
|
||||
const category2 = new Category();
|
||||
category2.name = "category #2";
|
||||
|
||||
const post = new Post();
|
||||
post.title = "Hello Post #1";
|
||||
|
||||
const postCategory1 = new PostCategory();
|
||||
postCategory1.addedByAdmin = true;
|
||||
postCategory1.addedByUser = false;
|
||||
postCategory1.category = category1;
|
||||
postCategory1.post = post;
|
||||
|
||||
const postCategory2 = new PostCategory();
|
||||
postCategory2.addedByAdmin = false;
|
||||
postCategory2.addedByUser = true;
|
||||
postCategory2.category = category2;
|
||||
postCategory2.post = post;
|
||||
|
||||
await connection.entityManager.persist(postCategory1);
|
||||
await connection.entityManager.persist(postCategory2);
|
||||
|
||||
// check that all persisted objects exist
|
||||
const loadedPost = await connection.entityManager
|
||||
.createQueryBuilder(Post, "post")
|
||||
.innerJoinAndSelect("post.categories", "postCategory")
|
||||
.innerJoinAndSelect("postCategory.category", "category")
|
||||
.getSingleResult();
|
||||
|
||||
expect(loadedPost).not.to.be.empty;
|
||||
loadedPost.should.be.eql({
|
||||
id: 1,
|
||||
title: "Hello Post #1",
|
||||
categories: [{
|
||||
addedByAdmin: true,
|
||||
addedByUser: false,
|
||||
category: {
|
||||
id: 1,
|
||||
name: "category #1"
|
||||
}
|
||||
}, {
|
||||
addedByAdmin: false,
|
||||
addedByUser: true,
|
||||
category: {
|
||||
id: 2,
|
||||
name: "category #2"
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
})));
|
||||
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user