diff --git a/sample/sample25-insert-from-inverse-side/app.ts b/sample/sample25-insert-from-inverse-side/app.ts new file mode 100644 index 000000000..6acbf446f --- /dev/null +++ b/sample/sample25-insert-from-inverse-side/app.ts @@ -0,0 +1,62 @@ +import "reflect-metadata"; +import {createConnection, CreateConnectionOptions} from "../../src/typeorm"; +import {Post} from "./entity/Post"; +import {Author} from "./entity/Author"; + +const options: CreateConnectionOptions = { + driver: "mysql", + connection: { + host: "192.168.99.100", + port: 3306, + username: "root", + password: "admin", + database: "test", + autoSchemaCreate: true, + logging: { + logOnlyFailedQueries: true, + logFailedQueryError: true + } + }, + entities: [Post, Author] +}; + +createConnection(options).then(connection => { + + let postRepository = connection.getRepository(Post); + let authorRepository = connection.getRepository(Author); + + const authorPromise = authorRepository.findOneById(1).then(author => { + if (!author) { + author = new Author(); + author.name = "Umed"; + return authorRepository.persist(author).then(savedAuthor => { + return authorRepository.findOneById(1); + }); + } + return author; + }); + + const postPromise = postRepository.findOneById(1).then(post => { + if (!post) { + post = new Post(); + post.title = "Hello post"; + post.text = "This is post contents"; + return postRepository.persist(post).then(savedPost => { + return postRepository.findOneById(1); + }); + } + return post; + }); + + return Promise.all([authorPromise, postPromise]) + .then(results => { + const [author, post] = results; + author.posts = [post]; + return authorRepository.persist(author); + }) + .then(savedAuthor => { + console.log("Author has been saved: ", savedAuthor); + }) + .catch(error => console.log(error.stack)); + +}, error => console.log("Cannot connect: ", error)); \ No newline at end of file diff --git a/sample/sample25-insert-from-inverse-side/entity/Author.ts b/sample/sample25-insert-from-inverse-side/entity/Author.ts new file mode 100644 index 000000000..3b67ef02e --- /dev/null +++ b/sample/sample25-insert-from-inverse-side/entity/Author.ts @@ -0,0 +1,18 @@ +import {PrimaryColumn, Column} from "../../../src/columns"; +import {Table} from "../../../src/tables"; +import {OneToMany} from "../../../src/decorator/relations/OneToMany"; +import {Post} from "./Post"; + +@Table("sample25_author") +export class Author { + + @PrimaryColumn("int", { generated: true }) + id: number; + + @Column() + name: string; + + @OneToMany(type => Post, author => author.author) + posts: Post[]; + +} \ No newline at end of file diff --git a/sample/sample25-insert-from-inverse-side/entity/Post.ts b/sample/sample25-insert-from-inverse-side/entity/Post.ts new file mode 100644 index 000000000..8f7444d0a --- /dev/null +++ b/sample/sample25-insert-from-inverse-side/entity/Post.ts @@ -0,0 +1,21 @@ +import {PrimaryColumn, Column} from "../../../src/columns"; +import {Table} from "../../../src/tables"; +import {Author} from "./Author"; +import {ManyToOne} from "../../../src/decorator/relations/ManyToOne"; + +@Table("sample25_post") +export class Post { + + @PrimaryColumn("int", { generated: true }) + id: number; + + @Column() + title: string; + + @Column() + text: string; + + @ManyToOne(type => Author, author => author.posts) + author: Author; + +} \ No newline at end of file diff --git a/src/driver/MysqlDriver.ts b/src/driver/MysqlDriver.ts index 3620cb718..7dc30c34e 100644 --- a/src/driver/MysqlDriver.ts +++ b/src/driver/MysqlDriver.ts @@ -183,6 +183,7 @@ export class MysqlDriver extends BaseDriver implements Driver { const updateValues = this.escapeObjectMap(valuesMap).join(","); const conditionString = this.escapeObjectMap(conditions).join(" AND "); const query = `UPDATE ${tableName} SET ${updateValues} ${conditionString ? (" WHERE " + conditionString) : ""}`; + // console.log("executing update: ", query); return this.query(query).then(() => {}); // const qb = this.createQueryBuilder().update(tableName, valuesMap).from(tableName, "t"); // Object.keys(conditions).forEach(key => qb.andWhere(key + "=:" + key, { [key]: ( conditions)[key] })); diff --git a/src/persistment/EntityPersistOperationsBuilder.ts b/src/persistment/EntityPersistOperationsBuilder.ts index 97a8e9f10..b4427a7ed 100644 --- a/src/persistment/EntityPersistOperationsBuilder.ts +++ b/src/persistment/EntityPersistOperationsBuilder.ts @@ -1,6 +1,5 @@ import {EntityMetadata} from "../metadata/EntityMetadata"; import {RelationMetadata} from "../metadata/RelationMetadata"; -import {Connection} from "../connection/Connection"; import {PersistOperation, EntityWithId} from "./operation/PersistOperation"; import {InsertOperation} from "./operation/InsertOperation"; import {UpdateByRelationOperation} from "./operation/UpdateByRelationOperation"; @@ -9,6 +8,7 @@ import {UpdateOperation} from "./operation/UpdateOperation"; import {CascadesNotAllowedError} from "./error/CascadesNotAllowedError"; import {RemoveOperation} from "./operation/RemoveOperation"; import {EntityMetadataCollection} from "../metadata/collection/EntityMetadataCollection"; +import {UpdateByInverseSideOperation} from "./operation/UpdateByInverseSideOperation"; /** * 1. collect all exist objects from the db entity @@ -72,10 +72,12 @@ export class EntityPersistOperationBuilder { persistOperation.allPersistedEntities = allPersistedEntities; persistOperation.inserts = this.findCascadeInsertedEntities(persistedEntity, dbEntities); persistOperation.updatesByRelations = this.updateRelations(persistOperation.inserts, persistedEntity); + persistOperation.updatesByInverseRelations = this.updateInverseRelations(metadata, dbEntity, persistedEntity); persistOperation.updates = this.findCascadeUpdateEntities(persistOperation.updatesByRelations, metadata, dbEntity, persistedEntity); persistOperation.junctionInserts = this.findJunctionInsertOperations(metadata, persistedEntity, dbEntities); persistOperation.removes = this.findCascadeRemovedEntities(metadata, dbEntity, allPersistedEntities, undefined, undefined, undefined); persistOperation.junctionRemoves = this.findJunctionRemoveOperations(metadata, dbEntity, allPersistedEntities); + return persistOperation; } @@ -221,6 +223,31 @@ export class EntityPersistOperationBuilder { return operations; } + private updateInverseRelations(metadata: EntityMetadata, + dbEntity: any, + newEntity: any, + operations: UpdateByInverseSideOperation[] = []): UpdateByInverseSideOperation[] { + metadata.relations + .filter(relation => relation.isOneToMany) // todo: maybe need to check isOneToOne and not owner + .filter(relation => newEntity[relation.propertyName] instanceof Array) // todo: what to do with empty relations? need to set to NULL from inverse side? + .forEach(relation => { + + // to find new objects in relation go throw all objects in newEntity and check if they don't exist in dbEntity + newEntity[relation.propertyName].filter((subEntity: any) => { + if (!dbEntity /* are you sure about this? */ || !dbEntity[relation.propertyName]) // if there is no items in dbEntity - then all items in newEntity are new + return true; + + return !dbEntity[relation.propertyName].find((dbSubEntity: any) => { + return relation.inverseEntityMetadata.getEntityId(subEntity) === relation.inverseEntityMetadata.getEntityId(dbSubEntity); + }); + }).forEach((subEntity: any) => { + operations.push(new UpdateByInverseSideOperation(subEntity, newEntity, relation)); + }); + }); + + return operations; + } + /** * To update relation, you need: * update table where this relation (owner side) @@ -351,7 +378,16 @@ export class EntityPersistOperationBuilder { return metadata.relations .filter(relation => relation.isManyToOne || (relation.isOneToOne && relation.isOwning)) .filter(relation => !updatesByRelations.find(operation => operation.targetEntity === newEntity && operation.updatedRelation === relation)) // try to find if there is update by relation operation - we dont need to generate update relation operation for this - .filter(relation => newEntity[relation.propertyName] !== dbEntity[relation.name]); + .filter(relation => { + if (!newEntity[relation.propertyName] && !dbEntity[relation.name]) + return false; + if (!newEntity[relation.propertyName] || !dbEntity[relation.name]) + return true; + + const newEntityRelationMetadata = this.entityMetadatas.findByTarget(newEntity[relation.propertyName].constructor); + const dbEntityRelationMetadata = this.entityMetadatas.findByTarget(dbEntity[relation.name].constructor); + return newEntityRelationMetadata.getEntityId(newEntity[relation.propertyName]) !== dbEntityRelationMetadata.getEntityId(dbEntity[relation.name]); + }); } private findEntityWithId(entityWithIds: EntityWithId[], entityClass: Function, id: any) { diff --git a/src/persistment/PersistOperationExecutor.ts b/src/persistment/PersistOperationExecutor.ts index afe4b665c..40718a3f4 100644 --- a/src/persistment/PersistOperationExecutor.ts +++ b/src/persistment/PersistOperationExecutor.ts @@ -8,6 +8,7 @@ import {UpdateByRelationOperation} from "./operation/UpdateByRelationOperation"; import {Broadcaster} from "../subscriber/Broadcaster"; import {EntityMetadataCollection} from "../metadata/collection/EntityMetadataCollection"; import {Driver} from "../driver/Driver"; +import {UpdateByInverseSideOperation} from "./operation/UpdateByInverseSideOperation"; /** * Executes PersistOperation in the given connection. @@ -43,6 +44,7 @@ export class PersistOperationExecutor { .then(() => this.executeRemoveJunctionsOperations(persistOperation)) .then(() => this.executeRemoveRelationOperations(persistOperation)) .then(() => this.executeUpdateRelationsOperations(persistOperation)) + .then(() => this.executeUpdateInverseRelationsOperations(persistOperation)) .then(() => this.executeUpdateOperations(persistOperation)) .then(() => this.executeRemoveOperations(persistOperation)) .then(() => this.driver.endTransaction()) @@ -169,6 +171,15 @@ export class PersistOperationExecutor { })); } + /** + * Executes update relations operations. + */ + private executeUpdateInverseRelationsOperations(persistOperation: PersistOperation) { + return Promise.all(persistOperation.updatesByInverseRelations.map(updateInverseOperation => { + return this.updateInverseRelation(updateInverseOperation); + })); + } + /** * Executes update operations. */ @@ -301,6 +312,36 @@ export class PersistOperationExecutor { return this.driver.update(tableName, { [relationName]: relationId }, { [idColumn]: id }); } + private updateInverseRelation(operation: UpdateByInverseSideOperation) { + /*let tableName: string, relationName: string, relationId: any, idColumn: string, id: any; + const relatedInsertOperation = insertOperations.find(o => o.entity === operation.targetEntity); + const idInInserts = relatedInsertOperation ? relatedInsertOperation.entityId : null; + if (operation.updatedRelation.isOneToMany) { + const metadata = this.entityMetadatas.findByTarget(operation.insertOperation.entity.constructor); + tableName = metadata.table.name; + relationName = operation.updatedRelation.inverseRelation.name; + relationId = operation.targetEntity[metadata.primaryColumn.propertyName] || idInInserts; + idColumn = metadata.primaryColumn.name; + id = operation.insertOperation.entityId; + + } else { + const metadata = this.entityMetadatas.findByTarget(operation.targetEntity.constructor); + tableName = metadata.table.name; + relationName = operation.updatedRelation.name; + relationId = operation.insertOperation.entityId; + idColumn = metadata.primaryColumn.name; + id = operation.targetEntity[metadata.primaryColumn.propertyName] || idInInserts; + }*/ + const targetEntityMetadata = this.entityMetadatas.findByTarget(operation.targetEntity.constructor); + const tableName = targetEntityMetadata.table.name; + const targetRelation = operation.fromRelation.inverseRelation; + const targetEntityId = operation.fromEntity[targetRelation.joinColumn.referencedColumn.name]; + const idColumn = targetEntityMetadata.primaryColumn.name; + const id = targetEntityMetadata.getEntityId(operation.targetEntity); + + return this.driver.update(tableName, { [targetRelation.name]: targetEntityId }, { [idColumn]: id }); + } + private update(updateOperation: UpdateOperation) { const entity = updateOperation.entity; const metadata = this.entityMetadatas.findByTarget(entity.constructor); diff --git a/src/persistment/operation/PersistOperation.ts b/src/persistment/operation/PersistOperation.ts index 40f461b26..af329cfca 100644 --- a/src/persistment/operation/PersistOperation.ts +++ b/src/persistment/operation/PersistOperation.ts @@ -4,6 +4,7 @@ import {UpdateOperation} from "./UpdateOperation"; import {JunctionInsertOperation} from "./JunctionInsertOperation"; import {JunctionRemoveOperation} from "./JunctionRemoveOperation"; import {UpdateByRelationOperation} from "./UpdateByRelationOperation"; +import {UpdateByInverseSideOperation} from "./UpdateByInverseSideOperation"; /** * @internal @@ -30,6 +31,7 @@ export class PersistOperation { junctionInserts: JunctionInsertOperation[] = []; junctionRemoves: JunctionRemoveOperation[] = []; updatesByRelations: UpdateByRelationOperation[] = []; + updatesByInverseRelations: UpdateByInverseSideOperation[] = []; log() { console.log("---------------------------------------------------------"); @@ -73,6 +75,10 @@ export class PersistOperation { console.log("---------------------------------------------------------"); console.log(this.updatesByRelations); console.log("---------------------------------------------------------"); + console.log("UPDATES BY INVERSE RELATIONS"); + console.log("---------------------------------------------------------"); + console.log(this.updatesByInverseRelations); + console.log("---------------------------------------------------------"); } } diff --git a/src/persistment/operation/UpdateByInverseSideOperation.ts b/src/persistment/operation/UpdateByInverseSideOperation.ts new file mode 100644 index 000000000..80d16b8ca --- /dev/null +++ b/src/persistment/operation/UpdateByInverseSideOperation.ts @@ -0,0 +1,11 @@ +import {RelationMetadata} from "../../metadata/RelationMetadata"; + +/** + * @internal + */ +export class UpdateByInverseSideOperation { + constructor(public targetEntity: any, + public fromEntity: any, + public fromRelation: RelationMetadata) { + } +} \ No newline at end of file diff --git a/src/query-builder/transformer/PlainObjectToDatabaseEntityTransformer.ts b/src/query-builder/transformer/PlainObjectToDatabaseEntityTransformer.ts index 37483af78..70489315b 100644 --- a/src/query-builder/transformer/PlainObjectToDatabaseEntityTransformer.ts +++ b/src/query-builder/transformer/PlainObjectToDatabaseEntityTransformer.ts @@ -31,9 +31,12 @@ export class PlainObjectToDatabaseEntityTransformer { const needToLoad = this.buildLoadMap(object, metadata, true); this.join(queryBuilder, needToLoad, alias); - return queryBuilder + + queryBuilder .where(alias + "." + metadata.primaryColumn.name + "=:id") - .setParameter("id", object[metadata.primaryColumn.name]) + .setParameter("id", object[metadata.primaryColumn.name]); + + return queryBuilder .getSingleResult(); }