mirror of
https://github.com/typeorm/typeorm.git
synced 2025-12-08 21:26:23 +00:00
added basic lazy loading support; fixed few non related bugs
This commit is contained in:
parent
b36937c099
commit
60fc079f85
73
sample/sample18-lazy-relations/app.ts
Normal file
73
sample/sample18-lazy-relations/app.ts
Normal 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));
|
||||
28
sample/sample18-lazy-relations/entity/Author.ts
Normal file
28
sample/sample18-lazy-relations/entity/Author.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
25
sample/sample18-lazy-relations/entity/Post.ts
Normal file
25
sample/sample18-lazy-relations/entity/Post.ts
Normal 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>;
|
||||
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
*/
|
||||
|
||||
@ -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];
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@ -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(() => {});
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user