added basic lazy loading support; fixed few non related bugs

This commit is contained in:
Umed Khudoiberdiev 2016-05-03 21:19:29 +05:00
parent b36937c099
commit 60fc079f85
17 changed files with 414 additions and 107 deletions

View File

@ -0,0 +1,73 @@
import {createConnection, CreateConnectionOptions} from "../../src/typeorm";
import {Post} from "./entity/Post";
import {Author} from "./entity/Author";
const options: CreateConnectionOptions = {
driver: "mysql",
connection: {
host: "192.168.99.100",
port: 3306,
username: "root",
password: "admin",
database: "test",
autoSchemaCreate: true,
logging: {
logOnlyFailedQueries: true,
logFailedQueryError: true
}
},
entities: [Post, Author]
};
createConnection(options).then(connection => {
let postRepository = connection.getRepository(Post);
let authorRepository = connection.getRepository(Author);
let author = authorRepository.create();
author.name = "Umed";
let post = postRepository.create();
post.text = "Hello how are you?";
post.title = "hello";
post.author = author.asPromise();
// same as: post.author = Promise.resolve(author);
postRepository
.persist(post)
.then(post => {
console.log("Post has been saved. Lets save post from inverse side.");
console.log(post);
let secondPost = postRepository.create();
secondPost.text = "Second post";
secondPost.title = "About second post";
author.posts = Promise.resolve([secondPost]);
return authorRepository.persist(author);
}).then(author => {
console.log("Author with a new post has been saved. Lets try to update post in the author");
return author.posts.then(posts => {
posts[0].title = "should be updated second post";
return authorRepository.persist(author);
});
})
.then(updatedAuthor => {
console.log("Author has been updated: ", updatedAuthor);
console.log("Now lets load all posts with their authors:");
return postRepository.find({ alias: "post", leftJoinAndSelect: { author: "post.author" } });
})
.then(posts => {
console.log("Posts are loaded: ", posts);
console.log("Now lets delete a post");
posts[0].author = Promise.resolve(null);
posts[1].author = Promise.resolve(null);
return postRepository.persist(posts[0]);
})
.then(posts => {
console.log("Two post's author has been removed.");
})
.catch(error => console.log(error.stack));
}, error => console.log("Cannot connect: ", error));

View File

@ -0,0 +1,28 @@
import {PrimaryColumn, Column} from "../../../src/columns";
import {Table} from "../../../src/tables";
import {Post} from "./Post";
import {OneToMany} from "../../../src/decorator/relations/OneToMany";
@Table("sample18_author")
export class Author {
@PrimaryColumn("int", { generated: true })
id: number;
@Column()
name: string;
@OneToMany(type => Post, post => post.author, {
cascadeInsert: true,
cascadeUpdate: true
})
posts: Promise<Post[]>;
/**
* You can add this helper method.
*/
asPromise() {
return Promise.resolve(this);
}
}

View File

@ -0,0 +1,25 @@
import {PrimaryColumn, Column} from "../../../src/columns";
import {Table} from "../../../src/tables";
import {Author} from "./Author";
import {ManyToOne} from "../../../src/decorator/relations/ManyToOne";
@Table("sample18_post")
export class Post {
@PrimaryColumn("int", { generated: true })
id: number;
@Column()
title: string;
@Column()
text: string;
@ManyToOne(type => Author, author => author.posts, {
cascadeInsert: true,
cascadeRemove: true,
onDelete: "SET NULL"
})
author: Promise<Author|null>;
}

View File

@ -31,6 +31,7 @@ export function Column(typeOrOptions?: ColumnType|ColumnOptions, options?: Colum
}
return function (object: Object, propertyName: string) {
// todo: need to store not string type, but original type instead? (like in relation metadata)
const reflectedType = ColumnTypes.typeToString(Reflect.getMetadata("design:type", object, propertyName));
// if type is not given implicitly then try to guess it

View File

@ -37,10 +37,13 @@ export function ManyToMany<T>(typeFunction: (type?: any) => ConstructorFunction<
return function (object: Object, propertyName: string) {
if (!options) options = {} as RelationOptions;
const reflectedType = Reflect.getMetadata("design:type", object, propertyName);
defaultMetadataStorage().relationMetadatas.add(new RelationMetadata({
target: object.constructor,
propertyName: propertyName,
propertyType: reflectedType,
relationType: RelationTypes.MANY_TO_MANY,
type: typeFunction,
inverseSideProperty: inverseSideProperty,

View File

@ -38,9 +38,12 @@ export function ManyToOne<T>(typeFunction: (type?: any) => ConstructorFunction<T
return function (object: Object, propertyName: string) {
if (!options) options = {} as RelationOptions;
const reflectedType = Reflect.getMetadata("design:type", object, propertyName);
defaultMetadataStorage().relationMetadatas.add(new RelationMetadata({
target: object.constructor,
propertyName: propertyName,
propertyType: reflectedType,
relationType: RelationTypes.MANY_TO_ONE,
type: typeFunction,
inverseSideProperty: inverseSideProperty,

View File

@ -35,9 +35,12 @@ export function OneToMany<T>(typeFunction: (type?: any) => ConstructorFunction<T
return function (object: Object, propertyName: string) {
if (!options) options = {} as RelationOptions;
const reflectedType = Reflect.getMetadata("design:type", object, propertyName);
defaultMetadataStorage().relationMetadatas.add(new RelationMetadata({
target: object.constructor,
propertyName: propertyName,
propertyType: reflectedType,
relationType: RelationTypes.ONE_TO_MANY,
type: typeFunction,
inverseSideProperty: inverseSideProperty,

View File

@ -35,9 +35,12 @@ export function OneToOne<T>(typeFunction: (type?: any) => ConstructorFunction<T>
return function (object: Object, propertyName: string) {
if (!options) options = {} as RelationOptions;
const reflectedType = Reflect.getMetadata("design:type", object, propertyName);
defaultMetadataStorage().relationMetadatas.add(new RelationMetadata({
target: object.constructor,
propertyName: propertyName,
propertyType: reflectedType,
relationType: RelationTypes.ONE_TO_ONE,
type: typeFunction,
inverseSideProperty: inverseSideProperty,

View File

@ -91,6 +91,11 @@ export class RelationMetadata extends PropertyMetadata {
*/
readonly onDelete: OnDeleteType;
/**
* The real reflected property type.
*/
readonly propertyType: any;
// ---------------------------------------------------------------------
// Private Properties
// ---------------------------------------------------------------------
@ -121,6 +126,8 @@ export class RelationMetadata extends PropertyMetadata {
if (args.options.name)
this._name = args.options.name;
if (args.propertyType)
this.propertyType = args.propertyType;
if (args.options.cascadeInsert || args.options.cascadeAll)
this.isCascadeInsert = true;
if (args.options.cascadeUpdate || args.options.cascadeAll)
@ -188,6 +195,10 @@ export class RelationMetadata extends PropertyMetadata {
get hasInverseSide(): boolean {
return !!this.inverseRelation;
}
get isLazy(): boolean {
return this.propertyType && this.propertyType.name && this.propertyType.name.toLowerCase() === "promise";
}
// ---------------------------------------------------------------------
// Private Methods

View File

@ -17,6 +17,11 @@ export interface RelationMetadataArgs {
*/
propertyName: string;
/**
* Original (reflected) class's property type.
*/
propertyType: any;
/**
* Type of relation. Can be one of the value of the RelationTypes class.
*/

View File

@ -8,6 +8,7 @@ import {JunctionInsertOperation} from "./operation/JunctionInsertOperation";
import {UpdateOperation} from "./operation/UpdateOperation";
import {CascadesNotAllowedError} from "./error/CascadesNotAllowedError";
import {RemoveOperation} from "./operation/RemoveOperation";
import {EntityMetadataCollection} from "../metadata/collection/EntityMetadataCollection";
/**
* 1. collect all exist objects from the db entity
@ -45,7 +46,7 @@ export class EntityPersistOperationBuilder {
// Constructor
// -------------------------------------------------------------------------
constructor(private entityMetadatas: EntityMetadata[]) {
constructor(private entityMetadatas: EntityMetadataCollection) {
}
// -------------------------------------------------------------------------
@ -75,7 +76,6 @@ export class EntityPersistOperationBuilder {
persistOperation.updatesByRelations = this.updateRelations(persistOperation.inserts, persistedEntity);
persistOperation.removes = this.findCascadeRemovedEntities(metadata, dbEntity, allPersistedEntities, undefined, undefined, undefined);
persistOperation.junctionRemoves = this.findJunctionRemoveOperations(metadata, dbEntity, allPersistedEntities);
return persistOperation;
}
@ -110,7 +110,7 @@ export class EntityPersistOperationBuilder {
fromRelation?: RelationMetadata,
operations: InsertOperation[] = []): InsertOperation[] {
// const metadata = this.connection.getEntityMetadata(newEntity.constructor);
const metadata = this.entityMetadatas.find(metadata => metadata.target === newEntity.constructor);
const metadata = this.entityMetadatas.findByTarget(newEntity.constructor);
const isObjectNew = !this.findEntityWithId(dbEntities, metadata.target, newEntity[metadata.primaryColumn.name]);
// if object is new and should be inserted, we check if cascades are allowed before add it to operations list
@ -121,18 +121,16 @@ export class EntityPersistOperationBuilder {
operations.push(new InsertOperation(newEntity));
}
metadata.relations
.filter(relation => !!newEntity[relation.propertyName])
.forEach(relation => {
const value = newEntity[relation.propertyName];
if (value instanceof Array) {
value.forEach((subEntity: any) => {
this.findCascadeInsertedEntities(subEntity, dbEntities, relation, operations);
});
} else {
this.findCascadeInsertedEntities(value, dbEntities, relation, operations);
}
});
metadata.relations.forEach(relation => {
const value = this.getEntityRelationValue(relation, newEntity);
if (!value) return;
if (value instanceof Array) {
value.map((subValue: any) => this.findCascadeInsertedEntities(subValue, dbEntities, relation, operations));
} else {
this.findCascadeInsertedEntities(value, dbEntities, relation, operations);
}
});
return operations;
}
@ -140,8 +138,8 @@ export class EntityPersistOperationBuilder {
private findCascadeUpdateEntities(metadata: EntityMetadata,
dbEntity: any,
newEntity: any,
fromRelation?: RelationMetadata): UpdateOperation[] {
let operations: UpdateOperation[] = [];
fromRelation?: RelationMetadata,
operations: UpdateOperation[] = []): UpdateOperation[] {
if (!dbEntity)
return operations;
@ -154,27 +152,29 @@ export class EntityPersistOperationBuilder {
operations.push(new UpdateOperation(newEntity, entityId, diff));
}
metadata.relations
.filter(relation => newEntity[relation.propertyName] && dbEntity[relation.propertyName])
.forEach(relation => {
const relMetadata = relation.relatedEntityMetadata;
const relationIdColumnName = relMetadata.primaryColumn.name;
const value = newEntity[relation.propertyName];
const dbValue = dbEntity[relation.propertyName];
metadata.relations.forEach(relation => {
const relMetadata = relation.relatedEntityMetadata;
const relationIdColumnName = relMetadata.primaryColumn.name;
const value = this.getEntityRelationValue(relation, newEntity);
const dbValue = this.getEntityRelationValue(relation, dbEntity);
if (value instanceof Array) {
value.forEach((subEntity: any) => {
const subDbEntity = dbValue.find((subDbEntity: any) => {
return subDbEntity[relationIdColumnName] === subEntity[relationIdColumnName];
});
const relationOperations = this.findCascadeUpdateEntities(relMetadata, subDbEntity, subEntity, relation);
operations = operations.concat(relationOperations);
if (!value || !dbValue)
return;
if (value instanceof Array) {
value.forEach((subEntity: any) => {
const subDbEntity = dbValue.find((subDbEntity: any) => {
return subDbEntity[relationIdColumnName] === subEntity[relationIdColumnName];
});
} else {
const relationOperations = this.findCascadeUpdateEntities(relMetadata, dbValue, newEntity[relation.propertyName], relation);
operations = operations.concat(relationOperations);
}
});
const relationOperations = this.findCascadeUpdateEntities(relMetadata, subDbEntity, subEntity, relation, operations);
operations.push(...relationOperations);
});
} else {
const relationOperations = this.findCascadeUpdateEntities(relMetadata, dbValue, value, relation, operations);
operations.push(...relationOperations);
}
});
return operations;
}
@ -201,21 +201,21 @@ export class EntityPersistOperationBuilder {
operations.push(new RemoveOperation(dbEntity, entityId, <EntityMetadata> fromMetadata, fromRelation, fromEntityId));
}
metadata.relations
.filter(relation => !!dbEntity[relation.propertyName])
.forEach(relation => {
const dbValue = dbEntity[relation.propertyName];
const relMetadata = relation.relatedEntityMetadata;
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 relationOperations = this.findCascadeRemovedEntities(relMetadata, dbValue, allPersistedEntities, relation, metadata, dbEntity[metadata.primaryColumn.name], isObjectRemoved);
operations = operations.concat(relationOperations);
}
}, []);
metadata.relations.forEach(relation => {
const dbValue = this.getEntityRelationValue(relation, dbEntity);
const relMetadata = relation.relatedEntityMetadata;
if (!dbValue) return;
if (dbValue instanceof Array) {
dbValue.forEach((subDbEntity: any) => {
const relationOperations = this.findCascadeRemovedEntities(relMetadata, subDbEntity, allPersistedEntities, relation, metadata, dbEntity[metadata.primaryColumn.name], isObjectRemoved);
operations.push(...relationOperations);
});
} else {
const relationOperations = this.findCascadeRemovedEntities(relMetadata, dbValue, allPersistedEntities, relation, metadata, dbEntity[metadata.primaryColumn.name], isObjectRemoved);
operations.push(...relationOperations);
}
}, []);
return operations;
}
@ -234,31 +234,37 @@ export class EntityPersistOperationBuilder {
}, <UpdateByRelationOperation[]> []);
}
private findRelationsWithEntityInside(insertOperation: InsertOperation, entityToSearchIn: any) {
// const metadata = this.connection.getEntityMetadata(entityToSearchIn.constructor);
const metadata = this.entityMetadatas.find(metadata => metadata.target === entityToSearchIn.constructor);
private findRelationsWithEntityInside(insertOperation: InsertOperation, entityToSearchIn: any): UpdateByRelationOperation[] {
// updateByt metadata = this.connection.getEntityMetadata(entityToSearchIn.constructor);
const metadata = this.entityMetadatas.findByTarget(entityToSearchIn.constructor);
const operations: UpdateByRelationOperation[] = [];
metadata.relations.forEach(relation => {
const value = this.getEntityRelationValue(relation, entityToSearchIn);
if (!value) return;
return metadata.relations.reduce((operations, relation) => {
const value = entityToSearchIn[relation.propertyName];
if (value instanceof Array) {
value.forEach((sub: any) => {
if (!relation.isManyToMany && sub === insertOperation.entity)
operations.push(new UpdateByRelationOperation(entityToSearchIn, insertOperation, relation));
const subOperations = this.findRelationsWithEntityInside(insertOperation, sub);
operations.concat(subOperations);
operations.push(...subOperations);
});
} else if (value) {
if (value === insertOperation.entity) {
operations.push(new UpdateByRelationOperation(entityToSearchIn, insertOperation, relation));
}
const subOperations = this.findRelationsWithEntityInside(insertOperation, value);
operations.concat(subOperations);
}
operations.push(...subOperations);
return operations;
}, <UpdateByRelationOperation[]> []);
}
});
return operations;
}
private findJunctionInsertOperations(metadata: EntityMetadata, newEntity: any, dbEntities: EntityWithId[]): JunctionInsertOperation[] {
@ -286,7 +292,7 @@ export class EntityPersistOperationBuilder {
}
const subOperations = this.findJunctionInsertOperations(relationMetadata, subEntity, dbEntities);
operations = operations.concat(subOperations);
operations.push(...subOperations);
});
return operations;
}, <JunctionInsertOperation[]> []);
@ -320,7 +326,7 @@ export class EntityPersistOperationBuilder {
}
const subOperations = this.findJunctionRemoveOperations(relationMetadata, subEntity, newEntities);
operations = operations.concat(subOperations);
operations.push(...subOperations);
});
return operations;
}, <JunctionInsertOperation[]> []);
@ -355,5 +361,9 @@ export class EntityPersistOperationBuilder {
return isAllowed;
}
private getEntityRelationValue(relation: RelationMetadata, entity: any) {
return (entity[relation.propertyName] instanceof Promise && relation.isLazy) ? entity["__" + relation.propertyName + "__"] : entity[relation.propertyName];
}
}

View File

@ -58,7 +58,11 @@ export class PersistOperationExecutor {
* Broadcast all before persistment events - beforeInsert, beforeUpdate and beforeRemove events.
*/
private broadcastBeforeEvents(persistOperation: PersistOperation) {
/*console.log("persistOperation.allPersistedEntities: ", persistOperation.allPersistedEntities);
console.log("inserts", persistOperation.inserts);
console.log("updates", persistOperation.updates);*/
const insertEvents = persistOperation.inserts.map(insertOperation => {
const persistedEntityWithId = persistOperation.allPersistedEntities.find(e => e.entity === insertOperation.entity);
return this.broadcaster.broadcastBeforeInsertEvent(persistedEntityWithId.entity);
@ -193,7 +197,7 @@ export class PersistOperationExecutor {
if (metadata.createDateColumn)
insertOperation.entity[metadata.createDateColumn.propertyName] = insertOperation.date;
if (metadata.versionColumn)
insertOperation.entity[metadata.versionColumn.propertyName] = 1;
insertOperation.entity[metadata.versionColumn.propertyName]++;
});
persistOperation.updates.forEach(updateOperation => {
const metadata = this.entityMetadatas.findByTarget(updateOperation.entity.constructor);
@ -222,7 +226,8 @@ export class PersistOperationExecutor {
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;
const relatedInsertOperation = insertOperations.find(o => o.entity === operation.targetEntity);
const idInInserts = relatedInsertOperation ? relatedInsertOperation.entityId : null;
if (operation.updatedRelation.isOneToMany) {
const metadata = this.entityMetadatas.findByTarget(operation.insertOperation.entity.constructor);
tableName = metadata.table.name;

View File

@ -333,13 +333,19 @@ export class QueryBuilder<Entity> {
return this.driver.query<any[]>(queryWithIds);
})
.then(results => this.rawResultsToEntities(results))
.then(results => this.addLazyProperties(results))
.then(results => this.broadcaster.broadcastLoadEventsForAll(results).then(() => results));
} else {
return this.driver
.query<any[]>(this.getSql())
.then(results => this.rawResultsToEntities(results))
.then(results => this.broadcaster.broadcastLoadEventsForAll(results).then(() => results));
.then(results => this.addLazyProperties(results))
.then(results => {
return this.broadcaster
.broadcastLoadEventsForAll(results)
.then(() => results);
});
}
}
@ -443,6 +449,43 @@ export class QueryBuilder<Entity> {
const transformer = new RawSqlResultsToEntityTransformer(this.driver, this.aliasMap);
return transformer.transform(results);
}
protected addLazyProperties(entities: any[]) {
entities.forEach(entity => {
const metadata = this.entityMetadatas.findByTarget(entity.constructor);
metadata.relations
.filter(relation => relation.isLazy)
.forEach(relation => {
const index = "__" + relation.propertyName + "__";
Object.defineProperty(entity, relation.propertyName, {
get: () => {
if (entity[index])
return Promise.resolve(entity[index]);
// find object metadata and try to load
return new QueryBuilder(this.driver, this.entityMetadatas, this.broadcaster)
.select(relation.propertyName)
.from(relation.target, relation.propertyName) // todo: change `id` after join column implemented
.where(relation.propertyName + ".id=:" + relation.propertyName + "Id")
.setParameter(relation.propertyName + "Id", entity[index])
.getSingleResult()
.then(result => {
entity[index] = result;
return entity[index];
});
},
set: (promise: Promise<any>) => {
if (promise instanceof Promise) {
promise.then(result => entity[index] = result);
} else {
entity[index] = promise;
}
}
});
});
});
return entities;
}
protected createSelectExpression() {
// todo throw exception if selects or from is missing

View File

@ -58,10 +58,12 @@ export class PlainObjectToDatabaseEntityTransformer<Entity> {
.filter(relation => {
// we only need to load empty relations for first-level depth objects, otherwise removal can break
// this is not reliable, refactor this part later
return isFirstLevelDepth || !(object[relation.propertyName] instanceof Array) || object[relation.propertyName].length > 0;
const value = (object[relation.propertyName] instanceof Promise && relation.isLazy) ? object["__" + relation.propertyName + "__"] : object[relation.propertyName];
return isFirstLevelDepth || !(value instanceof Array) || value.length > 0;
})
.map(relation => {
let value = object[relation.propertyName];
let value = (object[relation.propertyName] instanceof Promise && relation.isLazy) ? object["__" + relation.propertyName + "__"] : object[relation.propertyName];
// let value = object[relation.propertyName];
if (value instanceof Array)
value = Object.assign({}, ...value);

View File

@ -72,7 +72,11 @@ export class RawSqlResultsToEntityTransformer {
const relatedEntities = this.groupAndTransform(rawSqlResults, relationAlias);
const result = (relation.isManyToOne || relation.isOneToOne) ? relatedEntities[0] : relatedEntities;
if (result) {
entity[relation.propertyName] = result;
if (relation.isLazy) {
entity["__" + relation.propertyName + "__"] = result;
} else {
entity[relation.propertyName] = result;
}
hasData = true;
}
}

View File

@ -35,7 +35,7 @@ export class Repository<Entity> {
private entityMetadatas: EntityMetadataCollection,
private metadata: EntityMetadata) {
this.driver = connection.driver;
this.broadcaster = new Broadcaster(entityMetadatas, connection.eventSubscribers, connection.entityListeners);
this.broadcaster = new Broadcaster(entityMetadatas, connection.eventSubscribers, connection.entityListeners); // todo: inject broadcaster from connection
this.persistOperationExecutor = new PersistOperationExecutor(connection.driver, entityMetadatas, this.broadcaster);
this.entityPersistOperationBuilder = new EntityPersistOperationBuilder(entityMetadatas);
this.plainObjectToEntityTransformer = new PlainObjectToNewEntityTransformer();
@ -72,9 +72,45 @@ export class Repository<Entity> {
*/
create(fromRawEntity?: Object): Entity {
if (fromRawEntity)
return this.plainObjectToEntityTransformer.transform(fromRawEntity, this.metadata);
return this.addLazyProperties(this.plainObjectToEntityTransformer.transform(fromRawEntity, this.metadata));
return <Entity> this.metadata.create();
return <Entity> this.addLazyProperties(this.metadata.create());
}
// todo: duplication
private addLazyProperties(entity: any) {
const metadata = this.entityMetadatas.findByTarget(entity.constructor);
metadata.relations
.filter(relation => relation.isLazy)
.forEach(relation => {
const index = "__" + relation.propertyName + "__";
Object.defineProperty(entity, relation.propertyName, {
get: () => {
if (entity[index])
return Promise.resolve(entity[index]);
// find object metadata and try to load
return new QueryBuilder(this.driver, this.entityMetadatas, this.broadcaster)
.select(relation.propertyName)
.from(relation.target, relation.propertyName) // todo: change `id` after join column implemented
.where(relation.propertyName + ".id=:" + relation.propertyName + "Id")
.setParameter(relation.propertyName + "Id", entity[index])
.getSingleResult()
.then(result => {
entity[index] = result;
return entity[index];
});
},
set: (promise: Promise<any>) => {
if (promise instanceof Promise) {
promise.then(result => entity[index] = result);
} else {
entity[index] = promise;
}
}
});
});
return entity;
}
/**
@ -107,32 +143,53 @@ export class Repository<Entity> {
* else if entity already exist in the database then it updates it.
*/
persist(entity: Entity): Promise<Entity> {
let loadedDbEntity: any;
const allPersistedEntities = this.extractObjectsById(entity, this.metadata);
const promise: Promise<Entity> = !this.hasId(entity) ? Promise.resolve<Entity|null>(null) : this.initialize(entity);
return promise
let loadedDbEntity: any, allPersistedEntities: EntityWithId[];
return Promise.resolve() // resolve is required because need to wait until lazy relations loaded
.then(() => {
return this.extractObjectsById(entity, this.metadata);
})
.then(allPersisted => {
allPersistedEntities = allPersisted;
if (!this.hasId(entity))
return Promise.resolve<Entity|null>(null);
return this.initialize(entity);
})
.then(dbEntity => {
loadedDbEntity = dbEntity;
const entityWithIds = dbEntity ? this.extractObjectsById(dbEntity, this.metadata) : [];
return dbEntity ? this.extractObjectsById(dbEntity, this.metadata) : [];
}).then(entityWithIds => {
return this.findNotLoadedIds(entityWithIds, allPersistedEntities);
}) // need to find db entities that were not loaded by initialize method
.then(allDbEntities => {
const persistOperation = this.entityPersistOperationBuilder.buildFullPersistment(this.metadata, loadedDbEntity, entity, allDbEntities, allPersistedEntities);
return this.entityPersistOperationBuilder.buildFullPersistment(this.metadata, loadedDbEntity, entity, allDbEntities, allPersistedEntities);
})
.then(persistOperation => {
return this.persistOperationExecutor.executePersistOperation(persistOperation);
}).then(() => entity);
})
.then(() => entity);
}
/**
* Removes a given entity from the database.
*/
remove(entity: Entity): Promise<Entity> {
return this.initialize(entity).then(dbEntity => {
(<any> entity)[this.metadata.primaryColumn.name] = undefined;
const dbEntities = this.extractObjectsById(dbEntity, this.metadata);
const allPersistedEntities = this.extractObjectsById(entity, this.metadata);
const persistOperation = this.entityPersistOperationBuilder.buildOnlyRemovement(this.metadata, dbEntity, entity, dbEntities, allPersistedEntities);
return this.persistOperationExecutor.executePersistOperation(persistOperation);
}).then(() => entity);
let dbEntity: Entity;
return this
.initialize(entity)
.then(dbEnt => {
dbEntity = dbEnt;
(<any> entity)[this.metadata.primaryColumn.name] = undefined;
return Promise.all<any>([
this.extractObjectsById(dbEntity, this.metadata),
this.extractObjectsById(entity, this.metadata)
]);
})
// .then(([dbEntities, allPersistedEntities]: [EntityWithId[], EntityWithId[]]) => {
.then(results => {
const persistOperation = this.entityPersistOperationBuilder.buildOnlyRemovement(this.metadata, dbEntity, entity, results[0], results[1]);
return this.persistOperationExecutor.executePersistOperation(persistOperation);
}).then(() => entity);
}
/**
@ -304,27 +361,55 @@ export class Repository<Entity> {
/**
* Extracts unique objects from given entity and all its downside relations.
*/
private extractObjectsById(entity: any, metadata: EntityMetadata, entityWithIds: EntityWithId[] = []): EntityWithId[] {
metadata.relations
.filter(relation => !!entity[relation.propertyName])
.forEach(relation => {
private extractObjectsById(entity: any, metadata: EntityMetadata, entityWithIds: EntityWithId[] = []): Promise<EntityWithId[]> {
const promises = metadata.relations
// .filter(relation => !!entity[relation.propertyName])
.map(relation => {
const relMetadata = relation.relatedEntityMetadata;
const value = entity[relation.propertyName];
// const value = ;
const value = (entity[relation.propertyName] instanceof Promise && relation.isLazy) ? entity["__" + relation.propertyName + "__"] : entity[relation.propertyName];
if (!value)
return undefined;
if (value instanceof Array) {
value.forEach((subEntity: any) => this.extractObjectsById(subEntity, relMetadata, entityWithIds));
const subPromises = value.map((subEntity: any) => {
return this.extractObjectsById(subEntity, relMetadata, entityWithIds);
});
return Promise.all(subPromises);
/*} else if (value instanceof Promise && relation.isLazy) {
return value.then((resolvedValue: any) => { // todo: duplicate logic
if (resolvedValue && resolvedValue instanceof Array) {
const promises = resolvedValue.map((subEntity: any) => {
return this.extractObjectsById(subEntity, relMetadata, entityWithIds);
});
return Promise.all(promises);
} else if (resolvedValue) {
return this.extractObjectsById(resolvedValue, relMetadata, entityWithIds);
}
});*/
} else {
this.extractObjectsById(value, relMetadata, entityWithIds);
return this.extractObjectsById(value, relMetadata, entityWithIds);
}
});
})
.filter(result => !!result);
if (!entityWithIds.find(entityWithId => entityWithId.entity === entity)) {
entityWithIds.push({
id: entity[metadata.primaryColumn.name],
entity: entity
});
}
return entityWithIds;
return Promise.all(promises).then(() => {
if (!entityWithIds.find(entityWithId => entityWithId.entity === entity)) {
entityWithIds.push({
id: entity[metadata.primaryColumn.name],
entity: entity
});
}
return entityWithIds;
});
}
}

View File

@ -113,8 +113,10 @@ export class Broadcaster {
}
broadcastLoadEvents(entity: any): Promise<void> {
// const metadata = this.getEntityMetadata(entity.constructor);
const metadata = this.entityMetadatas.find(metadata => metadata.target === entity.constructor);
if (entity instanceof Promise)
return Promise.resolve();
const metadata = this.entityMetadatas.findByTarget(entity.constructor);
let promises: Promise<any>[] = [];
metadata
@ -143,7 +145,8 @@ export class Broadcaster {
}
broadcastLoadEventsForAll(entities: any[]): Promise<void> {
return Promise.all(entities.map(entity => this.broadcastLoadEvents(entity))).then(() => {});
const promises = entities.map(entity => this.broadcastLoadEvents(entity));
return Promise.all(promises).then(() => {});
}
// -------------------------------------------------------------------------