entity persist operation builder refactoring

This commit is contained in:
Umed Khudoiberdiev 2016-03-14 10:19:42 +05:00
parent a5956a5f62
commit b9d9f19995
7 changed files with 178 additions and 208 deletions

View File

@ -60,41 +60,161 @@ export class EntityPersistOperationBuilder {
difference(metadata: EntityMetadata, entity1: any, entity2: any): PersistOperation {
const dbEntities = this.extractObjectsById(entity1, metadata);
const allEntities = this.extractObjectsById(entity2, metadata);
const insertOperations = this.findCascadeInsertedEntities(metadata, entity2, dbEntities, null);
const removeOperations = this.findCascadeRemovedEntities(metadata, entity1, allEntities);
const updateOperations = this.findCascadeUpdateEntities(metadata, entity1, entity2, null);
const junctionInsertOperations = this.findJunctionInsertOperations(metadata, entity2, dbEntities);
const junctionRemoveOperations = this.findJunctionRemoveOperations(metadata, entity1, allEntities);
const updatesByRelationsOperations = this.updateRelations(insertOperations, entity2);
// todo: implement duplicates removal later
// remove duplicates from inserted entities (todo: shall we check if duplicates differ and throw exception?)
/*const uniqueInsertOperations = insertOperations.map(insertOperation => {
const otherInsertOperations = insertOperations.filter(o => o !== insertOperation && o.entity === insertOperation.entity);
const otherRelations = otherInsertOperations.reduce((relations, o) => {
return relations.concat(o.relations);
}, <RelationMetadata[]> []);
return <InsertOperation> {
entity: insertOperation.entity,
relations: insertOperation.relations.concat(otherRelations)
};
});*/
// sort inserts
return new PersistOperation(
insertOperations,
removeOperations,
updateOperations,
junctionInsertOperations,
junctionRemoveOperations,
updatesByRelationsOperations
);
const persistOperation = new PersistOperation();
persistOperation.inserts = this.findCascadeInsertedEntities(entity2, dbEntities, null);
persistOperation.removes = this.findCascadeRemovedEntities(metadata, entity1, allEntities);
persistOperation.updates = this.findCascadeUpdateEntities(metadata, entity1, entity2, null);
persistOperation.junctionInserts = this.findJunctionInsertOperations(metadata, entity2, dbEntities);
persistOperation.junctionRemoves = this.findJunctionRemoveOperations(metadata, entity1, allEntities);
persistOperation.updatesByRelations = this.updateRelations(persistOperation.inserts, entity2);
return persistOperation;
}
// -------------------------------------------------------------------------
// Private Methods
// -------------------------------------------------------------------------
private findCascadeInsertedEntities(newEntity: any,
dbEntities: EntityWithId[],
fromRelation: RelationMetadata): InsertOperation[] {
const metadata = this.connection.getMetadata(newEntity.constructor);
const operations: InsertOperation[] = [];
const isObjectNew = !dbEntities.find(dbEntity => {
return dbEntity.id === newEntity[metadata.primaryColumn.name] && dbEntity.entity.constructor === metadata.target;
});
if (isObjectNew && fromRelation && !fromRelation.isCascadeInsert) {
if (this.strictCascadesMode) {
throw new Error("Cascade inserts are not allowed in " + metadata.name + "#" + fromRelation.propertyName);
} else {
return [];
}
}
if (isObjectNew)
operations.push(new InsertOperation(newEntity));
return metadata.relations
.filter(relation => !!newEntity[relation.propertyName])
.reduce((insertedEntities, relation) => {
const value = newEntity[relation.propertyName];
if (value instanceof Array) {
value.forEach((subEntity: any) => {
const subInserted = this.findCascadeInsertedEntities(subEntity, dbEntities, relation);
insertedEntities = insertedEntities.concat(subInserted);
});
} else {
const subInserted = this.findCascadeInsertedEntities(value, dbEntities, relation);
insertedEntities = insertedEntities.concat(subInserted);
}
return insertedEntities;
}, operations);
}
private findCascadeUpdateEntities(metadata: EntityMetadata, dbEntity: any, newEntity: any, fromRelation: RelationMetadata): UpdateOperation[] {
if (!dbEntity)
return [];
const updatedEntities: any[] = [];
const diff = this.diffColumns(metadata, newEntity, dbEntity);
if (diff.length && fromRelation && !fromRelation.isCascadeUpdate) {
if (this.strictCascadesMode) {
throw new Error("Cascade updates are not allowed in " + metadata.name + "#" + fromRelation.propertyName);
} else {
return [];
}
}
if (diff.length) {
updatedEntities.push(new UpdateOperation(newEntity, diff));
}
return metadata.relations
.filter(relation => newEntity[relation.propertyName] && dbEntity[relation.propertyName])
.reduce((updatedColumns, relation) => {
const relMetadata = relation.relatedEntityMetadata;
const relationIdColumnName = relMetadata.primaryColumn.name;
if (newEntity[relation.propertyName] instanceof Array) {
newEntity[relation.propertyName].forEach((subEntity: any) => {
const subDbEntity = (dbEntity[relation.propertyName] as any[]).find(subDbEntity => {
return subDbEntity[relationIdColumnName] === subEntity[relationIdColumnName];
});
if (subDbEntity) {
const relationUpdatedColumns = this.findCascadeUpdateEntities(relMetadata, subDbEntity, subEntity, relation);
updatedColumns = updatedColumns.concat(relationUpdatedColumns);
}
});
} else {
const relationUpdatedColumns = this.findCascadeUpdateEntities(relMetadata, dbEntity[relation.propertyName], newEntity[relation.propertyName], relation);
updatedColumns = updatedColumns.concat(relationUpdatedColumns);
}
return updatedColumns;
}, updatedEntities);
}
private findCascadeRemovedEntities(metadata: EntityMetadata, dbEntity: any, newEntities: EntityWithId[]): any[] {
if (!dbEntity)
return [];
return metadata.relations
.filter(relation => !!dbEntity[relation.propertyName])
.reduce((removedEntities, relation) => {
const relationIdColumnName = relation.relatedEntityMetadata.primaryColumn.name;
const relMetadata = relation.relatedEntityMetadata;
if (dbEntity[relation.propertyName] instanceof Array) { // todo: propertyName or name here?
dbEntity[relation.propertyName].forEach((subEntity: any) => {
const isObjectRemoved = !newEntities.find(newEntity => {
return newEntity.id === subEntity[relationIdColumnName] && newEntity.entity.constructor === relMetadata.target;
});
if (isObjectRemoved && relation.isCascadeRemove)
removedEntities.push({
entity: subEntity,
fromEntityId: dbEntity[metadata.primaryColumn.name],
metadata: metadata,
relation: relation
});
removedEntities = removedEntities.concat(this.findCascadeRemovedEntities(relMetadata, subEntity, newEntities));
});
} else {
const relationId = dbEntity[relation.propertyName][relationIdColumnName];
const isObjectRemoved = !newEntities.find(newEntity => {
return newEntity.id === relationId && newEntity.entity.constructor === relMetadata.target;
});
if (isObjectRemoved && relation.isCascadeRemove)
removedEntities.push({
entity: dbEntity[relation.propertyName],
fromEntityId: dbEntity[metadata.primaryColumn.name],
metadata: metadata,
relation: relation
});
removedEntities = removedEntities.concat(this.findCascadeRemovedEntities(relMetadata, dbEntity[relation.propertyName], newEntities));
}
return removedEntities;
}, []);
}
/**
* To update relation, you need:
* update table where this relation (owner side)
* set its relation property to inserted id
* where
*
*/
private updateRelations(insertOperations: InsertOperation[], newEntity: any): UpdateByRelationOperation[] {
return insertOperations.reduce((operations, insertOperation) => {
return operations.concat(this.findRelationsWithEntityInside(insertOperation, newEntity));
}, <UpdateByRelationOperation[]> []);
}
private findRelationsWithEntityInside(insertOperation: InsertOperation, entityToSearchIn: any) {
const metadata = this.connection.getMetadata(entityToSearchIn.constructor);
@ -121,24 +241,10 @@ export class EntityPersistOperationBuilder {
return operations;
}, <UpdateByRelationOperation[]> []);
}
/**
* To update relation, you need:
* update table where this relation (owner side)
* set its relation property to inserted id
* where
*
*/
private updateRelations(insertOperations: InsertOperation[], newEntity: any): UpdateByRelationOperation[] {
return insertOperations.reduce((operations, insertOperation) => {
return operations.concat(this.findRelationsWithEntityInside(insertOperation, newEntity));
}, <UpdateByRelationOperation[]> []);
}
private findJunctionInsertOperations(metadata: EntityMetadata, newEntity: any, dbEntities: EntityWithId[]): JunctionInsertOperation[] {
const dbEntity = dbEntities.find(dbEntity => {
return dbEntity.id === newEntity[metadata.primaryColumn.name] && dbEntity.entity.constructor.name === metadata.name;
return dbEntity.id === newEntity[metadata.primaryColumn.name] && dbEntity.entity.constructor === metadata.target;
});
return metadata.relations
.filter(relation => relation.isManyToMany)
@ -172,7 +278,7 @@ export class EntityPersistOperationBuilder {
return [];
const newEntity = newEntities.find(newEntity => {
return newEntity.id === dbEntity[metadata.primaryColumn.name] && newEntity.entity.constructor.name === metadata.name;
return newEntity.id === dbEntity[metadata.primaryColumn.name] && newEntity.entity.constructor === metadata.target;
});
return metadata.relations
.filter(relation => relation.isManyToMany)
@ -201,89 +307,6 @@ export class EntityPersistOperationBuilder {
}, <JunctionInsertOperation[]> []);
}
private findCascadeInsertedEntities(metadata: EntityMetadata, newEntity: any, dbEntities: EntityWithId[], fromRelation: RelationMetadata): any[] {
const insertedEntities: any[] = [];
const isObjectNew = !dbEntities.find(dbEntity => {
return dbEntity.id === newEntity[metadata.primaryColumn.name] && dbEntity.entity.constructor.name === metadata.name;
});
if (isObjectNew && fromRelation && !fromRelation.isCascadeInsert) {
if (this.strictCascadesMode) {
throw new Error("Cascade inserts are not allowed in " + metadata.name + "#" + fromRelation.propertyName);
} else {
return [];
}
}
if (isObjectNew)
insertedEntities.push({
entity: newEntity
});
return metadata.relations
.filter(relation => !!newEntity[relation.propertyName])
.reduce((insertedEntities, relation) => {
const relMetadata = relation.relatedEntityMetadata;
const value = newEntity[relation.propertyName];
if (value instanceof Array) {
value.forEach((subEntity: any) => {
const subInserted = this.findCascadeInsertedEntities(relMetadata, subEntity, dbEntities, relation);
insertedEntities = insertedEntities.concat(subInserted);
});
} else {
const subInserted = this.findCascadeInsertedEntities(relMetadata, value, dbEntities, relation);
insertedEntities = insertedEntities.concat(subInserted);
}
return insertedEntities;
}, insertedEntities);
}
private findCascadeRemovedEntities(metadata: EntityMetadata, dbEntity: any, newEntities: EntityWithId[]): any[] {
if (!dbEntity)
return [];
return metadata.relations
.filter(relation => !!dbEntity[relation.propertyName])
.reduce((removedEntities, relation) => {
const relationIdColumnName = relation.relatedEntityMetadata.primaryColumn.name;
const relMetadata = relation.relatedEntityMetadata;
if (dbEntity[relation.propertyName] instanceof Array) { // todo: propertyName or name here?
dbEntity[relation.propertyName].forEach((subEntity: any) => {
const isObjectRemoved = !newEntities.find(newEntity => {
return newEntity.id === subEntity[relationIdColumnName] && newEntity.entity.constructor.name === relMetadata.name;
});
if (isObjectRemoved && relation.isCascadeRemove)
removedEntities.push({
entity: subEntity,
fromEntityId: dbEntity[metadata.primaryColumn.name],
metadata: metadata,
relation: relation
});
removedEntities = removedEntities.concat(this.findCascadeRemovedEntities(relMetadata, subEntity, newEntities));
});
} else {
const relationId = dbEntity[relation.propertyName][relationIdColumnName];
const isObjectRemoved = !newEntities.find(newEntity => {
return newEntity.id === relationId && newEntity.entity.constructor.name === relMetadata.name;
});
if (isObjectRemoved && relation.isCascadeRemove)
removedEntities.push({
entity: dbEntity[relation.propertyName],
fromEntityId: dbEntity[metadata.primaryColumn.name],
metadata: metadata,
relation: relation
});
removedEntities = removedEntities.concat(this.findCascadeRemovedEntities(relMetadata, dbEntity[relation.propertyName], newEntities));
}
return removedEntities;
}, []);
}
/**
* Extracts unique objects from given entity and all its downside relations.
*/
@ -305,57 +328,11 @@ export class EntityPersistOperationBuilder {
.reduce((col1: any[], col2: any[]) => col1.concat(col2), []) // flatten
.concat([{
id: entity[metadata.primaryColumn.name],
entity: entity//.constructor.name
entity: entity
}])
.filter((entity: any, index: number, allEntities: any[]) => allEntities.indexOf(entity) === index); // unique
}
private findCascadeUpdateEntities(metadata: EntityMetadata, dbEntity: any, newEntity: any, fromRelation: RelationMetadata): UpdateOperation[] {
if (!dbEntity)
return [];
const updatedEntities: any[] = [];
const diff = this.diffColumns(metadata, newEntity, dbEntity);
if (diff.length && fromRelation && !fromRelation.isCascadeUpdate) {
if (this.strictCascadesMode) {
throw new Error("Cascade updates are not allowed in " + metadata.name + "#" + fromRelation.propertyName);
} else {
return [];
}
}
if (diff.length) {
updatedEntities.push({
entity: newEntity,
columns: diff
});
}
return metadata.relations
.filter(relation => newEntity[relation.propertyName] && dbEntity[relation.propertyName])
.reduce((updatedColumns, relation) => {
const relMetadata = relation.relatedEntityMetadata;
const relationIdColumnName = relMetadata.primaryColumn.name;
if (newEntity[relation.propertyName] instanceof Array) {
newEntity[relation.propertyName].forEach((subEntity: any) => {
const subDbEntity = (dbEntity[relation.propertyName] as any[]).find(subDbEntity => {
return subDbEntity[relationIdColumnName] === subEntity[relationIdColumnName];
});
if (subDbEntity) {
const relationUpdatedColumns = this.findCascadeUpdateEntities(relMetadata, subDbEntity, subEntity, relation);
updatedColumns = updatedColumns.concat(relationUpdatedColumns);
}
});
} else {
const relationUpdatedColumns = this.findCascadeUpdateEntities(relMetadata, dbEntity[relation.propertyName], newEntity[relation.propertyName], relation);
updatedColumns = updatedColumns.concat(relationUpdatedColumns);
}
return updatedColumns;
}, updatedEntities);
}
private diffColumns(metadata: EntityMetadata, newEntity: any, dbEntity: any) {
return metadata.columns
.filter(column => !column.isVirtual)

View File

@ -1,4 +1,5 @@
export interface InsertOperation {
entity: any;
entityId: number;
export class InsertOperation {
constructor(public entity: any,
public entityId?: number) {
}
}

View File

@ -1,7 +1,8 @@
import {EntityMetadata} from "../../metadata-builder/metadata/EntityMetadata";
export interface JunctionInsertOperation {
metadata: EntityMetadata;
entity1: any;
entity2: any;
export class JunctionInsertOperation {
constructor(public metadata: EntityMetadata,
public entity1: any,
public entity2: any) {
}
}

View File

@ -1,7 +1,8 @@
import {EntityMetadata} from "../../metadata-builder/metadata/EntityMetadata";
export interface JunctionRemoveOperation {
metadata: EntityMetadata;
entity1: any;
entity2: any;
export class JunctionRemoveOperation {
constructor(public metadata: EntityMetadata,
public entity1: any,
public entity2: any) {
}
}

View File

@ -6,6 +6,9 @@ import {JunctionRemoveOperation} from "./JunctionRemoveOperation";
import {UpdateByRelationOperation} from "./UpdateByRelationOperation";
export class PersistOperation {
// todo: what if we have two same entities in the insert operations?
inserts: InsertOperation[];
removes: RemoveOperation[];
updates: UpdateOperation[];
@ -13,21 +16,6 @@ export class PersistOperation {
junctionRemoves: JunctionRemoveOperation[];
updatesByRelations: UpdateByRelationOperation[];
constructor(inserts: InsertOperation[],
removes: RemoveOperation[],
updates: UpdateOperation[],
junctionInserts: JunctionInsertOperation[],
junctionRemoves: JunctionRemoveOperation[],
updatesByRelations: UpdateByRelationOperation[]) {
this.inserts = inserts;
this.removes = removes;
this.updates = updates;
this.junctionInserts = junctionInserts;
this.junctionRemoves = junctionRemoves;
this.updatesByRelations = updatesByRelations;
}
log() {
console.log("---------------------------------------------------------");
console.log("DB ENTITY");

View File

@ -1,9 +1,10 @@
import {RelationMetadata} from "../../metadata-builder/metadata/RelationMetadata";
import {EntityMetadata} from "../../metadata-builder/metadata/EntityMetadata";
export interface RemoveOperation {
entity: any;
fromEntityId: any;
metadata: EntityMetadata;
relation: RelationMetadata;
export class RemoveOperation {
constructor(public metadata: EntityMetadata,
public relation: RelationMetadata,
public entity: any,
public fromEntityId: any) {
}
}

View File

@ -1,6 +1,7 @@
import {ColumnMetadata} from "../../metadata-builder/metadata/ColumnMetadata";
export interface UpdateOperation {
entity: any;
columns: ColumnMetadata[];
export class UpdateOperation {
constructor(public entity: any,
public columns: ColumnMetadata[]) {
}
}