Skip to content

Commit

Permalink
perf(core): optimize QB for simple cases
Browse files Browse the repository at this point in the history
Related: #732
  • Loading branch information
B4nan committed Aug 13, 2020
1 parent 3da5c43 commit 99cfca7
Show file tree
Hide file tree
Showing 7 changed files with 53 additions and 23 deletions.
4 changes: 2 additions & 2 deletions packages/core/src/EntityManager.ts
Expand Up @@ -322,7 +322,7 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {
*/
async nativeInsert<T extends AnyEntity<T>>(entityName: EntityName<T>, data: EntityData<T>): Promise<Primary<T>> {
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);

Expand All @@ -334,7 +334,7 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {
*/
async nativeUpdate<T extends AnyEntity<T>>(entityName: EntityName<T>, where: FilterQuery<T>, data: EntityData<T>, options: UpdateOptions<T> = {}): Promise<number> {
entityName = Utils.className(entityName);
data = QueryHelper.processParams(data);
data = QueryHelper.processObjectParams(data);
where = QueryHelper.processWhere(where as FilterQuery<T>, entityName, this.metadata);
where = await this.applyFilters(entityName, where, options.filters ?? {}, 'update');
this.validator.validateParams(data, 'update data');
Expand Down
12 changes: 9 additions & 3 deletions packages/core/src/utils/QueryHelper.ts
Expand Up @@ -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<T extends AnyEntity<T>>(where: Dictionary, meta: EntityMetadata<T>, metadata: MetadataStorage, key?: string): boolean {
if (Array.isArray(where)) {
where.forEach((item, i) => {
Expand Down
15 changes: 1 addition & 14 deletions packages/core/src/utils/Utils.ts
Expand Up @@ -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');
}

/**
Expand Down
8 changes: 8 additions & 0 deletions packages/knex/src/query/ObjectCriteriaNode.ts
Expand Up @@ -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];

Expand Down
8 changes: 5 additions & 3 deletions packages/knex/src/query/QueryBuilder.ts
Expand Up @@ -104,11 +104,11 @@ export class QueryBuilder<T extends AnyEntity<T> = AnyEntity> {
where(cond: QBFilterQuery<T>, operator?: keyof typeof GroupOperator): this;
where(cond: string, params?: any[], operator?: keyof typeof GroupOperator): this;
where(cond: QBFilterQuery<T> | 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;
Expand Down Expand Up @@ -493,7 +493,9 @@ export class QueryBuilder<T extends AnyEntity<T> = 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) {
Expand Down
27 changes: 27 additions & 0 deletions tests/QueryBuilder.test.ts
Expand Up @@ -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', () => {

Expand Down Expand Up @@ -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));

});
2 changes: 1 addition & 1 deletion 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';
Expand Down

0 comments on commit 99cfca7

Please sign in to comment.