import { DataSource } from "../data-source/DataSource" import { ObjectLiteral } from "../common/ObjectLiteral" import { QueryRunner } from "../query-runner/QueryRunner" import { RelationMetadata } from "../metadata/RelationMetadata" import { FindOptionsUtils } from "../find-options/FindOptionsUtils" import { SelectQueryBuilder } from "./SelectQueryBuilder" /** * Wraps entities and creates getters/setters for their relations * to be able to lazily load relations when accessing these relations. */ export class RelationLoader { // ------------------------------------------------------------------------- // Constructor // ------------------------------------------------------------------------- constructor(private connection: DataSource) {} // ------------------------------------------------------------------------- // Public Methods // ------------------------------------------------------------------------- /** * Loads relation data for the given entity and its relation. */ load( relation: RelationMetadata, entityOrEntities: ObjectLiteral | ObjectLiteral[], queryRunner?: QueryRunner, queryBuilder?: SelectQueryBuilder, ): Promise { // todo: check all places where it uses non array if (queryRunner && queryRunner.isReleased) queryRunner = undefined // get new one if already closed if (relation.isManyToOne || relation.isOneToOneOwner) { return this.loadManyToOneOrOneToOneOwner( relation, entityOrEntities, queryRunner, queryBuilder, ) } else if (relation.isOneToMany || relation.isOneToOneNotOwner) { return this.loadOneToManyOrOneToOneNotOwner( relation, entityOrEntities, queryRunner, queryBuilder, ) } else if (relation.isManyToManyOwner) { return this.loadManyToManyOwner( relation, entityOrEntities, queryRunner, queryBuilder, ) } else { // many-to-many non owner return this.loadManyToManyNotOwner( relation, entityOrEntities, queryRunner, queryBuilder, ) } } /** * 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 */ loadManyToOneOrOneToOneOwner( relation: RelationMetadata, entityOrEntities: ObjectLiteral | ObjectLiteral[], queryRunner?: QueryRunner, queryBuilder?: SelectQueryBuilder, ): Promise { const entities = Array.isArray(entityOrEntities) ? entityOrEntities : [entityOrEntities] const joinAliasName = relation.entityMetadata.name const qb = queryBuilder ? queryBuilder : this.connection .createQueryBuilder(queryRunner) .select(relation.propertyName) // category .from(relation.type, relation.propertyName) const mainAlias = qb.expressionMap.mainAlias!.name const columns = relation.entityMetadata.primaryColumns const joinColumns = relation.isOwning ? relation.joinColumns : relation.inverseRelation!.joinColumns const conditions = joinColumns .map((joinColumn) => { return `${relation.entityMetadata.name}.${ joinColumn.propertyName } = ${mainAlias}.${joinColumn.referencedColumn!.propertyName}` }) .join(" AND ") qb.innerJoin( relation.entityMetadata.target as Function, joinAliasName, conditions, ) if (columns.length === 1) { qb.where( `${joinAliasName}.${columns[0].propertyPath} IN (:...${ joinAliasName + "_" + columns[0].propertyName })`, ) qb.setParameter( joinAliasName + "_" + columns[0].propertyName, entities.map((entity) => columns[0].getEntityValue(entity, true), ), ) } else { const condition = entities .map((entity, entityIndex) => { return columns .map((column, columnIndex) => { const paramName = joinAliasName + "_entity_" + entityIndex + "_" + columnIndex qb.setParameter( paramName, column.getEntityValue(entity, true), ) return ( joinAliasName + "." + column.propertyPath + " = :" + paramName ) }) .join(" AND ") }) .map((condition) => "(" + condition + ")") .join(" OR ") qb.where(condition) } FindOptionsUtils.joinEagerRelations( qb, qb.alias, qb.expressionMap.mainAlias!.metadata, ) return qb.getMany() // return qb.getOne(); todo: fix all usages } /** * 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] */ loadOneToManyOrOneToOneNotOwner( relation: RelationMetadata, entityOrEntities: ObjectLiteral | ObjectLiteral[], queryRunner?: QueryRunner, queryBuilder?: SelectQueryBuilder, ): Promise { const entities = Array.isArray(entityOrEntities) ? entityOrEntities : [entityOrEntities] const columns = relation.inverseRelation!.joinColumns const qb = queryBuilder ? queryBuilder : this.connection .createQueryBuilder(queryRunner) .select(relation.propertyName) .from( relation.inverseRelation!.entityMetadata.target, relation.propertyName, ) const aliasName = qb.expressionMap.mainAlias!.name if (columns.length === 1) { qb.where( `${aliasName}.${columns[0].propertyPath} IN (:...${ aliasName + "_" + columns[0].propertyName })`, ) qb.setParameter( aliasName + "_" + columns[0].propertyName, entities.map((entity) => columns[0].referencedColumn!.getEntityValue(entity, true), ), ) } else { const condition = entities .map((entity, entityIndex) => { return columns .map((column, columnIndex) => { const paramName = aliasName + "_entity_" + entityIndex + "_" + columnIndex qb.setParameter( paramName, column.referencedColumn!.getEntityValue( entity, true, ), ) return ( aliasName + "." + column.propertyPath + " = :" + paramName ) }) .join(" AND ") }) .map((condition) => "(" + condition + ")") .join(" OR ") qb.where(condition) } FindOptionsUtils.joinEagerRelations( qb, qb.alias, qb.expressionMap.mainAlias!.metadata, ) return qb.getMany() // return relation.isOneToMany ? qb.getMany() : qb.getOne(); todo: fix all usages } /** * 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 */ loadManyToManyOwner( relation: RelationMetadata, entityOrEntities: ObjectLiteral | ObjectLiteral[], queryRunner?: QueryRunner, queryBuilder?: SelectQueryBuilder, ): Promise { const entities = Array.isArray(entityOrEntities) ? entityOrEntities : [entityOrEntities] const parameters = relation.joinColumns.reduce( (parameters, joinColumn) => { parameters[joinColumn.propertyName] = entities.map((entity) => joinColumn.referencedColumn!.getEntityValue(entity, true), ) return parameters }, {} as ObjectLiteral, ) const qb = queryBuilder ? queryBuilder : this.connection .createQueryBuilder(queryRunner) .select(relation.propertyName) .from(relation.type, relation.propertyName) const mainAlias = qb.expressionMap.mainAlias!.name const joinAlias = relation.junctionEntityMetadata!.tableName const joinColumnConditions = relation.joinColumns.map((joinColumn) => { return `${joinAlias}.${joinColumn.propertyName} IN (:...${joinColumn.propertyName})` }) const inverseJoinColumnConditions = relation.inverseJoinColumns.map( (inverseJoinColumn) => { return `${joinAlias}.${ inverseJoinColumn.propertyName }=${mainAlias}.${ inverseJoinColumn.referencedColumn!.propertyName }` }, ) qb.innerJoin( joinAlias, joinAlias, [...joinColumnConditions, ...inverseJoinColumnConditions].join( " AND ", ), ).setParameters(parameters) FindOptionsUtils.joinEagerRelations( qb, qb.alias, qb.expressionMap.mainAlias!.metadata, ) return qb.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 */ loadManyToManyNotOwner( relation: RelationMetadata, entityOrEntities: ObjectLiteral | ObjectLiteral[], queryRunner?: QueryRunner, queryBuilder?: SelectQueryBuilder, ): Promise { const entities = Array.isArray(entityOrEntities) ? entityOrEntities : [entityOrEntities] const qb = queryBuilder ? queryBuilder : this.connection .createQueryBuilder(queryRunner) .select(relation.propertyName) .from(relation.type, relation.propertyName) const mainAlias = qb.expressionMap.mainAlias!.name 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} IN (:...${inverseJoinColumn.propertyName})` }, ) const parameters = relation.inverseRelation!.inverseJoinColumns.reduce( (parameters, joinColumn) => { parameters[joinColumn.propertyName] = entities.map((entity) => joinColumn.referencedColumn!.getEntityValue(entity, true), ) return parameters }, {} as ObjectLiteral, ) qb.innerJoin( joinAlias, joinAlias, [...joinColumnConditions, ...inverseJoinColumnConditions].join( " AND ", ), ).setParameters(parameters) FindOptionsUtils.joinEagerRelations( qb, qb.alias, qb.expressionMap.mainAlias!.metadata, ) return qb.getMany() } /** * Wraps given entity and creates getters/setters for its given relation * to be able to lazily load data when accessing this relation. */ enableLazyLoad( relation: RelationMetadata, entity: ObjectLiteral, queryRunner?: QueryRunner, ) { const relationLoader = 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, we need this flag if loaded data is empty const setData = (entity: ObjectLiteral, value: any) => { entity[dataIndex] = value entity[resolveIndex] = true delete entity[promiseIndex] return value } const setPromise = (entity: ObjectLiteral, value: Promise) => { delete entity[resolveIndex] delete entity[dataIndex] entity[promiseIndex] = value value.then( // ensure different value is not assigned yet (result) => entity[promiseIndex] === value ? setData(entity, result) : result, ) return value } Object.defineProperty(entity, relation.propertyName, { get: function () { if ( this[resolveIndex] === true || this[dataIndex] !== undefined ) // if related data already was loaded then simply return it return Promise.resolve(this[dataIndex]) if (this[promiseIndex]) // if related data is loading then return a promise relationLoader loads it return this[promiseIndex] // nothing is loaded yet, load relation data and save it in the model once they are loaded const loader = relationLoader .load(relation, this, queryRunner) .then((result) => relation.isOneToOne || relation.isManyToOne ? result.length === 0 ? null : result[0] : result, ) return setPromise(this, loader) }, set: function (value: any | Promise) { if (value instanceof Promise) { // if set data is a promise then wait for its resolve and save in the object setPromise(this, value) } else { // if its direct data set (non promise, probably not safe-typed) setData(this, value) } }, configurable: true, enumerable: false, }) } }