more experiments over new persistent mechanizm

This commit is contained in:
Umed Khudoiberdiev 2016-11-13 13:01:25 +05:00
parent dfecec0fc9
commit 651eb8dd22
14 changed files with 500 additions and 25 deletions

View File

@ -561,6 +561,23 @@ export class EntityMetadata {
return hasAllIds ? map : undefined;
}
/**
*/
createSimpleIdMap(id: any): ObjectLiteral {
const map: ObjectLiteral = {};
if (this.parentEntityMetadata) {
this.primaryColumnsWithParentIdColumns.forEach(column => {
map[column.propertyName] = id;
});
} else {
this.primaryColumns.forEach(column => {
map[column.propertyName] = id;
});
}
return map;
}
/**
* todo: undefined entities should not go there??
* todo: shouldnt be entity ObjectLiteral here?

View File

@ -545,17 +545,17 @@ export class DatabaseEntityLoader<Entity extends ObjectLiteral> {
.findRelationIds(relation, persistedSubject.entity, inverseEntityRelationIds);
// now from all entities in the persisted entity find only those which aren't found in the db
const newRelationIds = inverseEntityRelationIds.filter(inverseEntityRelationId => {
/*const newRelationIds = inverseEntityRelationIds.filter(inverseEntityRelationId => {
return !existInverseEntityRelationIds.find(relationId => inverseEntityRelationId === relationId);
});
/*const persistedEntities = value.filter(val => {
const relationValue = relation.getInverseEntityRelationId(val);
return !existInverseEntityRelationIds.find(relationId => relationValue === relationId);
});*/ // todo: remove later if not necessary
});*/
const persistedEntities = value.filter(val => {
const relationValue = relation.getInverseEntityRelationId(val);
return !relationValue || !existInverseEntityRelationIds.find(relationId => relationValue === relationId);
}); // todo: remove later if not necessary
// finally create a new junction insert operation and push it to the array of such operations
if (newRelationIds.length > 0) {
const operation = new NewJunctionInsertOperation(relation, persistedSubject, newRelationIds);
if (persistedEntities.length > 0) {
const operation = new NewJunctionInsertOperation(relation, persistedSubject, persistedEntities);
junctionInsertOperations.push(operation);
}
});

View File

@ -50,6 +50,8 @@ export class PersistSubjectExecutor {
throw new Error(`Removed entity "${removeInUpdates.entityTarget}" is also scheduled for update operation. ` +
`Make sure you are not updating and removing same object (note that update or remove may be executed by cascade operations).`);
// todo: there is nothing to update in inserted entity too
try {
// broadcast events before we start updating
@ -64,7 +66,7 @@ export class PersistSubjectExecutor {
await this.executeInsertOperations(insertSubjects);
// await this.executeInsertClosureTableOperations(insertSubjects);
// await this.executeUpdateTreeLevelOperations(insertSubjects);
// await this.executeInsertJunctionsOperations(junctionInsertOperations, 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?
@ -132,6 +134,79 @@ export class PersistSubjectExecutor {
await Promise.all(firstInsertSubjects.map(subject => this.insert(subject, [])));
await Promise.all(secondInsertSubjects.map(subject => this.insert(subject, firstInsertSubjects)));
const updatePromises: Promise<any>[] = [];
insertSubjects.forEach(subject => {
// we need to update relation ids of the newly inserted objects (where we inserted NULLs in relations)
const updateOptions: ObjectLiteral = {};
subject.metadata.relationsWithJoinColumns.forEach(relation => {
const referencedColumn = relation.joinColumn.referencedColumn;
insertSubjects.forEach(insertedSubject => {
if (subject.entity[relation.propertyName] === insertedSubject.entity) {
if (referencedColumn.isGenerated)
updateOptions[relation.name] = insertedSubject.newlyGeneratedId;
// todo: implement other special referenced column types (update date, create date, version, discriminator column, etc.)
}
});
});
if (Object.keys(updateOptions).length > 0) {
const conditions = subject.metadata.getEntityIdMap(subject.entity) || subject.metadata.createSimpleIdMap(subject.newlyGeneratedId);
const updatePromise = this.queryRunner.update(subject.metadata.table.name, updateOptions, conditions);
updatePromises.push(updatePromise);
}
// we need to update relation ids if newly inserted objects are used from inverse side in one-to-many inverse relation
subject.metadata.oneToManyRelations.forEach(relation => {
const referencedColumn = relation.inverseRelation.joinColumn.referencedColumn;
const value = subject.entity[relation.propertyName];
if (value instanceof Array) {
value.forEach(subValue => {
insertSubjects.forEach(insertedSubject => {
if (subValue === insertedSubject.entity) {
if (referencedColumn.isGenerated) {
const conditions = insertedSubject.metadata.getEntityIdMap(insertedSubject.entity) || insertedSubject.metadata.createSimpleIdMap(insertedSubject.newlyGeneratedId);
const updateOptions = { [relation.inverseRelation.joinColumn.name]: subject.newlyGeneratedId };
const updatePromise = this.queryRunner.update(relation.inverseRelation.entityMetadata.table.name, updateOptions, conditions);
updatePromises.push(updatePromise);
}
// todo: implement other special referenced column types (update date, create date, version, discriminator column, etc.)
}
});
});
}
});
// we also need to update relation ids if newly inserted objects are used from inverse side in one-to-one inverse relation
subject.metadata.oneToOneRelations.filter(relation => !relation.isOwning).forEach(relation => {
const referencedColumn = relation.inverseRelation.joinColumn.referencedColumn;
insertSubjects.forEach(insertedSubject => {
if (subject.entity[relation.propertyName] === insertedSubject.entity) {
if (referencedColumn.isGenerated) {
const conditions = insertedSubject.metadata.getEntityIdMap(insertedSubject.entity) || insertedSubject.metadata.createSimpleIdMap(insertedSubject.newlyGeneratedId);
const updateOptions = { [relation.inverseRelation.joinColumn.name]: subject.newlyGeneratedId };
const updatePromise = this.queryRunner.update(relation.inverseRelation.entityMetadata.table.name, updateOptions, conditions);
updatePromises.push(updatePromise);
}
// todo: implement other special referenced column types (update date, create date, version, discriminator column, etc.)
}
});
});
});
await Promise.all(updatePromises);
// todo: make sure to search in all insertSubjects during updating too if updated entity uses links to the newly persisted entity
}
/**
@ -656,7 +731,7 @@ export class PersistSubjectExecutor {
}
}
private insertJunctions(junctionOperation: NewJunctionInsertOperation, insertOperations: Subject[]) {
/*private insertJunctions(junctionOperation: NewJunctionInsertOperation, insertOperations: Subject[]) {
// I think here we can only support to work only with single primary key entities
const metadata1 = this.connection.entityMetadatas.findByTarget(junctionOperation.entity1Target);
@ -688,13 +763,56 @@ export class PersistSubjectExecutor {
let values: any[];
// order may differ, find solution (column.table to compare with entity metadata table?)
if (metadata1.table === junctionMetadata.foreignKeys[0].referencedTable) {
if (metadata1.table === junctionOperation.metadata.foreignKeys[0].referencedTable) {
values = [id1, id2];
} else {
values = [id2, id1];
}
return this.queryRunner.insert(junctionMetadata.table.name, this.zipObject(columns, values));
return this.queryRunner.insert(junctionOperation.metadata.table.name, this.zipObject(columns, values));
}*/
private async insertJunctions(junctionOperation: NewJunctionInsertOperation, insertSubjects: Subject[]): Promise<void> {
// I think here we can only support to work only with single primary key entities
const relation = junctionOperation.relation;
const joinTable = relation.isOwning ? relation.joinTable : relation.inverseRelation.joinTable;
const firstColumn = relation.isOwning ? joinTable.referencedColumn : joinTable.inverseReferencedColumn;
const secondColumn = relation.isOwning ? joinTable.inverseReferencedColumn : joinTable.referencedColumn;
let ownId = junctionOperation.relation.getOwnEntityRelationId(junctionOperation.subject.entity);
if (!ownId) {
if (firstColumn.isGenerated) {
ownId = junctionOperation.subject.newlyGeneratedId;
}
// todo: implement other special referenced column types (update date, create date, version, discriminator column, etc.)
}
if (!ownId)
throw new Error(`Cannot insert object of ${junctionOperation.subject.entityTarget} type. Looks like its not persisted yet, or cascades are not set on the relation.`); // todo: better error message
const promises = junctionOperation.junctionEntities.map(newBindEntity => {
let relationId = junctionOperation.relation.getInverseEntityRelationId(newBindEntity);
if (!relationId) {
const insertSubject = insertSubjects.find(subject => subject.entity === newBindEntity);
if (insertSubject) {
if (secondColumn.isGenerated) {
relationId = insertSubject.newlyGeneratedId;
}
}
}
if (!relationId)
throw new Error(`Cannot insert object of ${relation.inverseRelation.entityTarget} type. Looks like its not persisted yet, or cascades are not set on the relation.`); // todo: better error message
const columns = relation.junctionEntityMetadata.columns.map(column => column.name);
const values = relation.isOwning ? [ownId, relationId] : [relationId, ownId];
return this.queryRunner.insert(relation.junctionEntityMetadata.table.name, this.zipObject(columns, values));
});
await Promise.all(promises);
}
private removeJunctions(junctionOperation: JunctionRemoveOperation) {

View File

@ -1,17 +1,13 @@
import {RelationMetadata} from "../../metadata/RelationMetadata";
import {EntityMetadata} from "../../metadata/EntityMetadata";
import {Subject} from "../subject/Subject";
import {ObjectLiteral} from "../../common/ObjectLiteral";
export class NewJunctionInsertOperation {
// todo: we can send subjects instead of entities and junction entities if needed
constructor(public relation: RelationMetadata,
public subject: Subject,
public junctionEntityRelationIds: any[]) { // junctionEntities can be replaced with ids?
}
get metadata(): EntityMetadata {
return this.relation.entityMetadata;
public junctionEntities: ObjectLiteral[]) { // junctionEntities can be replaced with ids?
}
}

View File

@ -76,11 +76,11 @@ export class Subject { // todo: move entity with id creation into metadata? // t
}
get mustBeInserted() {
return !this.databaseEntity;
return this.canBeInserted && !this.databaseEntity;
}
get mustBeUpdated() {
return this.diffColumns.length > 0 || this.diffRelations.length > 0;
return this.canBeUpdated && (this.diffColumns.length > 0 || this.diffRelations.length > 0);
}
get databaseEntity(): ObjectLiteral|undefined {

View File

@ -449,7 +449,7 @@ export class SpecificRepository<Entity extends ObjectLiteral> {
const relation = this.convertMixedRelationToMetadata(relationOrName);
if (!(entityOrEntities instanceof Array)) entityOrEntities = [entityOrEntities];
const entityReferencedColumn = relation.isOwning ? relation.joinTable.referencedColumn : relation.joinTable.inverseReferencedColumn;
const entityReferencedColumn = relation.isOwning ? relation.joinTable.referencedColumn : relation.inverseRelation.joinTable.inverseReferencedColumn;
const ownerEntityColumn = relation.isOwning ? relation.junctionEntityMetadata.columns[0] : relation.junctionEntityMetadata.columns[1];
const inverseEntityColumn = relation.isOwning ? relation.junctionEntityMetadata.columns[1] : relation.junctionEntityMetadata.columns[0];

View File

@ -20,26 +20,39 @@ describe("persistence > cascade operations", () => {
it.only("should work perfectly", () => Promise.all(connections.map(async connection => {
// create category
const category1 = new Category();
category1.name = "Category saved by cascades #1";
// category1.onePost = post1;
// create category
const category2 = new Category();
category2.name = "Category saved by cascades #2";
// category1.onePost = post1;
// create post
const post1 = new Post();
post1.title = "Hello Post #1";
// post1.oneCategory = category1;
// todo(next): check out to one
// create photos
const photo1 = new Photo();
photo1.url = "http://me.com/photo";
photo1.post = post1;
photo1.categories = [category1, category2];
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);
// category1.photos = [photo1, photo2];
// post1.category = category1;
// post1.category.photos = [photo1, photo2];
await connection.entityManager.persist(photo1);
console.log("********************************************************");

View File

@ -7,6 +7,8 @@ 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";
import {OneToOne} from "../../../../../src/decorator/relations/OneToOne";
import {JoinColumn} from "../../../../../src/decorator/relations/JoinColumn";
@Table()
export class Category {
@ -17,6 +19,14 @@ export class Category {
@Column()
name: string;
@OneToOne(type => Post, post => post.oneCategory, {
cascadeInsert: true,
cascadeUpdate: true,
cascadeRemove: true
})
@JoinColumn()
onePost: Post;
@OneToMany(type => Post, post => post.category, {
cascadeInsert: true,
cascadeUpdate: true,
@ -24,7 +34,7 @@ export class Category {
})
posts: Post[];
@ManyToMany(type => Photo, {
@ManyToMany(type => Photo, photo => photo.categories, {
cascadeInsert: true,
cascadeUpdate: true,
cascadeRemove: true

View File

@ -3,6 +3,8 @@ import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/Prima
import {Column} from "../../../../../src/decorator/columns/Column";
import {ManyToOne} from "../../../../../src/decorator/relations/ManyToOne";
import {Post} from "../entity/Post";
import {Category} from "../entity/Category";
import {ManyToMany} from "../../../../../src/decorator/relations/ManyToMany";
@Table()
export class Photo {
@ -21,4 +23,11 @@ export class Photo {
})
post: Post|null;
@ManyToMany(type => Category, category => category.photos, {
cascadeInsert: true,
cascadeUpdate: true,
cascadeRemove: true
})
categories: Category[];
}

View File

@ -6,6 +6,7 @@ import {Category} from "./Category";
import {Photo} from "./Photo";
import {ManyToMany} from "../../../../../src/decorator/relations/ManyToMany";
import {JoinTable} from "../../../../../src/decorator/relations/JoinTable";
import {OneToOne} from "../../../../../src/decorator/relations/OneToOne";
@Table()
export class Post {
@ -31,4 +32,11 @@ export class Post {
@JoinTable()
photos: Photo[];
@OneToOne(type => Category, category => category.onePost, {
cascadeInsert: true,
cascadeUpdate: true,
cascadeRemove: true
})
oneCategory: Category;
}

View File

@ -0,0 +1,52 @@
import {Table} from "../../../../../src/decorator/tables/Table";
import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn";
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";
import {ManyToOne} from "../../../../../src/decorator/relations/ManyToOne";
import {OneToOne} from "../../../../../src/decorator/relations/OneToOne";
import {JoinColumn} from "../../../../../src/decorator/relations/JoinColumn";
@Table()
export class Category {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@OneToOne(type => Post, post => post.oneCategory, {
cascadeInsert: true,
cascadeUpdate: true,
cascadeRemove: true
})
@JoinColumn()
onePost: Post;
@OneToMany(type => Post, post => post.category, {
cascadeInsert: true,
cascadeUpdate: true,
cascadeRemove: true
})
posts: Post[];
@ManyToMany(type => Photo, {
cascadeInsert: true,
cascadeUpdate: true,
cascadeRemove: true
})
@JoinTable()
photos: Photo[];
@ManyToOne(type => Photo, {
cascadeInsert: true,
cascadeUpdate: true,
cascadeRemove: true
})
photo: Photo|null;
}

View File

@ -0,0 +1,24 @@
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 {
@PrimaryGeneratedColumn()
id: number;
@Column()
url: string;
@ManyToOne(type => Post, {
cascadeInsert: true,
cascadeUpdate: true,
cascadeRemove: true,
nullable: false
})
post: Post|null;
}

View File

@ -0,0 +1,42 @@
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 {Category} from "./Category";
import {Photo} from "./Photo";
import {ManyToMany} from "../../../../../src/decorator/relations/ManyToMany";
import {JoinTable} from "../../../../../src/decorator/relations/JoinTable";
import {OneToOne} from "../../../../../src/decorator/relations/OneToOne";
@Table()
export class Post {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@ManyToOne(type => Category, category => category.posts, {
cascadeInsert: true,
cascadeUpdate: true,
cascadeRemove: true
})
category: Category|null;
@ManyToMany(type => Photo, {
cascadeInsert: true,
cascadeUpdate: true,
cascadeRemove: true
})
@JoinTable()
photos: Photo[];
@OneToOne(type => Category, category => category.onePost, {
cascadeInsert: true,
cascadeUpdate: true,
cascadeRemove: true
})
oneCategory: Category;
}

View File

@ -0,0 +1,186 @@
import "reflect-metadata";
import {setupTestingConnections, closeConnections, reloadDatabases} from "../../../utils/test-utils";
import {Connection} from "../../../../src/connection/Connection";
import {Post} from "./entity/Post";
import {Category} from "./entity/Category";
import {Photo} from "./entity/Photo";
describe("persistence > insert operations", () => {
let connections: Connection[];
before(async () => connections = await setupTestingConnections({
entities: [__dirname + "/entity/*{.js,.ts}"],
schemaCreate: true,
dropSchemaOnConnection: true,
}));
beforeEach(() => reloadDatabases(connections));
after(() => closeConnections(connections));
describe("cascade insert", function() {
it("should work perfectly", () => Promise.all(connections.map(async connection => {
// create category
const category1 = new Category();
category1.name = "Category saved by cascades #1";
// category1.onePost = post1;
// create post
const post1 = new Post();
post1.title = "Hello Post #1";
post1.oneCategory = category1;
// todo(next): check out to one
// 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
.createQueryBuilder(Post, "post")
.leftJoinAndSelect("post.category", "category")
// .innerJoinAndSelect("post.photos", "photos")
.getResults();
posts[0].title = "Updated Post #1";
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];
await connection.entityManager.persist(post1);
console.log("********************************************************");
console.log("********************************************************");
post1.photos = [photo1, photo2];
await connection.entityManager.persist(post1);
console.log("********************************************************");
console.log("********************************************************");
post1.title = "Updated Post";
await connection.entityManager.persist(post1);*/
})));
it("should insert entity when cascade option is set", () => Promise.all(connections.map(async connection => {
// create first category and post and save them
const category1 = new Category();
category1.name = "Category saved by cascades #1";
const post1 = new Post();
post1.title = "Hello Post #1";
post1.category = category1;
await connection.entityManager.persist(post1);
// create second category and post and save them
const category2 = new Category();
category2.name = "Category saved by cascades #2";
const post2 = new Post();
post2.title = "Hello Post #2";
post2.category = category2;
await connection.entityManager.persist(post2);
// now check
const posts = await connection.entityManager.find(Post, {
alias: "post",
innerJoinAndSelect: {
category: "post.category"
},
orderBy: {
"post.id": "ASC"
}
});
posts.should.be.eql([{
id: 1,
title: "Hello Post #1",
category: {
id: 1,
name: "Category saved by cascades #1"
}
}, {
id: 2,
title: "Hello Post #2",
category: {
id: 2,
name: "Category saved by cascades #2"
}
}]);
})));
it("should insert from inverse side when cascade option is set", () => Promise.all(connections.map(async connection => {
// create first post and category and save them
const post1 = new Post();
post1.title = "Hello Post #1";
const category1 = new Category();
category1.name = "Category saved by cascades #1";
category1.posts = [post1];
await connection.entityManager.persist(category1);
// create first post and category and save them
const post2 = new Post();
post2.title = "Hello Post #2";
const category2 = new Category();
category2.name = "Category saved by cascades #2";
category2.posts = [post2];
await connection.entityManager.persist(category2);
// now check
const posts = await connection.entityManager.find(Post, {
alias: "post",
innerJoinAndSelect: {
category: "post.category"
},
orderBy: {
"post.id": "ASC"
}
});
posts.should.be.eql([{
id: 1,
title: "Hello Post #1",
category: {
id: 1,
name: "Category saved by cascades #1"
}
}, {
id: 2,
title: "Hello Post #2",
category: {
id: 2,
name: "Category saved by cascades #2"
}
}]);
})));
});
});