diff --git a/package.json b/package.json index e8a5463de..f1bcdaaff 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "typeorm", "private": true, - "version": "0.2.0-alpha.5", + "version": "0.2.0-alpha.6", "description": "Data-Mapper ORM for TypeScript, ES7, ES6, ES5. Supports MySQL, PostgreSQL, MariaDB, SQLite, MS SQL Server, Oracle, WebSQL, MongoDB databases.", "license": "MIT", "readmeFilename": "README.md", diff --git a/src/metadata/EntityMetadata.ts b/src/metadata/EntityMetadata.ts index 4d80c56a6..eb6270a1c 100644 --- a/src/metadata/EntityMetadata.ts +++ b/src/metadata/EntityMetadata.ts @@ -480,6 +480,8 @@ export class EntityMetadata { /** * Compares two different entity instances by their ids. * Returns true if they match, false otherwise. + * + * @deprecated performance bottleneck */ compareEntities(firstEntity: ObjectLiteral, secondEntity: ObjectLiteral): boolean { diff --git a/src/persistence/EntityPersistExecutor.ts b/src/persistence/EntityPersistExecutor.ts index 2cf9ad94d..95e9ee828 100644 --- a/src/persistence/EntityPersistExecutor.ts +++ b/src/persistence/EntityPersistExecutor.ts @@ -7,7 +7,6 @@ import {CannotDetermineEntityError} from "../error/CannotDetermineEntityError"; import {QueryRunner} from "../query-runner/QueryRunner"; import {Connection} from "../connection/Connection"; import {Subject} from "./Subject"; -import {EntityMetadata} from "../metadata/EntityMetadata"; import {OneToManySubjectBuilder} from "./subject-builder/OneToManySubjectBuilder"; import {OneToOneInverseSideSubjectBuilder} from "./subject-builder/OneToOneInverseSideSubjectBuilder"; import {ManyToManySubjectBuilder} from "./subject-builder/ManyToManySubjectBuilder"; @@ -19,6 +18,15 @@ import {CascadesSubjectBuilder} from "./subject-builder/CascadesSubjectBuilder"; */ export class EntityPersistExecutor { + // ------------------------------------------------------------------------- + // Protected Properties + // ------------------------------------------------------------------------- + + /** + * All subjects being persisted in this executor. + */ + protected subjects: Subject[] = []; + // ------------------------------------------------------------------------- // Constructor // ------------------------------------------------------------------------- @@ -59,25 +67,58 @@ export class EntityPersistExecutor { try { // collect all operate subjects - const subjects: Subject[] = []; const entities: ObjectLiteral[] = this.entity instanceof Array ? this.entity : [this.entity]; - // console.log("entities", entities); - await Promise.all(entities.map(async entity => { + // console.time("building subjects..."); + + // create subjects for all entities we received for the persistence + entities.forEach(entity => { const entityTarget = this.target ? this.target : entity.constructor; if (entityTarget === Object) throw new CannotDetermineEntityError(this.mode); - const metadata = this.connection.getMetadata(entityTarget); - if (this.mode === "save") { - subjects.push(...await this.save(queryRunner, metadata, entity)); - } else { // remove - subjects.push(...await this.remove(queryRunner, metadata, entity)); - } - })); + this.subjects.push(new Subject({ + metadata: this.connection.getMetadata(entityTarget), + entity: entity, + canBeInserted: this.mode === "save", + canBeUpdated: this.mode === "save", + mustBeRemoved: this.mode === "remove" + })); + }); + + // console.time("building cascades..."); + // go thought each entity with metadata and create subjects and subjects by cascades for them + this.subjects.forEach(subject => { + // next step we build list of subjects we will operate with + // these subjects are subjects that we need to insert or update alongside with main persisted entity + new CascadesSubjectBuilder(subject, this.subjects).build(); + }); + // console.timeEnd("building cascades..."); + + // load database entities for all subjects we have + // next step is to load database entities for all operate subjects + // console.time("loading..."); + await new SubjectDatabaseEntityLoader(queryRunner, this.subjects).load(this.mode); + // console.timeEnd("loading..."); + + // console.time("other subjects..."); + // build all related subjects and change maps + if (this.mode === "save") { + new OneToManySubjectBuilder(this.subjects).build(); + new OneToOneInverseSideSubjectBuilder(this.subjects).build(); + new ManyToManySubjectBuilder(this.subjects).build(); + } else { + this.subjects.forEach(subject => { + if (subject.mustBeRemoved) { + new ManyToManySubjectBuilder(this.subjects).buildForAllRemoval(subject); + } + }); + } + // console.timeEnd("other subjects..."); + // console.timeEnd("building subjects..."); // console.log("subjects", subjects); // create a subject executor - const executor = new SubjectExecutor(queryRunner, subjects); + const executor = new SubjectExecutor(queryRunner, this.subjects); // make sure we have at least one executable operation before we create a transaction and proceed // if we don't have operations it means we don't really need to update or remove something @@ -100,8 +141,10 @@ export class EntityPersistExecutor { await executor.execute(); // commit transaction if it was started by us + // console.time("commit"); if (isTransactionStartedByUs === true) await queryRunner.commitTransaction(); + // console.timeEnd("commit"); } catch (error) { @@ -123,58 +166,4 @@ export class EntityPersistExecutor { }); } - // ------------------------------------------------------------------------- - // Protected Methods - // ------------------------------------------------------------------------- - - /** - * Builds operations for entity that is being inserted/updated. - */ - protected async save(queryRunner: QueryRunner, metadata: EntityMetadata, entity: ObjectLiteral): Promise { - - // create subject for currently persisted entity and mark that it can be inserted and updated - const mainSubject = new Subject({ - metadata: metadata, - entity: entity, - canBeInserted: true, - canBeUpdated: true, - }); - - // next step we build list of subjects we will operate with - // these subjects are subjects that we need to insert or update alongside with main persisted entity - const subjects = await new CascadesSubjectBuilder(mainSubject).build(); - - // next step is to load database entities of all operate subjects - await new SubjectDatabaseEntityLoader(queryRunner, subjects).load("save"); - - // build all related subjects and change maps - new OneToManySubjectBuilder(subjects).build(); - new OneToOneInverseSideSubjectBuilder(subjects).build(); - new ManyToManySubjectBuilder(subjects).build(); - - return subjects; - } - - /** - * Builds only remove operations for entity that is being removed. - */ - protected async remove(queryRunner: QueryRunner, metadata: EntityMetadata, entity: ObjectLiteral): Promise { - - // create subject for currently removed entity and mark that it must be removed - const mainSubject = new Subject({ - metadata: metadata, - entity: entity, - mustBeRemoved: true, - }); - const subjects: Subject[] = [mainSubject]; - - // next step is to load database entities for all operate subjects - await new SubjectDatabaseEntityLoader(queryRunner, subjects).load("remove"); - - // build subjects for junction tables - new ManyToManySubjectBuilder(subjects).buildForAllRemoval(mainSubject); - - return subjects; - } - } \ No newline at end of file diff --git a/src/persistence/SubjectExecutor.ts b/src/persistence/SubjectExecutor.ts index eedb0fde0..ea9ceb619 100644 --- a/src/persistence/SubjectExecutor.ts +++ b/src/persistence/SubjectExecutor.ts @@ -76,6 +76,9 @@ export class SubjectExecutor { */ async execute(): Promise { + // console.time("execution"); + // console.time("prepare"); + // broadcast "before" events before we start insert / update / remove operations await this.broadcastBeforeEventsForAll(); @@ -85,25 +88,34 @@ export class SubjectExecutor { // make sure our insert subjects are sorted (using topological sorting) to make cascade inserts work properly this.insertSubjects = new SubjectTopoligicalSorter(this.insertSubjects).sort("insert"); + // console.timeEnd("prepare"); + // execute all insert operations + // console.time("insertion"); await this.executeInsertOperations(); + // console.timeEnd("insertion"); // recompute update operations since insertion can create updation operations for the // properties it wasn't able to handle on its own (referenced columns) this.updateSubjects = this.allSubjects.filter(subject => subject.mustBeUpdated); // execute update operations + // console.time("updation"); await this.executeUpdateOperations(); + // console.timeEnd("updation"); // make sure our remove subjects are sorted (using topological sorting) when multiple entities are passed for the removal + // console.time("removal"); this.removeSubjects = new SubjectTopoligicalSorter(this.removeSubjects).sort("delete"); await this.executeRemoveOperations(); + // console.timeEnd("removal"); // update all special columns in persisted entities, like inserted id or remove ids from the removed entities await this.updateSpecialColumnsInPersistedEntities(); // finally broadcast "after" events after we finish insert / update / remove operations await this.broadcastAfterEventsForAll(); + // console.timeEnd("execution"); } // ------------------------------------------------------------------------- diff --git a/src/persistence/subject-builder/CascadesSubjectBuilder.ts b/src/persistence/subject-builder/CascadesSubjectBuilder.ts index c40514e2c..b55c6af43 100644 --- a/src/persistence/subject-builder/CascadesSubjectBuilder.ts +++ b/src/persistence/subject-builder/CascadesSubjectBuilder.ts @@ -11,7 +11,8 @@ export class CascadesSubjectBuilder { // Constructor // --------------------------------------------------------------------- - constructor(protected subject: Subject) { + constructor(protected subject: Subject, + protected allSubjects: Subject[]) { } // --------------------------------------------------------------------- @@ -21,10 +22,8 @@ export class CascadesSubjectBuilder { /** * Builds a cascade subjects tree and pushes them in into the given array of subjects. */ - build(): Subject[] { - const subjects: Subject[] = [this.subject]; - this.buildRecursively(subjects, this.subject); - return subjects; + build() { + this.buildRecursively(this.subject); } // --------------------------------------------------------------------- @@ -34,10 +33,10 @@ export class CascadesSubjectBuilder { /** * Builds a cascade subjects recursively. */ - protected buildRecursively(subjects: Subject[], subject: Subject) { + protected buildRecursively(subject: Subject) { subject.metadata - .extractRelationValuesFromEntity(subject.entity!, subject.metadata.relations) + .extractRelationValuesFromEntity(subject.entity!, subject.metadata.relations) // todo: we can create EntityMetadata.cascadeRelations .forEach(([relation, relationEntity, relationEntityMetadata]) => { // we need only defined values and insert or update cascades of the relation should be set @@ -52,7 +51,7 @@ export class CascadesSubjectBuilder { return; // if we already has this entity in list of operated subjects then skip it to avoid recursion - const alreadyExistRelationEntitySubject = this.findByPersistEntityLike(subjects, relationEntityMetadata.target, relationEntity); + const alreadyExistRelationEntitySubject = this.findByPersistEntityLike(relationEntityMetadata.target, relationEntity); if (alreadyExistRelationEntitySubject) { if (alreadyExistRelationEntitySubject.canBeInserted === false) // if its not marked for insertion yet alreadyExistRelationEntitySubject.canBeInserted = relation.isCascadeInsert === true; @@ -69,10 +68,10 @@ export class CascadesSubjectBuilder { canBeInserted: relation.isCascadeInsert === true, canBeUpdated: relation.isCascadeUpdate === true }); - subjects.push(relationEntitySubject); + this.allSubjects.push(relationEntitySubject); // go recursively and find other entities we need to insert/update - this.buildRecursively(subjects, relationEntitySubject); + this.buildRecursively(relationEntitySubject); }); } @@ -80,8 +79,8 @@ export class CascadesSubjectBuilder { * Finds subject where entity like given subject's entity. * Comparision made by entity id. */ - protected findByPersistEntityLike(subjects: Subject[], entityTarget: Function|string, entity: ObjectLiteral): Subject|undefined { - return subjects.find(subject => { + protected findByPersistEntityLike(entityTarget: Function|string, entity: ObjectLiteral): Subject|undefined { + return this.allSubjects.find(subject => { if (!subject.entity) return false; diff --git a/src/query-builder/DeleteQueryBuilder.ts b/src/query-builder/DeleteQueryBuilder.ts index fadecfa35..0a79edfa2 100644 --- a/src/query-builder/DeleteQueryBuilder.ts +++ b/src/query-builder/DeleteQueryBuilder.ts @@ -145,30 +145,21 @@ export class DeleteQueryBuilder extends QueryBuilder implements * Adds new AND WHERE with conditions for the given ids. */ whereInIds(ids: any|any[]): this { - ids = ids instanceof Array ? ids : [ids]; - const [whereExpression, parameters] = this.createWhereIdsExpression(ids); - this.where(whereExpression, parameters); - return this; + return this.where(this.createWhereIdsExpression(ids)); } /** * Adds new AND WHERE with conditions for the given ids. */ andWhereInIds(ids: any|any[]): this { - ids = ids instanceof Array ? ids : [ids]; - const [whereExpression, parameters] = this.createWhereIdsExpression(ids); - this.andWhere(whereExpression, parameters); - return this; + return this.andWhere(this.createWhereIdsExpression(ids)); } /** * Adds new OR WHERE with conditions for the given ids. */ orWhereInIds(ids: any|any[]): this { - ids = ids instanceof Array ? ids : [ids]; - const [whereExpression, parameters] = this.createWhereIdsExpression(ids); - this.orWhere(whereExpression, parameters); - return this; + return this.orWhere(this.createWhereIdsExpression(ids)); } /** * Optional returning/output clause. diff --git a/src/query-builder/QueryBuilder.ts b/src/query-builder/QueryBuilder.ts index 105ecb378..b6f35b77c 100644 --- a/src/query-builder/QueryBuilder.ts +++ b/src/query-builder/QueryBuilder.ts @@ -659,28 +659,34 @@ export abstract class QueryBuilder { /** * Creates "WHERE" expression and variables for the given "ids". */ - protected createWhereIdsExpression(ids: any[]): [string, ObjectLiteral] { + protected createWhereIdsExpression(ids: any|any[]): string { + ids = ids instanceof Array ? ids : [ids]; const metadata = this.expressionMap.mainAlias!.metadata; // create shortcuts for better readability const alias = this.expressionMap.aliasNamePrefixingEnabled ? this.escape(this.expressionMap.mainAlias!.name) + "." : ""; - const parameters: ObjectLiteral = {}; - const whereStrings = ids.map((id, index) => { + let parameterIndex = Object.keys(this.expressionMap.nativeParameters).length; + const whereStrings = (ids as any[]).map((id, index) => { id = metadata.ensureEntityIdMap(id); const whereSubStrings: string[] = []; metadata.primaryColumns.forEach((primaryColumn, secondIndex) => { - whereSubStrings.push(alias + this.escape(primaryColumn.databaseName) + "=:id_" + index + "_" + secondIndex); - parameters["id_" + index + "_" + secondIndex] = primaryColumn.getEntityValue(id); + const parameterName = "id_" + index + "_" + secondIndex; + // whereSubStrings.push(alias + this.escape(primaryColumn.databaseName) + "=:id_" + index + "_" + secondIndex); + whereSubStrings.push(alias + this.escape(primaryColumn.databaseName) + " = " + this.connection.driver.createParameter(parameterName, parameterIndex)); + this.expressionMap.nativeParameters[parameterName] = primaryColumn.getEntityValue(id); + parameterIndex++; }); metadata.parentIdColumns.forEach((parentIdColumn, secondIndex) => { - whereSubStrings.push(alias + this.escape(parentIdColumn.databaseName) + "=:parentId_" + index + "_" + secondIndex); - parameters["parentId_" + index + "_" + secondIndex] = parentIdColumn.getEntityValue(id); + // whereSubStrings.push(alias + this.escape(parentIdColumn.databaseName) + "=:parentId_" + index + "_" + secondIndex); + const parameterName = "parentId_" + index + "_" + secondIndex; + whereSubStrings.push(alias + this.escape(parentIdColumn.databaseName) + " = " + this.connection.driver.createParameter(parameterName, parameterIndex)); + this.expressionMap.nativeParameters[parameterName] = parentIdColumn.getEntityValue(id); + parameterIndex++; }); return whereSubStrings.join(" AND "); }); - const whereString = whereStrings.length > 1 ? whereStrings.map(whereString => "(" + whereString + ")").join(" OR ") : whereStrings[0]; - return [whereString, parameters]; + return whereStrings.length > 1 ? whereStrings.map(whereString => "(" + whereString + ")").join(" OR ") : whereStrings[0]; } /** diff --git a/src/query-builder/SelectQueryBuilder.ts b/src/query-builder/SelectQueryBuilder.ts index f3f025466..7cb0d1657 100644 --- a/src/query-builder/SelectQueryBuilder.ts +++ b/src/query-builder/SelectQueryBuilder.ts @@ -720,10 +720,7 @@ export class SelectQueryBuilder extends QueryBuilder implements * for example [{ firstId: 1, secondId: 2 }, { firstId: 2, secondId: 3 }, ...] */ whereInIds(ids: any|any[]): this { - ids = ids instanceof Array ? ids : [ids]; - const [whereExpression, parameters] = this.createWhereIdsExpression(ids); - this.where(whereExpression, parameters); - return this; + return this.where(this.createWhereIdsExpression(ids)); } /** @@ -735,10 +732,7 @@ export class SelectQueryBuilder extends QueryBuilder implements * for example [{ firstId: 1, secondId: 2 }, { firstId: 2, secondId: 3 }, ...] */ andWhereInIds(ids: any|any[]): this { - ids = ids instanceof Array ? ids : [ids]; - const [whereExpression, parameters] = this.createWhereIdsExpression(ids); - this.andWhere(whereExpression, parameters); - return this; + return this.andWhere(this.createWhereIdsExpression(ids)); } /** @@ -750,10 +744,7 @@ export class SelectQueryBuilder extends QueryBuilder implements * for example [{ firstId: 1, secondId: 2 }, { firstId: 2, secondId: 3 }, ...] */ orWhereInIds(ids: any|any[]): this { - ids = ids instanceof Array ? ids : [ids]; - const [whereExpression, parameters] = this.createWhereIdsExpression(ids); - this.orWhere(whereExpression, parameters); - return this; + return this.orWhere(this.createWhereIdsExpression(ids)); } /** diff --git a/src/query-builder/UpdateQueryBuilder.ts b/src/query-builder/UpdateQueryBuilder.ts index 98e497556..010465c6c 100644 --- a/src/query-builder/UpdateQueryBuilder.ts +++ b/src/query-builder/UpdateQueryBuilder.ts @@ -12,6 +12,8 @@ import {ReturningStatementNotSupportedError} from "../error/ReturningStatementNo import {ArrayParameter} from "./ArrayParameter"; import {ReturningResultsEntityUpdator} from "./ReturningResultsEntityUpdator"; import {SqljsDriver} from "../driver/sqljs/SqljsDriver"; +import {MysqlDriver} from "../driver/mysql/MysqlDriver"; +import {WebsqlDriver} from "../driver/websql/WebsqlDriver"; /** * Allows to build complex sql queries in a fashion way and execute those queries. @@ -160,30 +162,21 @@ export class UpdateQueryBuilder extends QueryBuilder implements * Adds new AND WHERE with conditions for the given ids. */ whereInIds(ids: any|any[]): this { - ids = ids instanceof Array ? ids : [ids]; - const [whereExpression, parameters] = this.createWhereIdsExpression(ids); - this.where(whereExpression, parameters); - return this; + return this.where(this.createWhereIdsExpression(ids)); } /** * Adds new AND WHERE with conditions for the given ids. */ andWhereInIds(ids: any|any[]): this { - ids = ids instanceof Array ? ids : [ids]; - const [whereExpression, parameters] = this.createWhereIdsExpression(ids); - this.andWhere(whereExpression, parameters); - return this; + return this.andWhere(this.createWhereIdsExpression(ids)); } /** * Adds new OR WHERE with conditions for the given ids. */ orWhereInIds(ids: any|any[]): this { - ids = ids instanceof Array ? ids : [ids]; - const [whereExpression, parameters] = this.createWhereIdsExpression(ids); - this.orWhere(whereExpression, parameters); - return this; + return this.orWhere(this.createWhereIdsExpression(ids)); } /** * Optional returning/output clause. @@ -287,6 +280,8 @@ export class UpdateQueryBuilder extends QueryBuilder implements // prepare columns and values to be updated const updateColumnAndValues: string[] = []; + const newParameters: ObjectLiteral = {}; + let parametersCount = this.connection.driver instanceof MysqlDriver || this.connection.driver instanceof WebsqlDriver ? 0 : Object.keys(this.expressionMap.nativeParameters).length; if (metadata) { EntityMetadataUtils.createPropertyPath(metadata, valuesSet).forEach(propertyPath => { // todo: make this and other query builder to work with properly with tables without metadata @@ -306,25 +301,28 @@ export class UpdateQueryBuilder extends QueryBuilder implements updateColumnAndValues.push(this.escape(column.databaseName) + " = " + value()); } else { if (this.connection.driver instanceof SqlServerDriver) { - this.setParameter(paramName, this.connection.driver.parametrizeValue(column, value)); - } else { + value = this.connection.driver.parametrizeValue(column, value); - // we need to store array values in a special class to make sure parameter replacement will work correctly - if (value instanceof Array) - value = new ArrayParameter(value); - - this.setParameter(paramName, value); + } else if (value instanceof Array) { + value = new ArrayParameter(value); } - updateColumnAndValues.push(this.escape(column.databaseName) + " = :" + paramName); + + if (this.connection.driver instanceof MysqlDriver || this.connection.driver instanceof WebsqlDriver) { + newParameters[paramName] = value; + } else { + this.expressionMap.nativeParameters[paramName] = value; + } + + updateColumnAndValues.push(this.escape(column.databaseName) + " = " + this.connection.driver.createParameter(paramName, parametersCount)); + parametersCount++; } }); }); - if (metadata.versionColumn) updateColumnAndValues.push(this.escape(metadata.versionColumn.databaseName) + " = " + this.escape(metadata.versionColumn.databaseName) + " + 1"); if (metadata.updateDateColumn) - updateColumnAndValues.push(this.escape(metadata.updateDateColumn.databaseName) + " = CURRENT_TIMESTAMP"); // todo: fix issue with CURRENT_TIMESTAMP(6) being used + updateColumnAndValues.push(this.escape(metadata.updateDateColumn.databaseName) + " = CURRENT_TIMESTAMP"); // todo: fix issue with CURRENT_TIMESTAMP(6) being used, can "DEFAULT" be used?! } else { Object.keys(valuesSet).map(key => { @@ -339,12 +337,24 @@ export class UpdateQueryBuilder extends QueryBuilder implements if (value instanceof Array) value = new ArrayParameter(value); - updateColumnAndValues.push(this.escape(key) + " = :" + key); - this.setParameter(key, value); + if (this.connection.driver instanceof MysqlDriver || this.connection.driver instanceof WebsqlDriver) { + newParameters[key] = value; + } else { + this.expressionMap.nativeParameters[key] = value; + } + + updateColumnAndValues.push(this.escape(key) + " = " + this.connection.driver.createParameter(key, parametersCount)); + parametersCount++; } }); } + // we re-write parameters this way because we want our "UPDATE ... SET" parameters to be first in the list of "nativeParameters" + // because some drivers like mysql depend on order of parameters + if (this.connection.driver instanceof MysqlDriver || this.connection.driver instanceof WebsqlDriver) { + this.expressionMap.nativeParameters = Object.assign(newParameters, this.expressionMap.nativeParameters); + } + // get a table name and all column database names const whereExpression = this.createWhereExpression(); const returningExpression = this.createReturningExpression(); diff --git a/src/util/OrmUtils.ts b/src/util/OrmUtils.ts index ed293c507..9ffc0960c 100644 --- a/src/util/OrmUtils.ts +++ b/src/util/OrmUtils.ts @@ -88,95 +88,6 @@ export class OrmUtils { static deepCompare(...args: any[]) { let i: any, l: any, leftChain: any, rightChain: any; - function compare2Objects(x: any, y: any) { - let p; - - // remember that NaN === NaN returns false - // and isNaN(undefined) returns true - if (isNaN(x) && isNaN(y) && typeof x === "number" && typeof y === "number") - return true; - - // Compare primitives and functions. - // Check if both arguments link to the same object. - // Especially useful on the step where we compare prototypes - if (x === y) - return true; - - if (x.equals instanceof Function && x.equals(y)) - return true; - - // Works in case when functions are created in constructor. - // Comparing dates is a common scenario. Another built-ins? - // We can even handle functions passed across iframes - if ((typeof x === "function" && typeof y === "function") || - (x instanceof Date && y instanceof Date) || - (x instanceof RegExp && y instanceof RegExp) || - (x instanceof String && y instanceof String) || - (x instanceof Number && y instanceof Number)) - return x.toString() === y.toString(); - - // At last checking prototypes as good as we can - if (!(x instanceof Object && y instanceof Object)) - return false; - - if (x.isPrototypeOf(y) || y.isPrototypeOf(x)) - return false; - - if (x.constructor !== y.constructor) - return false; - - if (x.prototype !== y.prototype) - return false; - - // Check for infinitive linking loops - if (leftChain.indexOf(x) > -1 || rightChain.indexOf(y) > -1) - return false; - - // Quick checking of one object being a subset of another. - // todo: cache the structure of arguments[0] for performance - for (p in y) { - if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) { - return false; - } - else if (typeof y[p] !== typeof x[p]) { - return false; - } - } - - for (p in x) { - if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) { - return false; - } - else if (typeof y[p] !== typeof x[p]) { - return false; - } - - switch (typeof (x[p])) { - case "object": - case "function": - - leftChain.push(x); - rightChain.push(y); - - if (!compare2Objects (x[p], y[p])) { - return false; - } - - leftChain.pop(); - rightChain.pop(); - break; - - default: - if (x[p] !== y[p]) { - return false; - } - break; - } - } - - return true; - } - if (arguments.length < 1) { return true; // Die silently? Don't know how to handle such case, please help... // throw "Need two or more arguments to compare"; @@ -187,7 +98,7 @@ export class OrmUtils { leftChain = []; // Todo: this can be cached rightChain = []; - if (!compare2Objects(arguments[0], arguments[i])) { + if (!this.compare2Objects(leftChain, rightChain, arguments[0], arguments[i])) { return false; } } @@ -220,4 +131,94 @@ export class OrmUtils { return object; }, {} as ObjectLiteral); } + + private static compare2Objects(leftChain: any, rightChain: any, x: any, y: any) { + let p; + + // remember that NaN === NaN returns false + // and isNaN(undefined) returns true + if (isNaN(x) && isNaN(y) && typeof x === "number" && typeof y === "number") + return true; + + // Compare primitives and functions. + // Check if both arguments link to the same object. + // Especially useful on the step where we compare prototypes + if (x === y) + return true; + + if (x.equals instanceof Function && x.equals(y)) + return true; + + // Works in case when functions are created in constructor. + // Comparing dates is a common scenario. Another built-ins? + // We can even handle functions passed across iframes + if ((typeof x === "function" && typeof y === "function") || + (x instanceof Date && y instanceof Date) || + (x instanceof RegExp && y instanceof RegExp) || + (x instanceof String && y instanceof String) || + (x instanceof Number && y instanceof Number)) + return x.toString() === y.toString(); + + // At last checking prototypes as good as we can + if (!(x instanceof Object && y instanceof Object)) + return false; + + if (x.isPrototypeOf(y) || y.isPrototypeOf(x)) + return false; + + if (x.constructor !== y.constructor) + return false; + + if (x.prototype !== y.prototype) + return false; + + // Check for infinitive linking loops + if (leftChain.indexOf(x) > -1 || rightChain.indexOf(y) > -1) + return false; + + // Quick checking of one object being a subset of another. + // todo: cache the structure of arguments[0] for performance + for (p in y) { + if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) { + return false; + } + else if (typeof y[p] !== typeof x[p]) { + return false; + } + } + + for (p in x) { + if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) { + return false; + } + else if (typeof y[p] !== typeof x[p]) { + return false; + } + + switch (typeof (x[p])) { + case "object": + case "function": + + leftChain.push(x); + rightChain.push(y); + + if (!this.compare2Objects(leftChain, rightChain, x[p], y[p])) { + return false; + } + + leftChain.pop(); + rightChain.pop(); + break; + + default: + if (x[p] !== y[p]) { + return false; + } + break; + } + } + + return true; + } + } \ No newline at end of file diff --git a/test/benchmark/bulk-save-case1/bulk-save-case1.ts b/test/benchmark/bulk-save-case1/bulk-save-case1.ts index 72abe2afc..0804d84cc 100644 --- a/test/benchmark/bulk-save-case1/bulk-save-case1.ts +++ b/test/benchmark/bulk-save-case1/bulk-save-case1.ts @@ -6,7 +6,7 @@ import {Post} from "./entity/Post"; describe("benchmark > bulk-save > case1", () => { let connections: Connection[]; - before(async () => connections = await createTestingConnections({ __dirname, enabledDrivers: ["mysql", "postgres"] })); + before(async () => connections = await createTestingConnections({ __dirname, enabledDrivers: ["postgres"] })); beforeEach(() => reloadTestingDatabases(connections)); after(() => closeTestingConnections(connections)); @@ -24,24 +24,51 @@ describe("benchmark > bulk-save > case1", () => { posts.push(post); } - // await connection.manager.save(posts); - await connection.manager.insert(Post, posts); + await connection.manager.save(posts); + // await connection.manager.insert(Post, posts); }))); /** - * Before getters refactoring + * Before persistence refactoring * - √ testing bulk save of 1000 objects (3149ms) - √ testing bulk save of 1000 objects (2008ms) - √ testing bulk save of 1000 objects (1893ms) - √ testing bulk save of 1000 objects (1744ms) - √ testing bulk save of 1000 objects (1836ms) - √ testing bulk save of 1000 objects (1787ms) - √ testing bulk save of 1000 objects (1904ms) - √ testing bulk save of 1000 objects (1848ms) - √ testing bulk save of 1000 objects (1947ms) - √ testing bulk save of 1000 objects (2004ms) + * MySql + * + * √ testing bulk save of 1000 objects (2686ms) + * √ testing bulk save of 1000 objects (1579ms) + * √ testing bulk save of 1000 objects (1664ms) + * √ testing bulk save of 1000 objects (1426ms) + * √ testing bulk save of 1000 objects (1512ms) + * √ testing bulk save of 1000 objects (1526ms) + * √ testing bulk save of 1000 objects (1605ms) + * √ testing bulk save of 1000 objects (1914ms) + * √ testing bulk save of 1000 objects (1983ms) + * √ testing bulk save of 1000 objects (1500ms) + * + * Postgres + * + * √ testing bulk save of 1000 objects (3704ms) + * √ testing bulk save of 1000 objects (2080ms) + * √ testing bulk save of 1000 objects (2176ms) + * √ testing bulk save of 1000 objects (2447ms) + * √ testing bulk save of 1000 objects (2259ms) + * √ testing bulk save of 1000 objects (2112ms) + * √ testing bulk save of 1000 objects (2193ms) + * √ testing bulk save of 1000 objects (2211ms) + * √ testing bulk save of 1000 objects (2282ms) + * √ testing bulk save of 1000 objects (2551ms) + * + * SqlServer + * + * √ testing bulk save of 1000 objects (8098ms) + * √ testing bulk save of 1000 objects (6534ms) + * √ testing bulk save of 1000 objects (5789ms) + * √ testing bulk save of 1000 objects (5505ms) + * √ testing bulk save of 1000 objects (5813ms) + * √ testing bulk save of 1000 objects (5932ms) + * √ testing bulk save of 1000 objects (6114ms) + * √ testing bulk save of 1000 objects (5960ms) + * √ testing bulk save of 1000 objects (5755ms) + * √ testing bulk save of 1000 objects (5935ms) */ - }); \ No newline at end of file diff --git a/test/benchmark/bulk-save-case2/bulk-save-case2.ts b/test/benchmark/bulk-save-case2/bulk-save-case2.ts index b8eeac539..0c0b53b2e 100644 --- a/test/benchmark/bulk-save-case2/bulk-save-case2.ts +++ b/test/benchmark/bulk-save-case2/bulk-save-case2.ts @@ -10,10 +10,10 @@ describe("benchmark > bulk-save > case2", () => { beforeEach(() => reloadTestingDatabases(connections)); after(() => closeTestingConnections(connections)); - it("testing bulk save of 5000 objects", () => Promise.all(connections.map(async connection => { + it("testing bulk save of 10000 objects", () => Promise.all(connections.map(async connection => { const documents: Document[] = []; - for (let i = 0; i < 5000; i++) { + for (let i = 0; i < 10000; i++) { const document = new Document(); document.id = i.toString(); @@ -44,8 +44,7 @@ describe("benchmark > bulk-save > case2", () => { // await connection.manager.insert(Document, document); } - await connection.manager.insert(Document, documents); - // await connection.manager.save(documents); + await connection.manager.save(documents); // await connection.manager.insert(Document, documents); }))); diff --git a/test/integration/sample2-one-to-one.ts b/test/integration/sample2-one-to-one.ts index 7c4649ac5..c53b61da3 100644 --- a/test/integration/sample2-one-to-one.ts +++ b/test/integration/sample2-one-to-one.ts @@ -323,6 +323,7 @@ describe("one-to-one", function() { }); }); + // todo: check why it generates extra query describe("cascade updates should be executed when cascadeUpdate option is set", function() { let newPost: Post, newImage: PostImage;