Skip to content

Commit

Permalink
refactor: move entity diffing to separate class
Browse files Browse the repository at this point in the history
  • Loading branch information
B4nan committed Sep 10, 2020
1 parent fd4e5fb commit 064f490
Show file tree
Hide file tree
Showing 13 changed files with 155 additions and 129 deletions.
8 changes: 7 additions & 1 deletion packages/core/src/EntityManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { LoadStrategy, LockMode, QueryOrderMap, ReferenceType, SCALAR_TYPES } fr
import { MetadataStorage } from './metadata';
import { Transaction } from './connections';
import { EventManager } from './events';
import { EntityComparator } from './utils/EntityComparator';
import { OptimisticLockError, ValidationError } from './errors';

/**
Expand All @@ -24,6 +25,7 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {
private readonly entityLoader: EntityLoader = new EntityLoader(this);
private readonly unitOfWork = new UnitOfWork(this);
private readonly entityFactory = new EntityFactory(this.unitOfWork, this);
private readonly comparator = new EntityComparator(this.metadata, this.driver.getPlatform());
private filters: Dictionary<FilterDef<any>> = {};
private filterParams: Dictionary<Dictionary> = {};
private transactionContext?: Transaction;
Expand Down Expand Up @@ -332,7 +334,7 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {

if (data === undefined) {
entityName = entityNameOrEntity.constructor.name;
data = Utils.prepareEntity(entityNameOrEntity as T, this.metadata, this.driver.getPlatform());
data = this.comparator.prepareEntity(entityNameOrEntity as T);
} else {
entityName = Utils.className(entityNameOrEntity as EntityName<T>);
}
Expand Down Expand Up @@ -680,6 +682,10 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {
return this.metadata;
}

getComparator(): EntityComparator {
return this.comparator;
}

private checkLockRequirements(mode: LockMode | undefined, meta: EntityMetadata): void {
if (!mode) {
return;
Expand Down
7 changes: 5 additions & 2 deletions packages/core/src/unit-of-work/ChangeSetComputer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import { ChangeSet, ChangeSetType } from './ChangeSet';
import { Collection, EntityIdentifier, EntityValidator } from '../entity';
import { Platform } from '../platforms';
import { ReferenceType } from '../enums';
import { EntityComparator } from '../utils/EntityComparator';

export class ChangeSetComputer {

private readonly comparator = new EntityComparator(this.metadata, this.platform);

constructor(private readonly validator: EntityValidator,
private readonly originalEntityData: Map<string, EntityData<AnyEntity>>,
private readonly identifierMap: Map<string, EntityIdentifier>,
Expand Down Expand Up @@ -48,10 +51,10 @@ export class ChangeSetComputer {

private computePayload<T extends AnyEntity<T>>(entity: T): EntityData<T> {
if (this.originalEntityData.get(entity.__helper!.__uuid)) {
return Utils.diffEntities<T>(this.originalEntityData.get(entity.__helper!.__uuid) as T, entity, this.metadata, this.platform);
return this.comparator.diffEntities<T>(this.originalEntityData.get(entity.__helper!.__uuid) as T, entity);
}

return Utils.prepareEntity(entity, this.metadata, this.platform);
return this.comparator.prepareEntity(entity);
}

private processReference<T extends AnyEntity<T>>(changeSet: ChangeSet<T>, prop: EntityProperty<T>): void {
Expand Down
22 changes: 11 additions & 11 deletions packages/core/src/unit-of-work/UnitOfWork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ChangeSet, ChangeSetType } from './ChangeSet';
import { ChangeSetComputer } from './ChangeSetComputer';
import { ChangeSetPersister } from './ChangeSetPersister';
import { CommitOrderCalculator } from './CommitOrderCalculator';
import { Utils } from '../utils';
import { Utils } from '../utils/Utils';
import { EntityManager } from '../EntityManager';
import { EventType, Cascade, LockMode, ReferenceType } from '../enums';
import { ValidationError, OptimisticLockError } from '../errors';
Expand Down Expand Up @@ -52,7 +52,7 @@ export class UnitOfWork {
this.identityMap.set(`${root.name}-${wrapped.__serializedPrimaryKey}`, entity);

if (mergeData || !this.originalEntityData.has(entity.__helper!.__uuid)) {
this.originalEntityData.set(entity.__helper!.__uuid, Utils.prepareEntity(entity, this.metadata, this.platform));
this.originalEntityData.set(entity.__helper!.__uuid, this.em.getComparator().prepareEntity(entity));
}

this.cascade(entity, Cascade.MERGE, visited, { mergeData: false });
Expand Down Expand Up @@ -117,7 +117,7 @@ export class UnitOfWork {
this.initIdentifier(entity);
this.changeSets.push(cs);
this.persistStack.delete(entity);
this.originalEntityData.set(entity.__helper!.__uuid, Utils.prepareEntity(entity, this.metadata, this.platform));
this.originalEntityData.set(entity.__helper!.__uuid, this.em.getComparator().prepareEntity(entity));
}

recomputeSingleChangeSet<T extends AnyEntity<T>>(entity: T): void {
Expand All @@ -131,7 +131,7 @@ export class UnitOfWork {

if (cs) {
Object.assign(this.changeSets[idx].payload, cs.payload);
this.originalEntityData.set(entity.__helper!.__uuid, Utils.prepareEntity(entity, this.metadata, this.platform));
this.originalEntityData.set(entity.__helper!.__uuid, this.em.getComparator().prepareEntity(entity));
}
}

Expand Down Expand Up @@ -282,7 +282,7 @@ export class UnitOfWork {
if (changeSet) {
this.changeSets.push(changeSet);
this.persistStack.delete(entity);
this.originalEntityData.set(wrapped.__uuid, Utils.prepareEntity(entity, this.metadata, this.platform));
this.originalEntityData.set(wrapped.__uuid, this.em.getComparator().prepareEntity(entity));
}
}

Expand Down Expand Up @@ -515,9 +515,9 @@ export class UnitOfWork {
}
});

const copy = Utils.prepareEntity(changeSet.entity, this.metadata, this.platform) as T;
const copy = this.em.getComparator().prepareEntity(changeSet.entity) as T;
await this.runHooks(EventType.beforeCreate, changeSet);
Object.assign(changeSet.payload, Utils.diffEntities<T>(copy, changeSet.entity, this.metadata, this.platform));
Object.assign(changeSet.payload, this.em.getComparator().diffEntities<T>(copy, changeSet.entity));
}

await this.changeSetPersister.executeInserts(changeSets, ctx);
Expand All @@ -534,9 +534,9 @@ export class UnitOfWork {
}

for (const changeSet of changeSets) {
const copy = Utils.prepareEntity(changeSet.entity, this.metadata, this.platform) as T;
const copy = this.em.getComparator().prepareEntity(changeSet.entity) as T;
await this.runHooks(EventType.beforeUpdate, changeSet);
Object.assign(changeSet.payload, Utils.diffEntities<T>(copy, changeSet.entity, this.metadata, this.platform));
Object.assign(changeSet.payload, this.em.getComparator().diffEntities<T>(copy, changeSet.entity));
}

await this.changeSetPersister.executeUpdates(changeSets, ctx);
Expand All @@ -553,9 +553,9 @@ export class UnitOfWork {
}

for (const changeSet of changeSets) {
const copy = Utils.prepareEntity(changeSet.entity, this.metadata, this.platform) as T;
const copy = this.em.getComparator().prepareEntity(changeSet.entity) as T;
await this.runHooks(EventType.beforeDelete, changeSet);
Object.assign(changeSet.payload, Utils.diffEntities<T>(copy, changeSet.entity, this.metadata, this.platform));
Object.assign(changeSet.payload, this.em.getComparator().diffEntities<T>(copy, changeSet.entity));
}

await this.changeSetPersister.executeDeletes(changeSets, ctx);
Expand Down
92 changes: 92 additions & 0 deletions packages/core/src/utils/EntityComparator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { AnyEntity, Dictionary, EntityData, EntityMetadata, EntityProperty, IMetadataStorage } from '../typings';
import { ReferenceType } from '../enums';
import { Platform } from '../platforms';
import { Utils } from './Utils';

export class EntityComparator {

constructor(private readonly metadata: IMetadataStorage,
private readonly platform: Platform) { }

/**
* Computes difference between two entities. First calls `prepareEntity` on both, then uses the `diff` method.
*/
diffEntities<T extends AnyEntity<T>>(a: T, b: T): EntityData<T> {
return Utils.diff(this.prepareEntity(a), this.prepareEntity(b)) as EntityData<T>;
}

/**
* Removes ORM specific code from entities and prepares it for serializing. Used before change set computation.
* References will be mapped to primary keys, collections to arrays of primary keys.
*/
prepareEntity<T extends AnyEntity<T>>(entity: T): EntityData<T> {
if ((entity as Dictionary).__prepared) {
return entity;
}

const meta = this.metadata.get<T>(entity.constructor.name);
const root = Utils.getRootEntity(this.metadata, meta);
const ret = {} as EntityData<T>;

if (meta.discriminatorValue) {
ret[root.discriminatorColumn as keyof T] = meta.discriminatorValue as unknown as T[keyof T];
}

// copy all props, ignore collections and references, process custom types
Object.values<EntityProperty<T>>(meta.properties).forEach(prop => {
if (this.shouldIgnoreProperty(entity, prop, root)) {
return;
}

if (prop.reference === ReferenceType.EMBEDDED) {
return Object.values<EntityProperty>(meta.properties).filter(p => p.embedded?.[0] === prop.name).forEach(childProp => {
ret[childProp.name as keyof T] = entity[prop.name][childProp.embedded![1]];
});
}

if (Utils.isEntity(entity[prop.name], true)) {
ret[prop.name] = Utils.getPrimaryKeyValues(entity[prop.name], this.metadata.find(prop.type)!.primaryKeys, true);

if (prop.customType) {
return ret[prop.name] = prop.customType.convertToDatabaseValue(ret[prop.name], this.platform);
}

return;
}

if (prop.customType) {
return ret[prop.name] = prop.customType.convertToDatabaseValue(entity[prop.name], this.platform);
}

if (Array.isArray(entity[prop.name]) || Utils.isObject(entity[prop.name])) {
return ret[prop.name] = Utils.copy(entity[prop.name]);
}

ret[prop.name] = entity[prop.name];
});

Object.defineProperty(ret, '__prepared', { value: true });

return ret;
}

private shouldIgnoreProperty<T extends AnyEntity<T>>(entity: T, prop: EntityProperty<T>, root: EntityMetadata) {
if (!(prop.name in entity) || prop.persist === false) {
return true;
}

const value = entity[prop.name];
const collection = Utils.isCollection(value);
const noPkRef = Utils.isEntity<T>(value, true) && !value.__helper!.__primaryKeys.every(pk => Utils.isDefined(pk, true));
const noPkProp = prop.primary && !Utils.isDefined(value, true);
const inverse = prop.reference === ReferenceType.ONE_TO_ONE && !prop.owner;
const discriminator = prop.name === root.discriminatorColumn;

// bidirectional 1:1 and m:1 fields are defined as setters, we need to check for `undefined` explicitly
const isSetter = [ReferenceType.ONE_TO_ONE, ReferenceType.MANY_TO_ONE].includes(prop.reference) && (prop.inversedBy || prop.mappedBy);
const emptyRef = isSetter && value === undefined;

return collection || noPkProp || noPkRef || inverse || discriminator || emptyRef;
}

}
83 changes: 1 addition & 82 deletions packages/core/src/utils/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { pathExists } from 'fs-extra';
import { createHash } from 'crypto';
import { recovery } from 'escaya';

import { AnyEntity, Dictionary, EntityData, EntityMetadata, EntityName, EntityProperty, Primary, IMetadataStorage } from '../typings';
import { AnyEntity, Dictionary, EntityMetadata, EntityName, EntityProperty, Primary, IMetadataStorage } from '../typings';
import { GroupOperator, ReferenceType, QueryOperator } from '../enums';
import { Collection } from '../entity';
import { Platform } from '../platforms';
Expand Down Expand Up @@ -119,87 +119,6 @@ export class Utils {
return ret;
}

/**
* Computes difference between two entities. First calls `prepareEntity` on both, then uses the `diff` method.
*/
static diffEntities<T extends AnyEntity<T>>(a: T, b: T, metadata: IMetadataStorage, platform: Platform): EntityData<T> {
return Utils.diff(Utils.prepareEntity(a, metadata, platform), Utils.prepareEntity(b, metadata, platform)) as EntityData<T>;
}

/**
* Removes ORM specific code from entities and prepares it for serializing. Used before change set computation.
* References will be mapped to primary keys, collections to arrays of primary keys.
*/
static prepareEntity<T extends AnyEntity<T>>(entity: T, metadata: IMetadataStorage, platform: Platform): EntityData<T> {
if ((entity as Dictionary).__prepared) {
return entity;
}

const meta = metadata.get<T>(entity.constructor.name);
const root = Utils.getRootEntity(metadata, meta);
const ret = {} as EntityData<T>;

if (meta.discriminatorValue) {
ret[root.discriminatorColumn as keyof T] = meta.discriminatorValue as unknown as T[keyof T];
}

// copy all props, ignore collections and references, process custom types
Object.values<EntityProperty<T>>(meta.properties).forEach(prop => {
if (Utils.shouldIgnoreProperty(entity, prop, root)) {
return;
}

if (prop.reference === ReferenceType.EMBEDDED) {
return Object.values<EntityProperty>(meta.properties).filter(p => p.embedded?.[0] === prop.name).forEach(childProp => {
ret[childProp.name as keyof T] = entity[prop.name][childProp.embedded![1]];
});
}

if (Utils.isEntity(entity[prop.name], true)) {
ret[prop.name] = Utils.getPrimaryKeyValues(entity[prop.name], metadata.find(prop.type)!.primaryKeys, true);

if (prop.customType) {
return ret[prop.name] = prop.customType.convertToDatabaseValue(ret[prop.name], platform);
}

return;
}

if (prop.customType) {
return ret[prop.name] = prop.customType.convertToDatabaseValue(entity[prop.name], platform);
}

if (Array.isArray(entity[prop.name]) || Utils.isObject(entity[prop.name])) {
return ret[prop.name] = Utils.copy(entity[prop.name]);
}

ret[prop.name] = entity[prop.name];
});

Object.defineProperty(ret, '__prepared', { value: true });

return ret;
}

private static shouldIgnoreProperty<T extends AnyEntity<T>>(entity: T, prop: EntityProperty<T>, root: EntityMetadata) {
if (!(prop.name in entity) || prop.persist === false) {
return true;
}

const value = entity[prop.name];
const collection = Utils.isCollection(value);
const noPkRef = Utils.isEntity<T>(value, true) && !value.__helper!.__primaryKeys.every(pk => Utils.isDefined(pk, true));
const noPkProp = prop.primary && !Utils.isDefined(value, true);
const inverse = prop.reference === ReferenceType.ONE_TO_ONE && !prop.owner;
const discriminator = prop.name === root.discriminatorColumn;

// bidirectional 1:1 and m:1 fields are defined as setters, we need to check for `undefined` explicitly
const isSetter = [ReferenceType.ONE_TO_ONE, ReferenceType.MANY_TO_ONE].includes(prop.reference) && (prop.inversedBy || prop.mappedBy);
const emptyRef = isSetter && value === undefined;

return collection || noPkProp || noPkRef || inverse || discriminator || emptyRef;
}

/**
* Creates deep copy of given entity.
*/
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './Utils';
export * from './RequestContext';
export * from './QueryHelper';
export * from './NullHighlighter';
export * from './EntityComparator';
40 changes: 22 additions & 18 deletions packages/knex/src/query/CriteriaNodeFactory.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Dictionary, MetadataStorage, ReferenceType, Utils, ValidationError } from '@mikro-orm/core';
import { Dictionary, EntityMetadata, MetadataStorage, ReferenceType, Utils, ValidationError } from '@mikro-orm/core';
import { ObjectCriteriaNode } from './ObjectCriteriaNode';
import { ArrayCriteriaNode } from './ArrayCriteriaNode';
import { ScalarCriteriaNode } from './ScalarCriteriaNode';
Expand Down Expand Up @@ -45,29 +45,33 @@ export class CriteriaNodeFactory {

const node = new ObjectCriteriaNode(metadata, entityName, parent, key);
node.payload = Object.keys(payload).reduce((o, item) => {
const prop = meta?.properties[item];
o[item] = this.createObjectItemNode(metadata, entityName, node, payload, item, meta);
return o;
}, {});

return node;
}

if (prop?.reference === ReferenceType.EMBEDDED) {
const operator = Object.keys(payload[item]).some(f => Utils.isOperator(f));
static createObjectItemNode(metadata: MetadataStorage, entityName: string, node: ICriteriaNode, payload: Dictionary, item: string, meta?: EntityMetadata) {
const prop = meta?.properties[item];

if (operator) {
throw ValidationError.cannotUseOperatorsInsideEmbeddables(entityName, prop.name, payload);
}
if (prop?.reference !== ReferenceType.EMBEDDED) {
const childEntity = prop && prop.reference !== ReferenceType.SCALAR ? prop.type : entityName;
return this.createNode(metadata, childEntity, payload[item], node, item);
}

const map = Object.keys(payload[item]).reduce((oo, k) => {
oo[prop.embeddedProps[k].name] = payload[item][k];
return oo;
}, {});
o[item] = this.createNode(metadata, entityName, map, node, item);
} else {
const childEntity = prop && prop.reference !== ReferenceType.SCALAR ? prop.type : entityName;
o[item] = this.createNode(metadata, childEntity, payload[item], node, item);
}
const operator = Object.keys(payload[item]).some(f => Utils.isOperator(f));

return o;
if (operator) {
throw ValidationError.cannotUseOperatorsInsideEmbeddables(entityName, prop.name, payload);
}

const map = Object.keys(payload[item]).reduce((oo, k) => {
oo[prop.embeddedProps[k].name] = payload[item][k];
return oo;
}, {});

return node;
return this.createNode(metadata, entityName, map, node, item);
}

}
Loading

0 comments on commit 064f490

Please sign in to comment.