refactored lazy relations wrapper

This commit is contained in:
Umed Khudoiberdiev 2017-05-17 10:34:51 +05:00
parent 74ed97b399
commit e03f54f341

View File

@ -4,7 +4,8 @@ import {Connection} from "../connection/Connection";
import {ObjectLiteral} from "../common/ObjectLiteral";
/**
* This class wraps entities and provides functions there to lazily load its relations.
* Wraps entities and creates getters/setters for their relations
* to be able to lazily load relations when accessing these relations.
*/
export class LazyRelationsWrapper {
@ -19,152 +20,43 @@ export class LazyRelationsWrapper {
// Public Methods
// -------------------------------------------------------------------------
wrap(object: Object, relation: RelationMetadata) {
const connection = this.connection;
const index = "__" + relation.propertyName + "__";
const promiseIndex = "__promise__" + relation.propertyName + "__";
const resolveIndex = "__has__" + relation.propertyName + "__";
/**
* Wraps given entity and creates getters/setters for its given relation
* to be able to lazily load data when accessing these relation.
*/
wrap(object: ObjectLiteral, relation: RelationMetadata) {
const that = this;
const dataIndex = "__" + relation.propertyName + "__"; // in what property of the entity loaded data will be stored
const promiseIndex = "__promise_" + relation.propertyName + "__"; // in what property of the entity loading promise will be stored
const resolveIndex = "__has_" + relation.propertyName + "__"; // indicates if relation data already was loaded or not
Object.defineProperty(object, relation.propertyName, {
get: function() {
if (this[resolveIndex] === true)
return Promise.resolve(this[index]);
if (this[promiseIndex])
return this[promiseIndex];
const qb = new QueryBuilder(connection);
if (this[resolveIndex] === true) // if related data already was loaded then simply return it
return Promise.resolve(this[dataIndex]);
if (relation.isManyToOne || relation.isOneToOneOwner) {
const joinColumns = relation.isOwning ? relation.joinColumns : relation.inverseRelation.joinColumns;
const conditions = joinColumns.map(joinColumn => {
return `${relation.entityMetadata.name}.${relation.propertyName} = ${relation.propertyName}.${joinColumn.referencedColumn!.propertyName}`;
}).join(" AND ");
// (ow) post.category<=>category.post
// loaded: category from post
// example: SELECT category.id AS category_id, category.name AS category_name FROM category category
// INNER JOIN post Post ON Post.category=category.id WHERE Post.id=1
qb.select(relation.propertyName) // category
.from(relation.type, relation.propertyName) // Category, category
.innerJoin(relation.entityMetadata.target as Function, relation.entityMetadata.name, conditions);
joinColumns.forEach(joinColumn => {
qb.andWhere(`${relation.entityMetadata.name}.${joinColumn.referencedColumn!.databaseName} = :${joinColumn.referencedColumn!.databaseName}`)
.setParameter(`${joinColumn.referencedColumn!.databaseName}`, this[joinColumn.referencedColumn!.databaseName]);
});
this[promiseIndex] = qb.getOne().then(result => {
this[index] = result;
this[resolveIndex] = true;
delete this[promiseIndex];
return this[index];
}).catch(err => {
throw err;
});
if (this[promiseIndex]) // if related data is loading then return a promise that loads it
return this[promiseIndex];
} else if (relation.isOneToMany || relation.isOneToOneNotOwner) {
// nothing is loaded yet, load relation data and save it in the model once they are loaded
this[promiseIndex] = that.loadRelationResults(relation, this).then(result => {
this[dataIndex] = result;
this[resolveIndex] = true;
delete this[promiseIndex];
return this[dataIndex];
/*
SELECT post
FROM post post
WHERE post.[joinColumn.name] = this[joinColumn.referencedColumn]
*/
qb.select(relation.propertyName)
.from(relation.inverseRelation.entityMetadata.target, relation.propertyName);
relation.inverseRelation.joinColumns.forEach(joinColumn => {
qb.andWhere(`${relation.propertyName}.${joinColumn.propertyName} = :${joinColumn.referencedColumn!.propertyName}`)
.setParameter(`${joinColumn.referencedColumn!.propertyName}`, this[joinColumn.referencedColumn!.propertyName]);
});
const result = relation.isOneToMany ? qb.getMany() : qb.getOne();
this[promiseIndex] = result.then(results => {
this[index] = results;
this[resolveIndex] = true;
delete this[promiseIndex];
return this[index];
}).catch(err => {
throw err;
});
return this[promiseIndex];
} else { // ManyToMany
const mainAlias = relation.propertyName;
const joinAlias = relation.junctionEntityMetadata.tableName;
let joinColumnConditions: string[] = [];
let inverseJoinColumnConditions: string[] = [];
let parameters: ObjectLiteral;
if (relation.isOwning) {
/*
SELECT category
FROM category category
INNER JOIN post_categories post_categories
ON post_categories.postId = :postId
AND post_categories.categoryId = category.id
*/
joinColumnConditions = relation.joinColumns.map(joinColumn => {
return `${joinAlias}.${joinColumn.propertyName} = :${joinColumn.propertyName}`;
});
inverseJoinColumnConditions = relation.inverseJoinColumns.map(inverseJoinColumn => {
return `${joinAlias}.${inverseJoinColumn.propertyName}=${mainAlias}.${inverseJoinColumn.referencedColumn!.propertyName}`;
});
parameters = relation.joinColumns.reduce((parameters, joinColumn) => {
parameters[joinColumn.propertyName] = this[joinColumn.referencedColumn!.propertyName];
return parameters;
}, {} as ObjectLiteral);
} else {
/*
SELECT post
FROM post post
INNER JOIN post_categories post_categories
ON post_categories.postId = post.id
AND post_categories.categoryId = post_categories.categoryId
*/
joinColumnConditions = relation.inverseRelation.joinColumns.map(joinColumn => {
return `${joinAlias}.${joinColumn.propertyName} = ${mainAlias}.${joinColumn.referencedColumn!.propertyName}`;
});
inverseJoinColumnConditions = relation.inverseRelation.inverseJoinColumns.map(inverseJoinColumn => {
return `${joinAlias}.${inverseJoinColumn.propertyName} = :${inverseJoinColumn.propertyName}`;
});
parameters = relation.inverseRelation.inverseJoinColumns.reduce((parameters, joinColumn) => {
parameters[joinColumn.propertyName] = this[joinColumn.referencedColumn!.propertyName];
return parameters;
}, {} as ObjectLiteral);
}
const conditions = joinColumnConditions.concat(inverseJoinColumnConditions).join(" AND ");
qb.select(mainAlias)
.from(relation.type, mainAlias)
.innerJoin(joinAlias, joinAlias, conditions)
.setParameters(parameters);
this[promiseIndex] = qb.getMany().then(results => {
this[index] = results;
this[resolveIndex] = true;
delete this[promiseIndex];
return this[index];
}).catch(err => {
throw err;
});
return this[promiseIndex];
}
}); // .catch((err: any) => { throw err; });
return this[promiseIndex];
},
set: function(promise: Promise<any>) {
if (promise instanceof Promise) {
if (promise instanceof Promise) { // if set data is a promise then wait for its resolve and save in the object
promise.then(result => {
this[index] = result;
this[dataIndex] = result;
this[resolveIndex] = true;
});
} else {
this[index] = promise;
} else { // if its direct data set (non promise, probably not safe-typed)
this[dataIndex] = promise;
this[resolveIndex] = true;
}
},
@ -172,4 +64,133 @@ export class LazyRelationsWrapper {
});
}
// -------------------------------------------------------------------------
// Protected Methods
// -------------------------------------------------------------------------
/**
* Loads relation data for the given entity and its relation.
*/
protected loadRelationResults(relation: RelationMetadata, entity: ObjectLiteral): Promise<any> {
if (relation.isManyToOne || relation.isOneToOneOwner) {
return this.loadManyToOneOrOneToOneOwner(relation, entity);
} else if (relation.isOneToMany || relation.isOneToOneNotOwner) {
return this.loadOneToManyOrOneToOneNotOwner(relation, entity);
} else if (relation.isManyToManyOwner) {
return this.loadManyToManyOwner(relation, entity);
} else { // many-to-many non owner
return this.loadManyToManyNotOwner(relation, entity);
}
}
/**
* Loads data for many-to-one and one-to-one owner relations.
*
* (ow) post.category<=>category.post
* loaded: category from post
* example: SELECT category.id AS category_id, category.name AS category_name FROM category category
* INNER JOIN post Post ON Post.category=category.id WHERE Post.id=1
*/
protected loadManyToOneOrOneToOneOwner(relation: RelationMetadata, entity: ObjectLiteral): Promise<any> {
const joinColumns = relation.isOwning ? relation.joinColumns : relation.inverseRelation.joinColumns;
const conditions = joinColumns.map(joinColumn => {
return `${relation.entityMetadata.name}.${relation.propertyName} = ${relation.propertyName}.${joinColumn.referencedColumn!.propertyName}`;
}).join(" AND ");
const qb = new QueryBuilder(this.connection)
.select(relation.propertyName) // category
.from(relation.type, relation.propertyName) // Category, category
.innerJoin(relation.entityMetadata.target as Function, relation.entityMetadata.name, conditions);
joinColumns.forEach(joinColumn => {
qb.andWhere(`${relation.entityMetadata.name}.${joinColumn.referencedColumn!.databaseName} = :${joinColumn.referencedColumn!.databaseName}`)
.setParameter(`${joinColumn.referencedColumn!.databaseName}`, entity[joinColumn.referencedColumn!.databaseName]);
});
return qb.getOne();
}
/**
* Loads data for one-to-many and one-to-one not owner relations.
*
* SELECT post
* FROM post post
* WHERE post.[joinColumn.name] = entity[joinColumn.referencedColumn]
*/
protected loadOneToManyOrOneToOneNotOwner(relation: RelationMetadata, entity: ObjectLiteral): Promise<any> {
const qb = new QueryBuilder(this.connection)
.select(relation.propertyName)
.from(relation.inverseRelation.entityMetadata.target, relation.propertyName);
relation.inverseRelation.joinColumns.forEach(joinColumn => {
qb.andWhere(`${relation.propertyName}.${joinColumn.propertyName} = :${joinColumn.referencedColumn!.propertyName}`)
.setParameter(`${joinColumn.referencedColumn!.propertyName}`, entity[joinColumn.referencedColumn!.propertyName]);
});
return relation.isOneToMany ? qb.getMany() : qb.getOne();
}
/**
* Loads data for many-to-many owner relations.
*
* SELECT category
* FROM category category
* INNER JOIN post_categories post_categories
* ON post_categories.postId = :postId
* AND post_categories.categoryId = category.id
*/
protected loadManyToManyOwner(relation: RelationMetadata, entity: ObjectLiteral): Promise<any> {
const mainAlias = relation.propertyName;
const joinAlias = relation.junctionEntityMetadata.tableName;
const joinColumnConditions = relation.joinColumns.map(joinColumn => {
return `${joinAlias}.${joinColumn.propertyName} = :${joinColumn.propertyName}`;
});
const inverseJoinColumnConditions = relation.inverseJoinColumns.map(inverseJoinColumn => {
return `${joinAlias}.${inverseJoinColumn.propertyName}=${mainAlias}.${inverseJoinColumn.referencedColumn!.propertyName}`;
});
const parameters = relation.joinColumns.reduce((parameters, joinColumn) => {
parameters[joinColumn.propertyName] = entity[joinColumn.referencedColumn!.propertyName];
return parameters;
}, {} as ObjectLiteral);
return new QueryBuilder(this.connection)
.select(mainAlias)
.from(relation.type, mainAlias)
.innerJoin(joinAlias, joinAlias, [...joinColumnConditions, ...inverseJoinColumnConditions].join(" AND "))
.setParameters(parameters)
.getMany();
}
/**
* Loads data for many-to-many not owner relations.
*
* SELECT post
* FROM post post
* INNER JOIN post_categories post_categories
* ON post_categories.postId = post.id
* AND post_categories.categoryId = post_categories.categoryId
*/
protected loadManyToManyNotOwner(relation: RelationMetadata, entity: ObjectLiteral): Promise<any> {
const mainAlias = relation.propertyName;
const joinAlias = relation.junctionEntityMetadata.tableName;
const joinColumnConditions = relation.inverseRelation.joinColumns.map(joinColumn => {
return `${joinAlias}.${joinColumn.propertyName} = ${mainAlias}.${joinColumn.referencedColumn!.propertyName}`;
});
const inverseJoinColumnConditions = relation.inverseRelation.inverseJoinColumns.map(inverseJoinColumn => {
return `${joinAlias}.${inverseJoinColumn.propertyName} = :${inverseJoinColumn.propertyName}`;
});
const parameters = relation.inverseRelation.inverseJoinColumns.reduce((parameters, joinColumn) => {
parameters[joinColumn.propertyName] = entity[joinColumn.referencedColumn!.propertyName];
return parameters;
}, {} as ObjectLiteral);
return new QueryBuilder(this.connection)
.select(mainAlias)
.from(relation.type, mainAlias)
.innerJoin(joinAlias, joinAlias, [...joinColumnConditions, ...inverseJoinColumnConditions].join(" AND "))
.setParameters(parameters)
.getMany();
}
}