diff --git a/packages/core/src/EntityManager.ts b/packages/core/src/EntityManager.ts index 54a652c07e2e..d1d61a51d00c 100644 --- a/packages/core/src/EntityManager.ts +++ b/packages/core/src/EntityManager.ts @@ -322,7 +322,7 @@ export class EntityManager { */ async nativeInsert>(entityName: EntityName, data: EntityData): Promise> { entityName = Utils.className(entityName); - data = QueryHelper.processParams(data); + data = QueryHelper.processObjectParams(data); this.validator.validateParams(data, 'insert data'); const res = await this.driver.nativeInsert(entityName, data, this.transactionContext); @@ -334,7 +334,7 @@ export class EntityManager { */ async nativeUpdate>(entityName: EntityName, where: FilterQuery, data: EntityData, options: UpdateOptions = {}): Promise { entityName = Utils.className(entityName); - data = QueryHelper.processParams(data); + data = QueryHelper.processObjectParams(data); where = QueryHelper.processWhere(where as FilterQuery, entityName, this.metadata); where = await this.applyFilters(entityName, where, options.filters ?? {}, 'update'); this.validator.validateParams(data, 'update data'); diff --git a/packages/core/src/utils/QueryHelper.ts b/packages/core/src/utils/QueryHelper.ts index 008221f87875..31dbf9f3dfcb 100644 --- a/packages/core/src/utils/QueryHelper.ts +++ b/packages/core/src/utils/QueryHelper.ts @@ -26,14 +26,20 @@ export class QueryHelper { } if (Utils.isPlainObject(params)) { - Object.keys(params).forEach(k => { - params[k] = QueryHelper.processParams(params[k], !!k); - }); + QueryHelper.processObjectParams(params); } return params; } + static processObjectParams(params: Dictionary = {}): any { + Object.keys(params).forEach(k => { + params[k] = QueryHelper.processParams(params[k], !!k); + }); + + return params; + } + static inlinePrimaryKeyObjects>(where: Dictionary, meta: EntityMetadata, metadata: MetadataStorage, key?: string): boolean { if (Array.isArray(where)) { where.forEach((item, i) => { diff --git a/packages/core/src/utils/Utils.ts b/packages/core/src/utils/Utils.ts index a8823e239ecf..41f177b4dc78 100644 --- a/packages/core/src/utils/Utils.ts +++ b/packages/core/src/utils/Utils.ts @@ -466,21 +466,8 @@ export class Utils { * Checks whether the value is POJO (e.g. `{ foo: 'bar' }`, and not instance of `Foo`) */ static isPlainObject(value: any): boolean { - if (!Utils.isObject(value)) { - return false; - } - - if (typeof value.constructor !== 'function') { - return false; - } - // eslint-disable-next-line no-prototype-builtins - if (!value.constructor.prototype.hasOwnProperty('isPrototypeOf')) { - return false; - } - - // most likely plain object - return true; + return value !== null && typeof value === 'object' && typeof value.constructor === 'function' && value.constructor.prototype.hasOwnProperty('isPrototypeOf'); } /** diff --git a/packages/knex/src/query/ObjectCriteriaNode.ts b/packages/knex/src/query/ObjectCriteriaNode.ts index c63cf220ebb0..fa739f431756 100644 --- a/packages/knex/src/query/ObjectCriteriaNode.ts +++ b/packages/knex/src/query/ObjectCriteriaNode.ts @@ -6,6 +6,14 @@ export class ObjectCriteriaNode extends CriteriaNode { static create(metadata: MetadataStorage, entityName: string, payload: Dictionary, parent?: CriteriaNode, key?: string): ObjectCriteriaNode { const node = new ObjectCriteriaNode(metadata, entityName, parent, key); const meta = metadata.find(entityName); + + if (!parent && Object.keys(payload).every(k => meta?.properties[k]?.reference === ReferenceType.SCALAR)) { + const node = new CriteriaNode(metadata, entityName, parent, key); + node.payload = payload; + + return node as ObjectCriteriaNode; + } + node.payload = Object.keys(payload).reduce((o, item) => { const prop = meta?.properties[item]; diff --git a/packages/knex/src/query/QueryBuilder.ts b/packages/knex/src/query/QueryBuilder.ts index d1631225380e..3d45307654d7 100644 --- a/packages/knex/src/query/QueryBuilder.ts +++ b/packages/knex/src/query/QueryBuilder.ts @@ -104,11 +104,11 @@ export class QueryBuilder = AnyEntity> { where(cond: QBFilterQuery, operator?: keyof typeof GroupOperator): this; where(cond: string, params?: any[], operator?: keyof typeof GroupOperator): this; where(cond: QBFilterQuery | string, params?: keyof typeof GroupOperator | any[], operator?: keyof typeof GroupOperator): this { - cond = QueryHelper.processWhere(cond as Dictionary, this.entityName, this.metadata)!; - if (Utils.isString(cond)) { cond = { [`(${cond})`]: Utils.asArray(params) }; operator = operator || '$and'; + } else { + cond = QueryHelper.processWhere(cond, this.entityName, this.metadata)!; } const op = operator || params as keyof typeof GroupOperator; @@ -493,7 +493,9 @@ export class QueryBuilder = AnyEntity> { }); } - QueryHelper.processParams([this._data, this._cond, this._having]); + QueryHelper.processObjectParams(this._data); + QueryHelper.processObjectParams(this._cond); + QueryHelper.processObjectParams(this._having); this.finalized = true; if (meta && this.flags.has(QueryFlag.PAGINATE) && this._limit! > 0) { diff --git a/tests/QueryBuilder.test.ts b/tests/QueryBuilder.test.ts index 10e50da5aba0..cb442d3bb1aa 100644 --- a/tests/QueryBuilder.test.ts +++ b/tests/QueryBuilder.test.ts @@ -6,6 +6,7 @@ import { MySqlDriver } from '@mikro-orm/mysql'; import { Address2, Author2, Book2, BookTag2, Car2, CarOwner2, Configuration2, FooBar2, FooBaz2, FooParam2, Publisher2, PublisherType, Test2, User2 } from './entities-sql'; import { initORMMySql } from './bootstrap'; import { BaseEntity2 } from './entities-sql/BaseEntity2'; +import { performance } from 'perf_hooks'; describe('QueryBuilder', () => { @@ -1437,6 +1438,32 @@ describe('QueryBuilder', () => { expect(qb4.getParams()).toEqual(['test']); }); + test('perf: insert', async () => { + const start = performance.now(); + for (let i = 1; i <= 10_000; i++) { + const qb = orm.em.createQueryBuilder(Publisher2); + qb.insert({ name: `test ${i}`, type: PublisherType.GLOBAL }).getKnexQuery(); + } + const took = performance.now() - start; + + if (took > 200) { + process.stdout.write(`insert test took ${took}\n`); + } + }); + + test('perf: update', async () => { + const start = performance.now(); + for (let i = 1; i <= 10_000; i++) { + const qb = orm.em.createQueryBuilder(Publisher2); + qb.update({ name: `test ${i}`, type: PublisherType.GLOBAL }).where({ id: 123 }).getKnexQuery(); + } + const took = performance.now() - start; + + if (took > 300) { + process.stdout.write(`update test took ${took}\n`); + } + }); + afterAll(async () => orm.close(true)); }); diff --git a/tests/bootstrap.ts b/tests/bootstrap.ts index e6c4bf832390..f754497fc114 100644 --- a/tests/bootstrap.ts +++ b/tests/bootstrap.ts @@ -1,5 +1,5 @@ import 'reflect-metadata'; -import { Configuration, EntityManager, JavaScriptMetadataProvider, MikroORM, Options, Utils } from '@mikro-orm/core'; +import { EntityManager, JavaScriptMetadataProvider, MikroORM, Options, Utils } from '@mikro-orm/core'; import { AbstractSqlDriver, SchemaGenerator, SqlEntityManager, SqlEntityRepository } from '@mikro-orm/knex'; import { SqliteDriver } from '@mikro-orm/sqlite'; import { MongoDriver } from '@mikro-orm/mongodb';