more experiments over new persistent mechanizm

This commit is contained in:
Umed Khudoiberdiev 2016-11-05 16:24:38 +05:00
parent 102f78b024
commit 21bca6b826
8 changed files with 116 additions and 34 deletions

View File

@ -162,8 +162,8 @@ export abstract class BaseEntityManager {
/**
* Creates a new query builder that can be used to build an sql query.
*/
createQueryBuilder<Entity>(entityClass: ObjectType<Entity>, alias: string): QueryBuilder<Entity> {
return this.getRepository(entityClass).createQueryBuilder(alias);
createQueryBuilder<Entity>(entityClass: ObjectType<Entity>|Function|string, alias: string): QueryBuilder<Entity> {
return this.getRepository(entityClass as any).createQueryBuilder(alias);
}
/**

View File

@ -432,13 +432,13 @@ export class RelationMetadata {
return ownEntity[this.joinTable.referencedColumn.propertyName];
} else if (this.isManyToManyNotOwner) {
return ownEntity[this.joinTable.inverseReferencedColumn.propertyName];
return ownEntity[this.inverseRelation.joinTable.inverseReferencedColumn.propertyName];
} else if (this.isOneToOneOwner || this.isManyToOne) {
return ownEntity[this.joinColumn.propertyName];
} else if (this.isOneToOneNotOwner || this.isOneToMany) {
return ownEntity[this.joinColumn.referencedColumn.propertyName];
return ownEntity[this.inverseRelation.joinColumn.referencedColumn.propertyName];
}
}
@ -447,13 +447,13 @@ export class RelationMetadata {
return inverseEntity[this.joinTable.inverseReferencedColumn.propertyName];
} else if (this.isManyToManyNotOwner) {
return inverseEntity[this.joinTable.referencedColumn.propertyName];
return inverseEntity[this.inverseRelation.joinTable.referencedColumn.propertyName];
} else if (this.isOneToOneOwner || this.isManyToOne) {
return inverseEntity[this.joinColumn.referencedColumn.propertyName];
} else if (this.isOneToOneNotOwner || this.isOneToMany) {
return inverseEntity[this.joinColumn.propertyName];
return inverseEntity[this.inverseRelation.joinColumn.propertyName];
}
}

View File

@ -62,7 +62,7 @@ export class DatabaseEntityLoader<Entity extends ObjectLiteral> {
/**
* If this gonna be reused then what to do with marked flags?
* One of solution can be clone this object and reset all marked states for this persistment.
* One of solution can be clone this object and reset all marked states for this persistence.
* Or from reused just extract databaseEntities from their subjects? (looks better)
*/
private loadedSubjects: SubjectCollection = new SubjectCollection();
@ -79,18 +79,20 @@ export class DatabaseEntityLoader<Entity extends ObjectLiteral> {
// -------------------------------------------------------------------------
async load(entity: Entity, metadata: EntityMetadata): Promise<void> {
// this.persistedEntity = new Subject(metadata, entity);
// this.loadedSubjects.push(this.persistedEntity);
const persistedEntity = new Subject(metadata, entity);
this.loadedSubjects.push(persistedEntity);
this.populateSubjectsWithCascadeUpdateAndInsertEntities(entity, metadata);
await this.loadDatabaseEntities();
// this.findCascadeInsertAndUpdateEntities(entity, metadata);
console.log("loadedSubjects: ", this.loadedSubjects);
const findCascadeRemoveOperations = this.loadedSubjects
.filter(subject => !!subject.databaseEntity) // means we only attempt to load for non new entities
.map(subject => this.findCascadeRemovedEntitiesToLoad(subject));
await Promise.all(findCascadeRemoveOperations);
console.log(this.loadedSubjects);
console.log("all persistence subjects: ", this.loadedSubjects);
}
// -------------------------------------------------------------------------
@ -191,7 +193,7 @@ export class DatabaseEntityLoader<Entity extends ObjectLiteral> {
* BIG NOTE: objects are being removed by cascades not only when relation is removed, but also when
* relation is replaced (e.g. changed with different object).
*/
protected async findCascadeRemovedEntitiesToLoad(subject: Subject/*, forceRemove = false*/): Promise<void> {
protected async findCascadeRemovedEntitiesToLoad(subject: Subject): Promise<void> {
// note: we can't use extractRelationValuesFromEntity here because it does not handle empty arrays
const promises = subject.metadata.relations.map(async relation => {
@ -236,7 +238,7 @@ export class DatabaseEntityLoader<Entity extends ObjectLiteral> {
const relationIdInDatabaseEntity = relation.getOwnEntityRelationId(subject.databaseEntity); // (example) returns post.detailsId
// if database relation id does not exist in the database object then nothing to remove
if (relationIdInDatabaseEntity !== null && relationIdInDatabaseEntity !== undefined)
if (relationIdInDatabaseEntity === null || relationIdInDatabaseEntity === undefined)
return;
// if this subject is persisted subject then we get its value to check if its not empty or its values changed
@ -248,7 +250,7 @@ export class DatabaseEntityLoader<Entity extends ObjectLiteral> {
}
// object is removed only if relation id in the persisted entity is empty or is changed
if (persistValueRelationId === null || persistValueRelationId === relationIdInDatabaseEntity)
if (persistValueRelationId !== null && persistValueRelationId === relationIdInDatabaseEntity)
return;
// first check if we already loaded this object before load from the database
@ -272,7 +274,7 @@ export class DatabaseEntityLoader<Entity extends ObjectLiteral> {
.getRepository<ObjectLiteral>(valueMetadata.target)
.createQueryBuilder(qbAlias)
.where(qbAlias + "." + relation.joinColumn.referencedColumn.propertyName + "=:id") // todo: need to escape alias and propertyName?
.setParameter("id", relation.getOwnEntityRelationId(subject.databaseEntity)) // (example) subject.entity is a post here
.setParameter("id", relationIdInDatabaseEntity) // (example) subject.entity is a post here
.enableOption("RELATION_ID_VALUES")
.getSingleResult();
@ -283,6 +285,12 @@ export class DatabaseEntityLoader<Entity extends ObjectLiteral> {
}
if (alreadyLoadedRelatedDatabaseSubject) {
// if object is already marked as removed then no need to proceed because it already was proceed
// if we remove this it will cause a recursion
if (alreadyLoadedRelatedDatabaseSubject.mustBeRemoved)
return;
alreadyLoadedRelatedDatabaseSubject.mustBeRemoved = true;
await this.findCascadeRemovedEntitiesToLoad(alreadyLoadedRelatedDatabaseSubject);
}
@ -316,7 +324,7 @@ export class DatabaseEntityLoader<Entity extends ObjectLiteral> {
const relationIdInDatabaseEntity = relation.getOwnEntityRelationId(subject.databaseEntity);
// if database relation id does not exist then nothing to remove (but can this be possible?)
if (relationIdInDatabaseEntity !== null && relationIdInDatabaseEntity !== undefined)
if (relationIdInDatabaseEntity === null || relationIdInDatabaseEntity === undefined)
return;
// first check if we already have this object loaded before load from the database
@ -340,7 +348,7 @@ export class DatabaseEntityLoader<Entity extends ObjectLiteral> {
.getRepository<ObjectLiteral>(valueMetadata.target)
.createQueryBuilder(qbAlias)
.where(qbAlias + "." + relation.inverseSideProperty + "=:id") // todo: need to escape alias and propertyName?
.setParameter("id", relation.getOwnEntityRelationId(subject.entity)) // (example) subject.entity is a details here, and the value is details.id
.setParameter("id", relationIdInDatabaseEntity) // (example) subject.entity is a details here, and the value is details.id
.enableOption("RELATION_ID_VALUES")
.getSingleResult();
@ -362,6 +370,11 @@ export class DatabaseEntityLoader<Entity extends ObjectLiteral> {
relation.getInverseEntityRelationId(alreadyLoadedRelatedDatabaseSubject.databaseEntity))
return;
// if object is already marked as removed then no need to proceed because it already was proceed
// if we remove this it will cause a recursion
if (alreadyLoadedRelatedDatabaseSubject.mustBeRemoved)
return;
alreadyLoadedRelatedDatabaseSubject.mustBeRemoved = true;
await this.findCascadeRemovedEntitiesToLoad(alreadyLoadedRelatedDatabaseSubject);
}
@ -393,7 +406,7 @@ export class DatabaseEntityLoader<Entity extends ObjectLiteral> {
const relationIdInDatabaseEntity = relation.getOwnEntityRelationId(subject.databaseEntity);
// if database relation id does not exist then nothing to remove (but can this be possible?)
if (relationIdInDatabaseEntity !== null && relationIdInDatabaseEntity !== undefined)
if (relationIdInDatabaseEntity === null || relationIdInDatabaseEntity === undefined)
return;
// if this subject is persisted subject then we get its value to check if its not empty or its values changed
@ -413,8 +426,8 @@ export class DatabaseEntityLoader<Entity extends ObjectLiteral> {
databaseEntities = await this.connection
.getRepository<ObjectLiteral>(valueMetadata.target)
.createQueryBuilder(qbAlias)
.innerJoin(subject.metadata.target, "persistenceJoinedRelation", "ON", qbAlias + "." + relation.joinTable.joinColumnName + "=:id") // todo: need to escape alias and propertyName?
.setParameter("id", relation.getOwnEntityRelationId(subject.entity))
.innerJoin(relation.junctionEntityMetadata.table.name, "persistenceJoinedRelation", "ON", "persistenceJoinedRelation." + relation.joinTable.joinColumnName + "=:id") // todo: need to escape alias and propertyName?
.setParameter("id", relationIdInDatabaseEntity)
.enableOption("RELATION_ID_VALUES")
.getResults();
@ -422,8 +435,8 @@ export class DatabaseEntityLoader<Entity extends ObjectLiteral> {
databaseEntities = await this.connection
.getRepository<ObjectLiteral>(valueMetadata.target)
.createQueryBuilder(qbAlias)
.innerJoin(subject.metadata.target, "persistenceJoinedRelation", "ON", qbAlias + "." + relation.joinTable.inverseJoinColumnName + "=:id") // todo: need to escape alias and propertyName?
.setParameter("id", relation.getOwnEntityRelationId(subject.entity))
.innerJoin(relation.junctionEntityMetadata.table.name, "persistenceJoinedRelation", "ON", "persistenceJoinedRelation." + relation.inverseRelation.joinTable.inverseJoinColumnName + "=:id") // todo: need to escape alias and propertyName?
.setParameter("id", relationIdInDatabaseEntity)
.enableOption("RELATION_ID_VALUES")
.getResults();
@ -432,7 +445,7 @@ export class DatabaseEntityLoader<Entity extends ObjectLiteral> {
.getRepository<ObjectLiteral>(valueMetadata.target)
.createQueryBuilder(qbAlias)
.where(qbAlias + "." + relation.inverseSideProperty + "=:id") // todo: need to escape alias and propertyName?
.setParameter("id", relation.getOwnEntityRelationId(subject.entity))
.setParameter("id", relationIdInDatabaseEntity)
.enableOption("RELATION_ID_VALUES")
.getResults();
}
@ -452,11 +465,16 @@ export class DatabaseEntityLoader<Entity extends ObjectLiteral> {
//
const promises = databaseEntities.map(async databaseEntity => {
const relatedEntitySubject = this.loadedSubjects.findByDatabaseEntityLike(valueMetadata.target, databaseEntity);
if (!relatedEntitySubject) return; // should not be possible
if (!relatedEntitySubject) return; // should not be possible, anyway add it for type-safety
// if object is already marked as removed then no need to proceed because it already was proceed
// if we remove this it will cause a recursion
if (relatedEntitySubject.mustBeRemoved) return;
if (persistValue === null) {
relatedEntitySubject.mustBeRemoved = true;
await this.findCascadeRemovedEntitiesToLoad(relatedEntitySubject);
return;
}
const relatedValue = (persistValue as ObjectLiteral[]).find(persistedRelatedValue => {

View File

@ -83,7 +83,7 @@ export class EntityPersistOperationBuilder {
if (dbEntity)
persistOperation.junctionRemoves = this.findJunctionRemoveOperations(metadata, dbEntity, allPersistedEntities);
persistOperation.log();
// persistOperation.log();
return persistOperation;
}

View File

@ -7,8 +7,6 @@ import {PersistOperationExecutor} from "./PersistOperationExecutor";
import {Connection} from "../connection/Connection";
import {QueryRunner} from "../query-runner/QueryRunner";
import {Subject} from "./subject/Subject";
import {SubjectFactory} from "./subject/SubjectFactory";
import {DatabaseSubjectsLoader} from "../query-builder/transformer/DatabaseSubjectsLoader";
import {SubjectCollection} from "./subject/SubjectCollection";
import {NewJunctionInsertOperation} from "./operation/NewJunctionInsertOperation";
import {NewJunctionRemoveOperation} from "./operation/NewJunctionRemoveOperation";
@ -123,10 +121,43 @@ export class EntityPersister<Entity extends ObjectLiteral> {
* 2. load from the database all entities that has primary keys and might be updated
*/
async persist(entity: Entity): Promise<Entity> {
if (true === true) {
const databaseEntityLoader = new DatabaseEntityLoader(this.connection);
await databaseEntityLoader.load(entity, this.metadata);
}
const allNewEntities = await this.flattenEntityRelationTree(entity, this.metadata);
const persistedEntity = allNewEntities.find(operatedEntity => operatedEntity.entity === entity);
if (!persistedEntity)
throw new Error(`Internal error. Persisted entity was not found in the list of prepared operated entities`);
let dbEntity: Subject|undefined, allDbInNewEntities: Subject[] = [];
// if entity has an id then check
if (this.metadata.hasId(entity)) {
const queryBuilder = new QueryBuilder<Entity>(this.connection, this.queryRunner)
.select(this.metadata.table.name)
.from(this.metadata.target, this.metadata.table.name);
const plainObjectToDatabaseEntityTransformer = new PlainObjectToDatabaseEntityTransformer();
const loadedDbEntity = await plainObjectToDatabaseEntityTransformer.transform(entity, this.metadata, queryBuilder);
if (loadedDbEntity) {
dbEntity = new Subject(this.metadata, loadedDbEntity);
allDbInNewEntities = await this.flattenEntityRelationTree(loadedDbEntity, this.metadata);
}
}
// need to find db entities that were not loaded by initialize method
const allDbEntities = await this.findNotLoadedIds(allNewEntities, allDbInNewEntities);
const entityPersistOperationBuilder = new EntityPersistOperationBuilder(this.connection.entityMetadatas);
const persistOperation = entityPersistOperationBuilder.buildFullPersistment(dbEntity, persistedEntity, allDbEntities, allNewEntities);
const persistOperationExecutor = new PersistOperationExecutor(this.connection.driver, this.connection.entityMetadatas, this.connection.broadcaster, this.queryRunner); // todo: better to pass connection?
await persistOperationExecutor.executePersistOperation(persistOperation);
return entity;
/*if (true === true) {
const databaseEntityLoader = new DatabaseEntityLoader(this.connection);
await databaseEntityLoader.load(entity, this.metadata);
console.log();
return entity;
}
@ -190,7 +221,7 @@ export class EntityPersister<Entity extends ObjectLiteral> {
/*let dbEntity: Subject|undefined, allDbInNewEntities: Subject[] = [];
/!*let dbEntity: Subject|undefined, allDbInNewEntities: Subject[] = [];
// if entity has an id then check
if (this.hasId(entity)) {
@ -203,13 +234,13 @@ export class EntityPersister<Entity extends ObjectLiteral> {
dbEntity = new Subject(this.metadata, loadedDbEntity);
allDbInNewEntities = this.flattenEntityRelationTree(loadedDbEntity, this.metadata);
}
}*/
}*!/
// need to find db entities that were not loaded by initialize method
// const allDbEntities = await this.findNotLoadedIds(persistedSubjects, allDbInNewEntities);
const persistOperation = entityPersistOperationBuilder.buildFullPersistment(databaseSubject, persistedSubject, databaseSubjects, persistedSubjects);
await persistOperationExecutor.executePersistOperation(persistOperation);
return entity;
return entity;*/
}
/**

View File

@ -34,8 +34,24 @@ describe("persistence > cascade operations", () => {
const post1 = new Post();
post1.title = "Hello Post #1";
post1.category = category1;
post1.category.photos = [photo1, photo2];
await connection.entityManager.persist(post1);
console.log("********************************************************");
const posts = await connection.entityManager
.createQueryBuilder(Post, "post")
.leftJoinAndSelect("post.category", "category")
// .innerJoinAndSelect("post.photos", "photos")
.getResults();
console.log("********************************************************");
console.log("posts: ", posts);
// posts[0].category = null; // todo: uncomment to check remove
console.log("removing post's category: ", posts[0]);
await connection.entityManager.persist(posts[0]);
/* await connection.entityManager.persist([photo1, photo2]);
post1.photos = [photo1];

View File

@ -3,6 +3,9 @@ import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/Prima
import {Column} from "../../../../../src/decorator/columns/Column";
import {Post} from "./Post";
import {OneToMany} from "../../../../../src/decorator/relations/OneToMany";
import {Photo} from "./Photo";
import {ManyToMany} from "../../../../../src/decorator/relations/ManyToMany";
import {JoinTable} from "../../../../../src/decorator/relations/JoinTable";
@Table()
export class Category {
@ -14,8 +17,18 @@ export class Category {
name: string;
@OneToMany(type => Post, post => post.category, {
cascadeInsert: true
cascadeInsert: true,
cascadeUpdate: true,
cascadeRemove: true
})
posts: Post[];
@ManyToMany(type => Photo, {
cascadeInsert: true,
cascadeUpdate: true,
cascadeRemove: true
})
@JoinTable()
photos: Photo[];
}

View File

@ -17,12 +17,16 @@ export class Post {
title: string;
@ManyToOne(type => Category, category => category.posts, {
cascadeInsert: true
cascadeInsert: true,
cascadeUpdate: true,
cascadeRemove: true
})
category: Category;
category: Category|null;
@ManyToMany(type => Photo, {
cascadeInsert: true
cascadeInsert: true,
cascadeUpdate: true,
cascadeRemove: true
})
@JoinTable()
photos: Photo[];