Skip to content

Commit

Permalink
feat(core): implement auto-flush mode
Browse files Browse the repository at this point in the history
WIP

Closes #2359
  • Loading branch information
B4nan committed Dec 3, 2021
1 parent 84b5d1b commit dd0a27c
Show file tree
Hide file tree
Showing 24 changed files with 358 additions and 113 deletions.
25 changes: 22 additions & 3 deletions packages/core/src/EntityManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { UnitOfWork } from './unit-of-work';
import type { CountOptions, DeleteOptions, EntityManagerType, FindOneOptions, FindOneOrFailOptions, FindOptions, IDatabaseDriver, InsertOptions, LockOptions, UpdateOptions, GetReferenceOptions } from './drivers';
import type { AnyEntity, AutoPath, Dictionary, EntityData, EntityDictionary, EntityDTO, EntityMetadata, EntityName, FilterDef, FilterQuery, GetRepository, Loaded, New, Populate, PopulateOptions, Primary } from './typings';
import type { IsolationLevel } from './enums';
import { LoadStrategy, LockMode, ReferenceType, SCALAR_TYPES } from './enums';
import { FlushMode, LoadStrategy, LockMode, ReferenceType, SCALAR_TYPES } from './enums';
import type { MetadataStorage } from './metadata';
import type { Transaction } from './connections';
import { EventManager, TransactionEventBroadcaster } from './events';
Expand Down Expand Up @@ -93,7 +93,9 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {
* Finds all entities matching your `where` query. You can pass additional options via the `options` parameter.
*/
async find<T extends AnyEntity<T>, P extends string = never>(entityName: EntityName<T>, where: FilterQuery<T>, options: FindOptions<T, P> = {}): Promise<Loaded<T, P>[]> {
if (options.disableIdentityMap) {
if (!options.disableIdentityMap) {
await this.tryFlush(entityName, options);
} else {
const fork = this.fork({ clear: false });
const ret = await fork.find<T, P>(entityName, where, { ...options, disableIdentityMap: false });
fork.clear();
Expand Down Expand Up @@ -277,7 +279,9 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {
* Finds first entity matching your `where` query.
*/
async findOne<T extends AnyEntity<T>, P extends string = never>(entityName: EntityName<T>, where: FilterQuery<T>, options: FindOneOptions<T, P> = {}): Promise<Loaded<T, P> | null> {
if (options.disableIdentityMap) {
if (!options.disableIdentityMap) {
await this.tryFlush(entityName, options);
} else {
const fork = this.fork({ clear: false });
const ret = await fork.findOne<T, P>(entityName, where, { ...options, disableIdentityMap: false });
fork.clear();
Expand Down Expand Up @@ -378,6 +382,7 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {
async rollback(): Promise<void> {
await this.getConnection('write').rollback(this.transactionContext, new TransactionEventBroadcaster(this));
delete this.transactionContext;
this.getUnitOfWork().clearActionsQueue();
}

/**
Expand Down Expand Up @@ -677,6 +682,20 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {
await this.getUnitOfWork().commit();
}

protected async tryFlush<T>(entityName: EntityName<T>, options: { flushMode?: FlushMode }): Promise<void> {
const flushMode = options.flushMode ?? this.config.get('flushMode');
entityName = Utils.className(entityName);
const meta = this.metadata.get(entityName);

if (flushMode === FlushMode.COMMIT) {
return;
}

if (flushMode === FlushMode.ALWAYS || this.getUnitOfWork().shouldAutoFlush(meta)) {
await this.flush();
}
}

/**
* Clears the EntityManager. All entities that are currently managed by this EntityManager become detached.
*/
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/drivers/IDatabaseDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type {
IPrimaryKey, PopulateOptions, EntityDictionary, ExpandProperty, AutoPath,
} from '../typings';
import type { Connection, QueryResult, Transaction } from '../connections';
import type { LockMode, QueryOrderMap, QueryFlag, LoadStrategy } from '../enums';
import type { FlushMode, LockMode, QueryOrderMap, QueryFlag, LoadStrategy } from '../enums';
import type { Platform } from '../platforms';
import type { MetadataStorage } from '../metadata';
import type { Collection } from '../entity';
Expand Down Expand Up @@ -100,6 +100,7 @@ export interface FindOptions<T, P extends string = never> {
groupBy?: string | string[];
having?: QBFilterQuery<T>;
strategy?: LoadStrategy;
flushMode?: FlushMode;
filters?: Dictionary<boolean | Dictionary> | string[] | boolean;
lockMode?: Exclude<LockMode, LockMode.OPTIMISTIC>;
lockTableAliases?: string[];
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/entity/BaseEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ export abstract class BaseEntity<T, PK extends keyof T, P extends string = never
return (this as unknown as AnyEntity<T>).__helper!.__initialized;
}

isTouched(): boolean {
return (this as unknown as AnyEntity<T>).__helper!.__touched;
}

populated(populated = true): void {
(this as unknown as AnyEntity<T>).__helper!.populated(populated);
}
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/entity/EntityFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export class EntityFactory {
const entity = exists ?? this.createEntity<T>(data, meta2, options);
entity.__helper!.__initialized = options.initialized;
this.hydrate(entity, meta2, data, options);
entity.__helper!.__touched = false;

if (options.merge && entity.__helper!.hasPrimaryKey()) {
this.unitOfWork.registerManaged(entity, data, options.refresh && options.initialized, options.newEntity);
Expand Down Expand Up @@ -113,6 +114,8 @@ export class EntityFactory {
this.create(prop.type, data[prop.name] as EntityData<T>, options); // we can ignore the value, we just care about the `mergeData` call
}
});

entity.__helper!.__touched = false;
}

createReference<T>(entityName: EntityName<T>, id: Primary<T> | Primary<T>[] | Record<string, Primary<T>>, options: Pick<FactoryOptions, 'merge' | 'convertCustomTypes' | 'schema'> = {}): T {
Expand Down
62 changes: 38 additions & 24 deletions packages/core/src/entity/EntityHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,9 @@ export class EntityHelper {
}

EntityHelper.defineBaseProperties(meta, meta.prototype, fork);
EntityHelper.defineProperties(meta);
const prototype = meta.prototype as Dictionary;

if (em.config.get('propagateToOneOwner')) {
EntityHelper.defineReferenceProperties(meta);
}

if (!prototype.toJSON) { // toJSON can be overridden
prototype.toJSON = function (this: T, ...args: any[]) {
return EntityTransformer.toObject<T>(this, ...args.slice(meta.toJsonParams.length));
Expand All @@ -55,6 +52,12 @@ export class EntityHelper {
});
}

/**
* As a performance optimization, we create entity state methods in a lazy manner. We first add
* the `null` value to the prototype to reserve space in memory. Then we define a setter on the
* prototype, that will be executed exactly once per entity instance. There we redefine given
* property on the entity instance, so shadowing the prototype setter.
*/
private static defineBaseProperties<T extends AnyEntity<T>>(meta: EntityMetadata<T>, prototype: T, em: EntityManager) {
const helperParams = meta.embeddable ? [] : [em.getComparator().getPkGetter(meta), em.getComparator().getPkSerializer(meta), em.getComparator().getPkGetterConverted(meta)];
Object.defineProperties(prototype, {
Expand Down Expand Up @@ -84,18 +87,14 @@ export class EntityHelper {
* First defines a setter on the prototype, once called, actual get/set handlers are registered on the instance rather
* than on its prototype. Thanks to this we still have those properties enumerable (e.g. part of `Object.keys(entity)`).
*/
private static defineReferenceProperties<T extends AnyEntity<T>>(meta: EntityMetadata<T>): void {
private static defineProperties<T extends AnyEntity<T>>(meta: EntityMetadata<T>): void {
Object
.values<EntityProperty>(meta.properties)
.filter(prop => [ReferenceType.ONE_TO_ONE, ReferenceType.MANY_TO_ONE].includes(prop.reference) && (prop.inversedBy || prop.mappedBy) && !prop.mapToPk)
.filter(prop => prop.persist !== false)
.forEach(prop => {
Object.defineProperty(meta.prototype, prop.name, {
set(val: AnyEntity) {
if (!('__data' in this)) {
Object.defineProperty(this, '__data', { value: {} });
}

EntityHelper.defineReferenceProperty(meta, prop, this, val);
EntityHelper.defineProperty(meta, prop, this, val);
},
});
});
Expand All @@ -118,19 +117,34 @@ export class EntityHelper {
}
}

private static defineReferenceProperty<T extends AnyEntity<T>>(meta: EntityMetadata<T>, prop: EntityProperty<T>, ref: T, val: AnyEntity): void {
Object.defineProperty(ref, prop.name, {
get() {
return this.__data[prop.name];
},
set(val: AnyEntity | Reference<AnyEntity>) {
const entity = Reference.unwrapReference(val ?? this.__data[prop.name]);
this.__data[prop.name] = Reference.wrapReference(val as T, prop);
EntityHelper.propagate(meta, entity, this, prop, Reference.unwrapReference(val));
},
enumerable: true,
configurable: true,
});
private static defineProperty<T extends AnyEntity<T>>(meta: EntityMetadata<T>, prop: EntityProperty<T>, ref: T, val: unknown): void {
if ([ReferenceType.ONE_TO_ONE, ReferenceType.MANY_TO_ONE].includes(prop.reference) && (prop.inversedBy || prop.mappedBy) && !prop.mapToPk) {
Object.defineProperty(ref, prop.name, {
get() {
return this.__helper.__data[prop.name];
},
set(val: AnyEntity | Reference<AnyEntity>) {
const entity = Reference.unwrapReference(val ?? this.__helper.__data[prop.name]);
this.__helper.__data[prop.name] = Reference.wrapReference(val as T, prop);
this.__helper.__touched = true;
EntityHelper.propagate(meta, entity, this, prop, Reference.unwrapReference(val));
},
enumerable: true,
configurable: true,
});
} else {
Object.defineProperty(ref, prop.name, {
get() {
return this.__helper.__data[prop.name];
},
set(val: unknown) {
this.__helper.__data[prop.name] = val;
this.__helper.__touched = true;
},
enumerable: true,
configurable: true,
});
}
ref[prop.name] = val as T[string & keyof T];
}

Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/entity/WrappedEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ import type { EntityIdentifier } from './EntityIdentifier';
export class WrappedEntity<T extends AnyEntity<T>, PK extends keyof T> {

__initialized = true;
__touched = false;
__populated?: boolean;
__lazyInitialized?: boolean;
__managed?: boolean;
__schema?: string;
__em?: EntityManager;
__serializationContext: { root?: SerializationContext<T>; populate?: PopulateOptions<T>[] } = {};
__loadedProperties = new Set<string>();
__data: Dictionary = {};

/** holds last entity data snapshot so we can compute changes when persisting managed entities */
__originalEntityData?: EntityData<T>;
Expand All @@ -38,6 +40,10 @@ export class WrappedEntity<T extends AnyEntity<T>, PK extends keyof T> {
return this.__initialized;
}

isTouched(): boolean {
return this.__touched;
}

populated(populated = true): void {
this.__populated = populated;
this.__lazyInitialized = false;
Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/enums.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import type { ExpandProperty } from './typings';

export const enum FlushMode {
/** The `EntityManager` tries to delay the flush until the current Transaction is committed, although it might flush prematurely too. */
COMMIT,
/** This is the default mode and it flushes the `EntityManager` only if necessary. */
AUTO,
/** Flushes the `EntityManager` before every query. */
ALWAYS,
}

export enum GroupOperator {
$and = 'and',
$or = 'or',
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/typings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export type QBFilterQuery<T = any> = FilterQuery<T> | Dictionary;

export interface IWrappedEntity<T extends AnyEntity<T>, PK extends keyof T | unknown = PrimaryProperty<T>, P extends string = string> {
isInitialized(): boolean;
isTouched(): boolean;
populated(populated?: boolean): void;
init<P extends Populate<T> = Populate<T>>(populated?: boolean, populate?: P, lockMode?: LockMode): Promise<T>;
toReference<PK2 extends PK | unknown = PrimaryProperty<T>, P2 extends string = string>(): IdentifiedReference<T, PK2> & LoadedReference<T>;
Expand All @@ -115,6 +116,7 @@ export interface IWrappedEntityInternal<T, PK extends keyof T | unknown = Primar
__platform: Platform;
__factory: EntityFactory; // internal factory instance that has its own global fork
__initialized: boolean;
__touched: boolean;
__originalEntityData?: EntityData<T>;
__loadedProperties: Set<string>;
__identifier?: EntityIdentifier;
Expand Down
Loading

0 comments on commit dd0a27c

Please sign in to comment.