more experiments over new persistent mechanizm

This commit is contained in:
Umed Khudoiberdiev 2016-11-10 15:45:21 +05:00
parent aa8ac65f58
commit dfecec0fc9
9 changed files with 181 additions and 62 deletions

View File

@ -77,5 +77,5 @@ export class JoinColumnMetadata {
return this.relation.inverseEntityMetadata.firstPrimaryColumn;
}
}

View File

@ -5,6 +5,7 @@ import {Subject} from "./subject/Subject";
import {SubjectCollection} from "./subject/SubjectCollection";
import {NewJunctionRemoveOperation} from "./operation/NewJunctionRemoveOperation";
import {NewJunctionInsertOperation} from "./operation/NewJunctionInsertOperation";
const DepGraph = require("dependency-graph").DepGraph;
// at the end, subjects which does not have database entities are newly persisted entities
@ -111,6 +112,7 @@ export class DatabaseEntityLoader<Entity extends ObjectLiteral> {
// persistedEntity.mustBeRemoved = true;
// todo: execute operations
}
// -------------------------------------------------------------------------
@ -135,6 +137,10 @@ export class DatabaseEntityLoader<Entity extends ObjectLiteral> {
// if there is a value in the relation and insert or update cascades are set - it means we must load entity
if (relation.isEntityDefined(value) && (relation.isCascadeInsert || relation.isCascadeUpdate)) {
// if we already has this entity in list of loaded subjects then skip it to avoid recursion
if (this.loadedSubjects.hasWithEntity(value))
return;
// add to the array of subjects to load only if there is no same entity there already
const subject = new Subject(valueMetadata, value); // todo: store relations inside to create correct order then? // todo: try to find by likeDatabaseEntity and replace its persistment entity?
subject.canBeInserted = relation.isCascadeInsert === true;

View File

@ -13,6 +13,7 @@ import {CascadesNotAllowedError} from "./error/CascadesNotAllowedError";
import {NewUpdateOperation} from "./operation/NewUpdateOperation";
import {NewRemoveOperation} from "./operation/NewRemoveOperation";
import {DatabaseEntityLoader} from "./DatabaseEntityLoader";
import {PersistSubjectExecutor} from "./PersistSubjectExecutor";
/**
* Manages entity persistence - insert, update and remove of entity.
@ -122,6 +123,11 @@ export class EntityPersister<Entity extends ObjectLiteral> {
const databaseEntityLoader = new DatabaseEntityLoader(this.connection);
await databaseEntityLoader.load(entity, this.metadata);
console.log("all persistence subjects: ", databaseEntityLoader.loadedSubjects);
const executor = new PersistSubjectExecutor(this.connection, this.queryRunner);
await executor.execute(databaseEntityLoader.loadedSubjects, databaseEntityLoader.junctionInsertOperations, databaseEntityLoader.junctionRemoveOperations);
return entity;
// const allNewEntities = await this.flattenEntityRelationTree(entity, this.metadata);

View File

@ -43,7 +43,7 @@ export class PersistSubjectExecutor {
// check if remove subject also must be inserted or updated - then we throw an exception
const removeInInserts = removeSubjects.find(removeSubject => insertSubjects.indexOf(removeSubject) !== -1);
if (removeInInserts)
throw new Error(`Removed entity ${removeInInserts.entityTarget} is also scheduled for insert operation.`);
throw new Error(`Removed entity ${removeInInserts.entityTarget} is also scheduled for insert operation. This looks like ORM problem. Please report a github issue.`);
const removeInUpdates = removeSubjects.find(removeSubject => updateSubjects.indexOf(removeSubject) !== -1);
if (removeInUpdates)
@ -62,22 +62,22 @@ export class PersistSubjectExecutor {
}
await this.executeInsertOperations(insertSubjects);
await this.executeInsertClosureTableOperations(insertSubjects);
await this.executeUpdateTreeLevelOperations(insertSubjects);
await this.executeInsertJunctionsOperations(junctionInsertOperations, insertSubjects);
await this.executeRemoveJunctionsOperations(junctionRemoveOperations);
await this.executeRemoveRelationOperations(persistOperation); // todo: can we add these operations into list of updated?
await this.executeUpdateRelationsOperations(persistOperation); // todo: merge these operations with update operations?
await this.executeUpdateInverseRelationsOperations(persistOperation); // todo: merge these operations with update operations?
await this.executeUpdateOperations(updateSubjects);
await this.executeRemoveOperations(removeSubjects);
// await this.executeInsertClosureTableOperations(insertSubjects);
// await this.executeUpdateTreeLevelOperations(insertSubjects);
// await this.executeInsertJunctionsOperations(junctionInsertOperations, insertSubjects);
// await this.executeRemoveJunctionsOperations(junctionRemoveOperations);
// await this.executeRemoveRelationOperations(persistOperation); // todo: can we add these operations into list of updated?
// await this.executeUpdateRelationsOperations(persistOperation); // todo: merge these operations with update operations?
// await this.executeUpdateInverseRelationsOperations(persistOperation); // todo: merge these operations with update operations?
// await this.executeUpdateOperations(updateSubjects);
// await this.executeRemoveOperations(removeSubjects);
// commit transaction if it was started by us
if (isTransactionStartedByItself === true)
await this.queryRunner.commitTransaction();
// update all special columns in persisted entities, like inserted id or remove ids from the removed entities
await this.updateSpecialColumnsInPersistedEntities(insertSubjects, updateSubjects);
// await this.updateSpecialColumnsInPersistedEntities(insertSubjects, updateSubjects, removeSubjects);
// finally broadcast events
this.connection.broadcaster.broadcastAfterEventsForAll(insertSubjects, updateSubjects, removeSubjects);
@ -103,7 +103,35 @@ export class PersistSubjectExecutor {
* Executes insert operations.
*/
private async executeInsertOperations(insertSubjects: Subject[]): Promise<void> {
await Promise.all(insertSubjects.map(subject => this.insert(subject)));
// for insertion we separate two groups of entities:
// - first group of entities are entities which does not have any relations
// or entities which does not have any non-nullable relation
// - second group of entities are entities which does have non-nullable relations
// note: these two groups should be inserted in sequence, not in parallel, because second group is depend on first
// insert process of the entities from the first group which can only have nullable relations are actually a two-step process:
// - first we insert entities without their relations, explicitly left them NULL
// - later we update inserted entity once again with id of the object inserted with it
// yes, two queries are being executed, but this is by design.
// there is no better way to solve this problem and others at the same time.
// insert process of the entities from the second group which can have only non nullable relations is a single-step process:
// - we simply insert all entities and get into attention all its dependencies which were inserted in the first group
const firstInsertSubjects = insertSubjects.filter(subject => {
const relationsWithJoinColumns = subject.metadata.relationsWithJoinColumns;
return relationsWithJoinColumns.length === 0 || !!relationsWithJoinColumns.find(relation => relation.isNullable);
});
const secondInsertSubjects = insertSubjects.filter(subject => {
return !firstInsertSubjects.find(subjectFromFirstGroup => subjectFromFirstGroup === subject);
});
// console.log("firstInsertSubjects: ", firstInsertSubjects);
// console.log("secondInsertSubjects: ", secondInsertSubjects);
await Promise.all(firstInsertSubjects.map(subject => this.insert(subject, [])));
await Promise.all(secondInsertSubjects.map(subject => this.insert(subject, firstInsertSubjects)));
}
/**
@ -459,94 +487,134 @@ export class PersistSubjectExecutor {
* If entity has an generated column, then after saving new generated value will be stored to the InsertOperation.
* If entity uses class-table-inheritance, then multiple inserts may by performed to save all entities.
*/
private async insert(subject: Subject): Promise<any> {
private async insert(subject: Subject, alreadyInsertedSubjects: Subject[]): Promise<any> {
let generatedId: any;
const parentEntityMetadata = subject.metadata.parentEntityMetadata;
const metadata = subject.metadata;
const entity = subject.entity;
if (metadata.table.isClassTableChild) {
const parentValuesMap = this.collectColumnsAndValues(parentEntityMetadata, entity, subject.date, undefined, metadata.discriminatorValue);
generatedId = await this.queryRunner.insert(parentEntityMetadata.table.name, parentValuesMap, parentEntityMetadata.generatedColumnIfExist);
const childValuesMap = this.collectColumnsAndValues(metadata, entity, subject.date, generatedId);
// if entity uses class table inheritance then we need to separate entity into sub values that will be inserted into multiple tables
if (metadata.table.isClassTableChild) { // todo: with current implementation inheritance of multiple class table children will not work
// first insert entity values into parent class table
const parentValuesMap = this.collectColumnsAndValues(parentEntityMetadata, entity, subject.date, undefined, metadata.discriminatorValue, alreadyInsertedSubjects);
subject.newlyGeneratedId = await this.queryRunner.insert(parentEntityMetadata.table.name, parentValuesMap, parentEntityMetadata.generatedColumnIfExist);
// second insert entity values into child class table
const childValuesMap = this.collectColumnsAndValues(metadata, entity, subject.date, subject.newlyGeneratedId, undefined, alreadyInsertedSubjects);
const secondGeneratedId = await this.queryRunner.insert(metadata.table.name, childValuesMap, metadata.generatedColumnIfExist);
if (!generatedId && secondGeneratedId) generatedId = secondGeneratedId;
} else {
const valuesMap = this.collectColumnsAndValues(metadata, entity, subject.date);
generatedId = await this.queryRunner.insert(metadata.table.name, valuesMap, metadata.generatedColumnIfExist);
if (!subject.newlyGeneratedId && secondGeneratedId) subject.newlyGeneratedId = secondGeneratedId;
} else { // in the case when class table inheritance is not used
const valuesMap = this.collectColumnsAndValues(metadata, entity, subject.date, undefined, undefined, alreadyInsertedSubjects);
subject.newlyGeneratedId = await this.queryRunner.insert(metadata.table.name, valuesMap, metadata.generatedColumnIfExist);
}
// todo: remove this block once usage of subject.entityId are removed
// if there is a generated column and we have a generated id then store it in the insert operation for further use
if (parentEntityMetadata && parentEntityMetadata.hasGeneratedColumn && generatedId) {
subject.entityId = { [parentEntityMetadata.generatedColumn.propertyName]: generatedId };
if (parentEntityMetadata && parentEntityMetadata.hasGeneratedColumn && subject.newlyGeneratedId) {
subject.entityId = { [parentEntityMetadata.generatedColumn.propertyName]: subject.newlyGeneratedId };
} else if (metadata.hasGeneratedColumn && generatedId) {
subject.entityId = { [metadata.generatedColumn.propertyName]: generatedId };
} else if (metadata.hasGeneratedColumn && subject.newlyGeneratedId) {
subject.entityId = { [metadata.generatedColumn.propertyName]: subject.newlyGeneratedId };
}
}
private collectColumnsAndValues(metadata: EntityMetadata, entity: any, date: Date, parentIdColumnValue?: any, discriminatorValue?: any): ObjectLiteral {
private collectColumnsAndValues(metadata: EntityMetadata, entity: any, date: Date, parentIdColumnValue: any, discriminatorValue: any, alreadyInsertedSubjects: Subject[]): ObjectLiteral {
const columns = metadata.columns
.filter(column => !column.isVirtual && !column.isParentId && !column.isDiscriminator && column.hasEntityValue(entity));
// extract all columns
const columns = metadata.columns.filter(column => {
return !column.isVirtual && !column.isParentId && !column.isDiscriminator && column.hasEntityValue(entity);
});
const values = columns.map(column => this.connection.driver.preparePersistentValue(column.getEntityValue(entity), column));
const relationColumns = metadata.relationsWithJoinColumns
.filter(relation => entity.hasOwnProperty(relation.propertyName));
const relationColumns = metadata.relations
.filter(relation => !relation.isManyToMany && relation.isOwning && !!relation.inverseEntityMetadata)
.filter(relation => entity.hasOwnProperty(relation.propertyName))
.map(relation => relation.name);
const columnNames = columns.map(column => column.name);
const relationColumnNames = relationColumns.map(relation => relation.name);
const allColumnNames = columnNames.concat(relationColumnNames);
const relationValues = metadata.relations
.filter(relation => !relation.isManyToMany && relation.isOwning && !!relation.inverseEntityMetadata)
.filter(relation => entity.hasOwnProperty(relation.propertyName))
.map(relation => {
const value = this.getEntityRelationValue(relation, entity);
if (value !== null && value !== undefined) // in the case if relation has null, which can be saved
return value[relation.inverseEntityMetadata.firstPrimaryColumn.propertyName]; // todo: it should be get by field set in join column in the relation metadata
const columnValues = columns.map(column => {
return this.connection.driver.preparePersistentValue(column.getEntityValue(entity), column);
});
// extract all values
const relationValues = relationColumns.map(relation => {
const value = relation.getEntityValue(entity);
if (value === null || value === undefined)
return value;
// if relation value is stored in the entity itself then use it from there
const relationId = relation.getInverseEntityRelationId(value); // todo: check it
if (relationId)
return relationId;
// otherwise try to find relational value from just inserted subjects
const alreadyInsertedSubject = alreadyInsertedSubjects.find(insertedSubject => {
return insertedSubject.entity === value;
});
if (alreadyInsertedSubject) {
const referencedColumn = relation.joinColumn.referencedColumn;
const allColumns = columns.map(column => column.name).concat(relationColumns);
const allValues = values.concat(relationValues);
// if join column references to the primary generated column then seek in the newEntityId of the insertedSubject
if (referencedColumn.isGenerated)
return alreadyInsertedSubject.newlyGeneratedId;
// if it references to create or update date columns
if (referencedColumn.isCreateDate || referencedColumn.isUpdateDate)
return this.connection.driver.preparePersistentValue(alreadyInsertedSubject.date, referencedColumn);
// if it references to version column
if (referencedColumn.isVersion)
return this.connection.driver.preparePersistentValue(1, referencedColumn);
// todo: implement other referenced column types
}
});
const allValues = columnValues.concat(relationValues);
// add special column and value - date of creation
if (metadata.hasCreateDateColumn) {
allColumns.push(metadata.createDateColumn.name);
allColumnNames.push(metadata.createDateColumn.name);
allValues.push(this.connection.driver.preparePersistentValue(date, metadata.createDateColumn));
}
// add special column and value - date of updating
if (metadata.hasUpdateDateColumn) {
allColumns.push(metadata.updateDateColumn.name);
allColumnNames.push(metadata.updateDateColumn.name);
allValues.push(this.connection.driver.preparePersistentValue(date, metadata.updateDateColumn));
}
// add special column and value - version column
if (metadata.hasVersionColumn) {
allColumns.push(metadata.versionColumn.name);
allColumnNames.push(metadata.versionColumn.name);
allValues.push(this.connection.driver.preparePersistentValue(1, metadata.versionColumn));
}
// add special column and value - discriminator value (for tables using table inheritance)
if (metadata.hasDiscriminatorColumn) {
allColumns.push(metadata.discriminatorColumn.name);
allColumnNames.push(metadata.discriminatorColumn.name);
allValues.push(this.connection.driver.preparePersistentValue(discriminatorValue || metadata.discriminatorValue, metadata.discriminatorColumn));
}
// add special column and value - tree level and tree parents (for tree-type tables)
if (metadata.hasTreeLevelColumn && metadata.hasTreeParentRelation) {
const parentEntity = entity[metadata.treeParentRelation.propertyName];
const parentLevel = parentEntity ? (parentEntity[metadata.treeLevelColumn.propertyName] || 0) : 0;
allColumns.push(metadata.treeLevelColumn.name);
allColumnNames.push(metadata.treeLevelColumn.name);
allValues.push(parentLevel + 1);
}
// add special column and value - parent id column (for tables using table inheritance)
if (metadata.parentEntityMetadata && metadata.hasParentIdColumn) {
allColumns.push(metadata.parentIdColumn.name); // todo: should be array of primary keys
allColumnNames.push(metadata.parentIdColumn.name); // todo: should be array of primary keys
allValues.push(parentIdColumnValue || entity[metadata.parentEntityMetadata.firstPrimaryColumn.propertyName]); // todo: should be array of primary keys
}
return this.zipObject(allColumns, allValues);
return this.zipObject(allColumnNames, allValues);
}
private insertIntoClosureTable(subject: Subject, updateMap: ObjectLiteral) {

View File

@ -25,9 +25,20 @@ export class Subject { // todo: move entity with id creation into metadata? // t
/**
* When subject is newly persisted it may have a generated entity id.
* In this case it should be written here.
*
* @deprecated use newlyGeneratedId instead. Difference between this and newly generated id
* is that newly generated id hold value itself, without being in object.
* When we have generated value we always have only one primary key thous we dont need object
*/
entityId: any; // todo: rename to newEntityId
/**
* When subject is newly persisted it may have a generated entity id.
* In this case it should be written here.
*
*/
newlyGeneratedId: any;
/**
* Used in newly persisted entities which are tree tables.
*/
@ -65,7 +76,7 @@ export class Subject { // todo: move entity with id creation into metadata? // t
}
get mustBeInserted() {
return !!this.databaseEntity;
return !this.databaseEntity;
}
get mustBeUpdated() {
@ -78,8 +89,10 @@ export class Subject { // todo: move entity with id creation into metadata? // t
set databaseEntity(databaseEntity: ObjectLiteral|undefined) {
this._databaseEntity = databaseEntity;
this.buildDiffColumns();
this.buildDiffRelationalColumns();
if (this.entity && databaseEntity) {
this.buildDiffColumns();
this.buildDiffRelationalColumns();
}
}
// -------------------------------------------------------------------------

View File

@ -86,4 +86,8 @@ export class SubjectCollection extends Array<Subject> {
});
}
hasWithEntity(entity: ObjectLiteral): boolean {
return !!this.findByEntity(entity);
}
}

View File

@ -24,22 +24,26 @@ describe("persistence > cascade operations", () => {
const category1 = new Category();
category1.name = "Category saved by cascades #1";
// create photos
const photo1 = new Photo();
photo1.url = "http://me.com/photo";
const photo2 = new Photo();
photo2.url = "http://me.com/photo";
// create post
const post1 = new Post();
post1.title = "Hello Post #1";
// create photos
const photo1 = new Photo();
photo1.url = "http://me.com/photo";
photo1.post = post1;
const photo2 = new Photo();
photo2.url = "http://me.com/photo";
photo2.post = post1;
post1.category = category1;
post1.category.photos = [photo1, photo2];
await connection.entityManager.persist(post1);
console.log("********************************************************");
const posts = await connection.entityManager
/*const posts = await connection.entityManager
.createQueryBuilder(Post, "post")
.leftJoinAndSelect("post.category", "category")
// .innerJoinAndSelect("post.photos", "photos")
@ -52,7 +56,7 @@ describe("persistence > cascade operations", () => {
// 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(posts[0]);*/
/* await connection.entityManager.persist([photo1, photo2]);

View File

@ -6,6 +6,7 @@ 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";
import {ManyToOne} from "../../../../../src/decorator/relations/ManyToOne";
@Table()
export class Category {
@ -31,4 +32,11 @@ export class Category {
@JoinTable()
photos: Photo[];
@ManyToOne(type => Photo, {
cascadeInsert: true,
cascadeUpdate: true,
cascadeRemove: true
})
photo: Photo|null;
}

View File

@ -1,6 +1,8 @@
import {Table} from "../../../../../src/decorator/tables/Table";
import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn";
import {Column} from "../../../../../src/decorator/columns/Column";
import {ManyToOne} from "../../../../../src/decorator/relations/ManyToOne";
import {Post} from "../entity/Post";
@Table()
export class Photo {
@ -11,4 +13,12 @@ export class Photo {
@Column()
url: string;
@ManyToOne(type => Post, {
cascadeInsert: true,
cascadeUpdate: true,
cascadeRemove: true,
nullable: false
})
post: Post|null;
}