implemented remove operation

This commit is contained in:
Umed Khudoiberdiev 2016-03-21 00:59:19 +05:00
parent eb78d759d7
commit e3c7a5f42a
6 changed files with 237 additions and 88 deletions

View File

@ -1,7 +1,7 @@
import {EntityMetadata} from "../metadata-builder/metadata/EntityMetadata";
import {RelationMetadata} from "../metadata-builder/metadata/RelationMetadata";
import {Connection} from "../connection/Connection";
import {PersistOperation} from "./operation/PersistOperation";
import {PersistOperation, EntityWithId} from "./operation/PersistOperation";
import {InsertOperation} from "./operation/InsertOperation";
import {UpdateByRelationOperation} from "./operation/UpdateByRelationOperation";
import {JunctionInsertOperation} from "./operation/JunctionInsertOperation";
@ -9,11 +9,6 @@ import {UpdateOperation} from "./operation/UpdateOperation";
import {CascadesNotAllowedError} from "./error/CascadesNotAllowedError";
import {RemoveOperation} from "./operation/RemoveOperation";
interface EntityWithId {
id: any;
entity: any;
}
/**
* 1. collect all exist objects from the db entity
* 2. collect all objects from the new entity
@ -59,17 +54,39 @@ export class EntityPersistOperationBuilder {
/**
* Finds columns and relations from entity2 which does not exist or does not match in entity1.
*/
difference(metadata: EntityMetadata, entity1: any, entity2: any): PersistOperation {
const dbEntities = this.extractObjectsById(entity1, metadata);
const allEntities = this.extractObjectsById(entity2, metadata);
buildFullPersistment(metadata: EntityMetadata, dbEntity: any, persistedEntity: any): PersistOperation {
const dbEntities = this.extractObjectsById(dbEntity, metadata);
const allPersistedEntities = this.extractObjectsById(persistedEntity, metadata);
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);
persistOperation.dbEntity = dbEntity;
persistOperation.persistedEntity = persistedEntity;
persistOperation.allDbEntities = dbEntities;
persistOperation.allPersistedEntities = allPersistedEntities;
persistOperation.inserts = this.findCascadeInsertedEntities(persistedEntity, dbEntities, null);
persistOperation.updates = this.findCascadeUpdateEntities(metadata, dbEntity, persistedEntity, null);
persistOperation.junctionInserts = this.findJunctionInsertOperations(metadata, persistedEntity, dbEntities);
persistOperation.updatesByRelations = this.updateRelations(persistOperation.inserts, persistedEntity);
persistOperation.removes = this.findCascadeRemovedEntities(metadata, dbEntity, allPersistedEntities, null, null, null);
persistOperation.junctionRemoves = this.findJunctionRemoveOperations(metadata, dbEntity, allPersistedEntities);
return persistOperation;
}
/**
* Finds columns and relations from entity2 which does not exist or does not match in entity1.
*/
buildOnlyRemovement(metadata: EntityMetadata, dbEntity: any, newEntity: any): PersistOperation {
const dbEntities = this.extractObjectsById(dbEntity, metadata);
const allEntities = this.extractObjectsById(newEntity, metadata);
const persistOperation = new PersistOperation();
persistOperation.dbEntity = dbEntity;
persistOperation.persistedEntity = newEntity;
persistOperation.allDbEntities = dbEntities;
persistOperation.allPersistedEntities = allEntities;
persistOperation.removes = this.findCascadeRemovedEntities(metadata, dbEntity, allEntities, null, null, null);
persistOperation.junctionRemoves = this.findJunctionRemoveOperations(metadata, dbEntity, allEntities);
return persistOperation;
}
@ -88,10 +105,9 @@ export class EntityPersistOperationBuilder {
// if object is new and should be inserted, we check if cascades are allowed before add it to operations list
if (isObjectNew && !this.checkCascadesAllowed("insert", metadata, fromRelation)) {
return operations; // return empty operations here
return operations; // looks like object is new here, but cascades are not allowed - then we should stop iteration
// looks like object is new here, but cascades are not allowed - then we should stop iteration
} else if (isObjectNew) {
} else if (isObjectNew) { // object is new and cascades are allow here
operations.push(new InsertOperation(newEntity));
}
@ -113,9 +129,9 @@ export class EntityPersistOperationBuilder {
return operations;
}
private findCascadeUpdateEntities(metadata: EntityMetadata,
dbEntity: any,
newEntity: any,
private findCascadeUpdateEntities(metadata: EntityMetadata,
dbEntity: any,
newEntity: any,
fromRelation: RelationMetadata): UpdateOperation[] {
let operations: UpdateOperation[] = [];
if (!dbEntity)
@ -146,7 +162,7 @@ export class EntityPersistOperationBuilder {
operations = operations.concat(relationOperations);
});
} else {
const relationOperations = this.findCascadeUpdateEntities(relMetadata, dbValue, newEntity[relation.propertyName], relation);
const relationOperations = this.findCascadeUpdateEntities(relMetadata, dbValue, newEntity[relation.propertyName], relation);
operations = operations.concat(relationOperations);
}
});
@ -154,38 +170,45 @@ export class EntityPersistOperationBuilder {
return operations;
}
private findCascadeRemovedEntities(metadata: EntityMetadata, dbEntity: any, newEntities: EntityWithId[]): any[] {
private findCascadeRemovedEntities(metadata: EntityMetadata,
dbEntity: any,
allPersistedEntities: EntityWithId[],
fromRelation: RelationMetadata,
fromMetadata: EntityMetadata,
fromEntityId: any,
parentAlreadyRemoved: boolean = false): RemoveOperation[] {
let operations: RemoveOperation[] = [];
if (!dbEntity)
return [];
return operations;
return metadata.relations
const relationId = dbEntity[metadata.primaryColumn.name];
const isObjectRemoved = parentAlreadyRemoved || !this.findEntityWithId(allPersistedEntities, metadata.target, relationId);
// if object is removed and should be removed, we check if cascades are allowed before add it to operations list
if (isObjectRemoved && !this.checkCascadesAllowed("remove", metadata, fromRelation)) {
return operations; // looks like object is removed here, but cascades are not allowed - then we should stop iteration
} else if (isObjectRemoved) { // object is remove and cascades are allow here
operations.push(new RemoveOperation(dbEntity, fromMetadata, fromRelation, fromEntityId));
}
metadata.relations
.filter(relation => !!dbEntity[relation.propertyName])
.reduce((removedEntities, relation) => {
const relationIdColumnName = relation.relatedEntityMetadata.primaryColumn.name;
.forEach(relation => {
const dbValue = dbEntity[relation.propertyName];
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(new RemoveOperation(metadata, relation, subEntity, dbEntity[metadata.primaryColumn.name]));
removedEntities = removedEntities.concat(this.findCascadeRemovedEntities(relMetadata, subEntity, newEntities));
if (dbValue instanceof Array) {
dbValue.forEach((subDbEntity: any) => {
const relationOperations = this.findCascadeRemovedEntities(relMetadata, subDbEntity, allPersistedEntities, relation, metadata, dbEntity[metadata.primaryColumn.name], isObjectRemoved);
operations = operations.concat(relationOperations);
});
} 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(new RemoveOperation(metadata, relation, dbEntity[relation.propertyName], dbEntity[metadata.primaryColumn.name]));
removedEntities = removedEntities.concat(this.findCascadeRemovedEntities(relMetadata, dbEntity[relation.propertyName], newEntities));
const relationOperations = this.findCascadeRemovedEntities(relMetadata, dbValue, allPersistedEntities, relation, metadata, dbEntity[metadata.primaryColumn.name], isObjectRemoved);
operations = operations.concat(relationOperations);
}
return removedEntities;
}, []);
return operations;
}
/**

View File

@ -35,7 +35,8 @@ export class PersistOperationExecutor {
.then(() => this.executeUpdateOperations(persistOperation))
.then(() => this.executeRemoveRelationOperations(persistOperation))
.then(() => this.executeRemoveOperations(persistOperation))
.then(() => this.executeUpdateByIdOperations(persistOperation));
.then(() => this.updateIdsOfInsertedEntities(persistOperation))
.then(() => this.updateIdsOfRemovedEntities(persistOperation));
}
// -------------------------------------------------------------------------
@ -93,9 +94,12 @@ export class PersistOperationExecutor {
* Executes remove relations operations.
*/
private executeRemoveRelationOperations(persistOperation: PersistOperation) {
return Promise.all(persistOperation.removes.map(operation => {
return this.updateDeletedRelations(operation);
}));
return Promise.all(persistOperation.removes
.filter(operation => operation.relation && !operation.relation.isManyToMany && !operation.relation.isOneToMany)
.map(operation => {
return this.updateDeletedRelations(operation);
})
);
}
/**
@ -108,15 +112,31 @@ export class PersistOperationExecutor {
}
/**
* Executes update by id operations.
* Updates all ids of the inserted entities.
*/
private executeUpdateByIdOperations(persistOperation: PersistOperation) {
private updateIdsOfInsertedEntities(persistOperation: PersistOperation) {
persistOperation.inserts.forEach(insertOperation => {
const metadata = this.connection.getMetadata(insertOperation.entity.constructor);
insertOperation.entity[metadata.primaryColumn.name] = insertOperation.entityId;
});
}
/**
* Removes all ids of the removed entities.
*/
private updateIdsOfRemovedEntities(persistOperation: PersistOperation) {
// console.log("OPERATION REMOVES: ", persistOperation.removes);
// console.log("ALL NEW ENTITIES: ", persistOperation.allNewEntities);
persistOperation.removes.forEach(removeOperation => {
const metadata = this.connection.getMetadata(removeOperation.entity.constructor);
const removedEntity = persistOperation.allPersistedEntities.find(allNewEntity => {
return allNewEntity.entity.constructor === removeOperation.entity.constructor && allNewEntity.id === removeOperation.entity[metadata.primaryColumn.name];
});
if (removedEntity)
removedEntity.entity[metadata.primaryColumn.propertyName] = undefined;
});
}
private updateByRelation(operation: UpdateByRelationOperation, insertOperations: InsertOperation[]) {
let tableName: string, relationName: string, relationId: any, idColumn: string, id: any;
const idInInserts = insertOperations.find(o => o.entity === operation.targetEntity).entityId;
@ -150,12 +170,10 @@ export class PersistOperationExecutor {
}
private updateDeletedRelations(removeOperation: RemoveOperation) { // todo: check if both many-to-one deletions work too
if (removeOperation.relation.isManyToMany || removeOperation.relation.isOneToMany) return;
return this.connection.driver.update(
removeOperation.metadata.table.name,
removeOperation.fromMetadata.table.name,
{ [removeOperation.relation.name]: null },
{ [removeOperation.metadata.primaryColumn.name]: removeOperation.fromEntityId }
{ [removeOperation.fromMetadata.primaryColumn.name]: removeOperation.fromEntityId }
);
}

View File

@ -5,34 +5,43 @@ import {JunctionInsertOperation} from "./JunctionInsertOperation";
import {JunctionRemoveOperation} from "./JunctionRemoveOperation";
import {UpdateByRelationOperation} from "./UpdateByRelationOperation";
export interface EntityWithId {
id: any;
entity: any;
}
export class PersistOperation {
// todo: what if we have two same entities in the insert operations?
inserts: InsertOperation[];
removes: RemoveOperation[];
updates: UpdateOperation[];
junctionInserts: JunctionInsertOperation[];
junctionRemoves: JunctionRemoveOperation[];
updatesByRelations: UpdateByRelationOperation[];
dbEntity: any;
persistedEntity: any;
allDbEntities: EntityWithId[];
allPersistedEntities: EntityWithId[];
inserts: InsertOperation[] = [];
removes: RemoveOperation[] = [];
updates: UpdateOperation[] = [];
junctionInserts: JunctionInsertOperation[] = [];
junctionRemoves: JunctionRemoveOperation[] = [];
updatesByRelations: UpdateByRelationOperation[] = [];
log() {
console.log("---------------------------------------------------------");
console.log("DB ENTITY");
console.log("---------------------------------------------------------");
// console.log(entity1);
console.log(this.dbEntity);
console.log("---------------------------------------------------------");
console.log("NEW ENTITY");
console.log("---------------------------------------------------------");
// console.log(entity2);
console.log(this.persistedEntity);
console.log("---------------------------------------------------------");
console.log("DB ENTITIES");
console.log("---------------------------------------------------------");
// console.log(dbEntities);
console.log(this.allDbEntities);
console.log("---------------------------------------------------------");
console.log("ALL NEW ENTITIES");
console.log("---------------------------------------------------------");
// console.log(allEntities);
console.log(this.allPersistedEntities);
console.log("---------------------------------------------------------");
console.log("INSERTED ENTITIES");
console.log("---------------------------------------------------------");

View File

@ -2,9 +2,9 @@ import {RelationMetadata} from "../../metadata-builder/metadata/RelationMetadata
import {EntityMetadata} from "../../metadata-builder/metadata/EntityMetadata";
export class RemoveOperation {
constructor(public metadata: EntityMetadata,
constructor(public entity: any,
public fromMetadata: EntityMetadata, //todo: use relation.metadata instead?
public relation: RelationMetadata,
public entity: any,
public fromEntityId: any) {
}
}

View File

@ -9,7 +9,6 @@ import {EntityPersistOperationBuilder} from "../persistment/EntityPersistOperati
import {PersistOperationExecutor} from "../persistment/PersistOperationExecutor";
// todo: think how we can implement queryCount, queryManyAndCount
// todo: extract non safe methods from repository (removeById, removeByConditions)
/**
* Repository is supposed to work with your entity objects. Find entities, insert, update, delete, etc.
@ -113,7 +112,8 @@ export class Repository<Entity> {
const persister = new PersistOperationExecutor(this.connection);
const promise = !this.hasId(entity) ? Promise.resolve(null) : this.initialize(entity);
return promise.then(dbEntity => {
const persistOperation = this.difference(dbEntity, entity);
const builder = new EntityPersistOperationBuilder(this.connection);
const persistOperation = builder.buildFullPersistment(this.metadata, dbEntity, entity);
return persister.executePersistOperation(persistOperation);
}).then(() => entity);
}
@ -125,7 +125,9 @@ export class Repository<Entity> {
const persister = new PersistOperationExecutor(this.connection);
return this.initialize(entity).then(dbEntity => {
// make this only to remove
const persistOperation = this.difference(dbEntity, entity);
(<any> entity)[this.metadata.primaryColumn.name] = undefined;
const builder = new EntityPersistOperationBuilder(this.connection);
const persistOperation = builder.buildOnlyRemovement(this.metadata, dbEntity, entity);
return persister.executePersistOperation(persistOperation);
}).then(() => entity);
}
@ -168,17 +170,4 @@ export class Repository<Entity> {
return this.connection.driver.query(query);
}
// -------------------------------------------------------------------------
// Private Methods
// -------------------------------------------------------------------------
/**
* Finds columns and relations from entity2 which does not exist or does not match in entity1. Returns an object
* that contains all information about what needs to be persisted.
*/
private difference(entity1: Entity, entity2: Entity): PersistOperation {
const builder = new EntityPersistOperationBuilder(this.connection);
return builder.difference(this.metadata, entity1, entity2);
}
}

View File

@ -8,10 +8,8 @@ import {SchemaCreator} from "../../src/schema-creator/SchemaCreator";
import {PostDetails} from "../../sample/sample4-many-to-many/entity/PostDetails";
import {Post} from "../../sample/sample4-many-to-many/entity/Post";
import {PostCategory} from "../../sample/sample4-many-to-many/entity/PostCategory";
import {PostAuthor} from "../../sample/sample4-many-to-many/entity/PostAuthor";
import {PostMetadata} from "../../sample/sample4-many-to-many/entity/PostMetadata";
import {PostImage} from "../../sample/sample4-many-to-many/entity/PostImage";
import {PostInformation} from "../../sample/sample4-many-to-many/entity/PostInformation";
chai.should();
describe("many-to-many", function() {
@ -26,7 +24,11 @@ describe("many-to-many", function() {
username: "root",
password: "admin",
database: "test",
autoSchemaCreate: true
autoSchemaCreate: true,
logging: {
logOnlyFailedQueries: true,
logFailedQueryError: true
}
};
// connect to db
@ -509,5 +511,113 @@ describe("many-to-many", function() {
});
});
// -------------------------------------------------------------------------
// Remove the post
// -------------------------------------------------------------------------
describe("remove post should remove it but not post details because of cascade settings", function() {
let newPost: Post, details: PostDetails, savedPostId: number, savedDetailsId: number;
before(reloadDatabase);
before(function() {
details = new PostDetails();
details.comment = "post details comment";
newPost = new Post();
newPost.text = "Hello post";
newPost.title = "this is post title";
newPost.details.push(details);
return postRepository
.persist(newPost) // first save
.then(savedPost => {
savedPostId = savedPost.id;
savedDetailsId = details.id;
return postRepository.remove(newPost);
}); // now remove newly saved
});
it("should have a savedPostId and savedDetailsId because it was persisted before removal", function () {
expect(savedPostId).not.to.be.empty;
expect(savedDetailsId).not.to.be.empty;
});
it("should not have a post id since object was removed from the db", function () {
expect(newPost.id).to.be.empty;
});
it("should have a details id since details was not removed from db because of cascades settings", function () {
expect(details.id).not.to.be.empty;
});
it("should not have post in the database", function() {
return postRepository.findById(savedPostId).should.eventually.eql(undefined);
});
it("should have details in the database because it was not removed because cascades do not allow it", function() {
const details = new PostDetails();
details.id = savedDetailsId;
details.comment = "post details comment";
return postDetailsRepository.findById(savedDetailsId).should.eventually.eql(details);
});
});
describe("remove post should remove it and its categories", function() {
let newPost: Post, category1: PostCategory, category2: PostCategory, savedPostId: number,
savedCategory1Id: number, savedCategory2Id: number;
before(reloadDatabase);
before(function() {
category1 = new PostCategory();
category1.name = "post category #1";
category2 = new PostCategory();
category2.name = "post category #2";
newPost = new Post();
newPost.text = "Hello post";
newPost.title = "this is post title";
newPost.categories.push(category1, category2);
return postRepository
.persist(newPost) // first save
.then(savedPost => {
savedPostId = savedPost.id;
savedCategory1Id = category1.id;
savedCategory2Id = category2.id;
return postRepository.remove(newPost);
}); // now remove newly saved
});
it("should have a savedPostId and savedCategory1Id and savedCategory2Id because it was persisted before removal", function () {
expect(savedPostId).not.to.be.empty;
expect(savedCategory1Id).not.to.be.empty;
expect(savedCategory2Id).not.to.be.empty;
});
it("should not have a post and category ids since object was removed from the db", function () {
console.log("removed post: ", newPost);
expect(newPost.id).to.be.empty;
expect(category1.id).to.be.empty;
expect(category2.id).to.be.empty;
});
it("should not have post in the database", function() {
return postRepository.findById(savedPostId).should.eventually.eql(undefined);
});
it("should not have category1 in the database", function() {
return postCategoryRepository.findById(savedCategory1Id).should.eventually.eql(undefined);
});
it("should not have category2 in the database", function() {
return postCategoryRepository.findById(savedCategory2Id).should.eventually.eql(undefined);
});
});
});