diff --git a/src/lazy-loading/LazyRelationsWrapper.ts b/src/lazy-loading/LazyRelationsWrapper.ts index 5323b9106..9ebcfb8a8 100644 --- a/src/lazy-loading/LazyRelationsWrapper.ts +++ b/src/lazy-loading/LazyRelationsWrapper.ts @@ -1,6 +1,7 @@ import {RelationMetadata} from "../metadata/RelationMetadata"; import {QueryBuilder} from "../query-builder/QueryBuilder"; import {Connection} from "../connection/Connection"; +import {ObjectLiteral} from "../common/ObjectLiteral"; /** * This class wraps entities and provides functions there to lazily load its relations. @@ -31,88 +32,26 @@ export class LazyRelationsWrapper { if (this[promiseIndex]) return this[promiseIndex]; const qb = new QueryBuilder(connection); - if (relation.isManyToMany) { - /*if (relation.isManyToManyOwner) { + if (relation.isManyToOne || relation.isOneToOneOwner) { - const mainAlias = relation.propertyName; - const joinAlias = relation.junctionEntityMetadata.table.name; - const joinColumnConditions = relation.joinTable.joinColumns.map(joinColumn => { - return `${joinAlias}.${joinColumn.name} = :${joinColumn.name}`; - }); - const inverseJoinColumnConditions = relation.joinTable.inverseJoinColumns.map(joinColumn => { - return `${joinAlias}.${joinColumn.name} = :${joinColumn.name}`; - }); - const parameters = relation.joinTable.joinColumns.reduce((parameters, joinColumn) => { - parameters[joinColumn.name] = this[joinColumn.referencedColumn.propertyName]; - return parameters; - }, {} as ObjectLiteral); + 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 conditions = joinColumnConditions.concat(inverseJoinColumnConditions).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); - qb.select(mainAlias) - .from(relation.type, mainAlias) - .innerJoin(relation.junctionEntityMetadata.table.name, joinAlias, - `${relation.junctionEntityMetadata.table.name}.${relation.joinTable.joinColumnName}=:${mainAlias}Id AND ` + - `${relation.junctionEntityMetadata.table.name}.${relation.joinTable.inverseJoinColumnName}=${mainAlias}.${relation.joinTable.referencedColumn.propertyName}`) - .setParameter(mainAlias + "Id", this[relation.joinTable.referencedColumn.propertyName]); - - } else { // non-owner - qb.select(relation.propertyName) - .from(relation.type, relation.propertyName) - .innerJoin(relation.junctionEntityMetadata.table.name, relation.junctionEntityMetadata.table.name, - `${relation.junctionEntityMetadata.table.name}.${relation.inverseRelation.joinTable.inverseJoinColumnName}=:${relation.propertyName}Id AND ` + - `${relation.junctionEntityMetadata.table.name}.${relation.inverseRelation.joinTable.joinColumnName}=${relation.propertyName}.${relation.inverseRelation.joinTable.referencedColumn.propertyName}`) - .setParameter(relation.propertyName + "Id", this[relation.inverseRelation.joinTable.referencedColumn.propertyName]); - }*/ - - this[promiseIndex] = qb.getMany().then(results => { - this[index] = results; - this[resolveIndex] = true; - delete this[promiseIndex]; - return this[index]; - }).catch(err => { - throw err; + relation.joinColumns.forEach(joinColumn => { + qb.andWhere(`${relation.entityMetadata.name}.${joinColumn.referencedColumn.fullName} = :${joinColumn.referencedColumn.fullName}`) + .setParameter(`${joinColumn.referencedColumn.fullName}`, this[joinColumn.referencedColumn.fullName]) }); - return this[promiseIndex]; - - } else if (relation.isOneToMany) { - - qb.select(relation.propertyName) - .from(relation.inverseRelation.entityMetadata.target, relation.propertyName) - .innerJoin(`${relation.propertyName}.${relation.inverseRelation.propertyName}`, relation.entityMetadata.targetName) - .where(relation.entityMetadata.targetName + "." + relation.inverseEntityMetadata.firstPrimaryColumn.propertyName + "=:id", { id: relation.entityMetadata.getEntityIdMixedMap(this) }); - - 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]; - - } else { // todo: fix issues with joinColumn[0] - - if (relation.hasInverseSide) { - qb.select(relation.propertyName) - .from(relation.inverseRelation.entityMetadata.target, relation.propertyName) - .innerJoin(`${relation.propertyName}.${relation.inverseRelation.propertyName}`, relation.entityMetadata.targetName) - .where(relation.entityMetadata.targetName + "." + relation.joinColumns[0].referencedColumn.fullName + "=:id", { id: relation.entityMetadata.getEntityIdMixedMap(this) }); // is referenced column usage is correct here? - - } else { - // (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, - `${relation.entityMetadata.name}.${relation.propertyName}=${relation.propertyName}.${relation.isOwning ? relation.joinColumns[0].referencedColumn.propertyName : relation.inverseRelation.joinColumns[0].referencedColumn.propertyName }`) - .where(relation.entityMetadata.name + "." + relation.joinColumns[0].referencedColumn.fullName + "=:id", { id: relation.entityMetadata.getEntityIdMixedMap(this) }); // is referenced column usage is correct here? - } this[promiseIndex] = qb.getOne().then(result => { this[index] = result; @@ -124,6 +63,98 @@ export class LazyRelationsWrapper { throw err; }); return this[promiseIndex]; + + } else if (relation.isOneToMany || relation.isOneToOneNotOwner) { + + /* + 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.name} = :${joinColumn.referencedColumn.fullName}`) + .setParameter(`${joinColumn.referencedColumn.fullName}`, this[joinColumn.referencedColumn.fullName]) + }); + + 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.table.name; + 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.joinTable.joinColumns.map(joinColumn => { + return `${joinAlias}.${joinColumn.name} = :${joinColumn.name}`; + }); + inverseJoinColumnConditions = relation.joinTable.inverseJoinColumns.map(inverseJoinColumn => { + return `${joinAlias}.${inverseJoinColumn.name}=${mainAlias}.${inverseJoinColumn.referencedColumn.fullName}`; + }); + parameters = relation.joinTable.joinColumns.reduce((parameters, joinColumn) => { + parameters[joinColumn.name] = 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.joinTable.joinColumns.map(joinColumn => { + return `${joinAlias}.${joinColumn.name} = ${mainAlias}.${joinColumn.referencedColumn.fullName}`; + }); + inverseJoinColumnConditions = relation.inverseRelation.joinTable.inverseJoinColumns.map(inverseJoinColumn => { + return `${joinAlias}.${inverseJoinColumn.name} = :${inverseJoinColumn.name}`; + }); + parameters = relation.inverseRelation.joinTable.inverseJoinColumns.reduce((parameters, joinColumn) => { + parameters[joinColumn.name] = 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]; } }, set: function(promise: Promise) { diff --git a/src/persistence/SubjectBuilder.ts b/src/persistence/SubjectBuilder.ts index 271feb482..42cc1ee75 100644 --- a/src/persistence/SubjectBuilder.ts +++ b/src/persistence/SubjectBuilder.ts @@ -578,7 +578,7 @@ export class SubjectBuilder { const conditions = joinColumnConditions.concat(inverseJoinColumnConditions).join(" AND "); // (example) returns us referenced column (detail's id) - const parameters = relation.joinTable.joinColumns.reduce((parameters, joinColumn) => { + const parameters = relation.inverseRelation.joinTable.inverseJoinColumns.reduce((parameters, joinColumn) => { parameters[joinColumn.name] = subject.databaseEntity[joinColumn.referencedColumn.propertyName]; return parameters; }, {} as ObjectLiteral); @@ -592,7 +592,7 @@ export class SubjectBuilder { .getMany(); } else { // this case can only be a oneToMany relation - + // todo: fix issues with joinColumn[0] // (example) returns us referenced column (detail's id) const relationIdInDatabaseEntity = subject.databaseEntity[relation.inverseRelation.joinColumns[0].referencedColumn.propertyName]; diff --git a/test/functional/lazy-relations/entity/Category.ts b/test/functional/lazy-relations/entity/Category.ts index 818486488..63aa92fb5 100644 --- a/test/functional/lazy-relations/entity/Category.ts +++ b/test/functional/lazy-relations/entity/Category.ts @@ -1,9 +1,10 @@ import {Entity} from "../../../../src/decorator/entity/Entity"; import {PrimaryGeneratedColumn} from "../../../../src/decorator/columns/PrimaryGeneratedColumn"; import {Column} from "../../../../src/decorator/columns/Column"; -import {Post} from "./Post"; import {ManyToMany} from "../../../../src/decorator/relations/ManyToMany"; import {OneToMany} from "../../../../src/decorator/relations/OneToMany"; +import {OneToOne} from "../../../../src/decorator/relations/OneToOne"; +import {Post} from "./Post"; @Entity() export class Category { @@ -14,6 +15,9 @@ export class Category { @Column() name: string; + @OneToOne(type => Post, post => post.oneCategory) + onePost: Promise; + @ManyToMany(type => Post, post => post.twoSideCategories) twoSidePosts: Promise; diff --git a/test/functional/lazy-relations/entity/Post.ts b/test/functional/lazy-relations/entity/Post.ts index 84a6a9555..e6044f9a2 100644 --- a/test/functional/lazy-relations/entity/Post.ts +++ b/test/functional/lazy-relations/entity/Post.ts @@ -5,6 +5,8 @@ import {Category} from "./Category"; 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"; @Entity() export class Post { @@ -32,6 +34,10 @@ export class Post { @ManyToOne(type => Category) category: Promise; + @OneToOne(type => Category, category => category.onePost) + @JoinColumn() + oneCategory: Promise; + @ManyToOne(type => Category, category => category.twoSidePosts2) twoSideCategory: Promise; diff --git a/test/functional/lazy-relations/lazy-relations.ts b/test/functional/lazy-relations/lazy-relations.ts index 02ac23bfb..331ef3043 100644 --- a/test/functional/lazy-relations/lazy-relations.ts +++ b/test/functional/lazy-relations/lazy-relations.ts @@ -8,7 +8,7 @@ import {Category} from "./entity/Category"; * Because lazy relations are overriding prototype is impossible to run these tests on multiple connections. * So we run tests only for mysql. */ -describe.skip("lazy-relations", () => { +describe("lazy-relations", () => { let userSchema: any, profileSchema: any; try { @@ -222,4 +222,111 @@ describe.skip("lazy-relations", () => { loadedCategory.name.should.be.equal("category of great post"); }))); + it("should persist and hydrate successfully on a one-to-many relation", () => Promise.all(connections.map(async connection => { + + // create some fake posts and categories to make sure that there are several post ids in the db + const fakePosts: Post[] = []; + for (let i = 0; i < 8; i++) { + const fakePost = new Post(); + fakePost.title = "post #" + i; + fakePost.text = "post #" + i; + fakePosts.push(fakePost); + } + await connection.entityManager.persist(fakePosts); + + const fakeCategories: Category[] = []; + for (let i = 0; i < 30; i++) { + const fakeCategory = new Category(); + fakeCategory.name = "category #" + i; + fakeCategories.push(fakeCategory); + } + await connection.entityManager.persist(fakeCategories); + + const category = new Category(); + category.name = "category of great post"; + await connection.entityManager.persist(category); + + const post = new Post(); + post.title = "post with great category"; + post.text = "post with great category and great text"; + post.twoSideCategory = Promise.resolve(category); + await connection.entityManager.persist(post); + + const loadedCategory = await connection.entityManager.findOne(Category, { where: { name: "category of great post" } }); + const loadedPost = await loadedCategory!.twoSidePosts2; + + loadedPost[0].title.should.be.equal("post with great category"); + }))); + + it("should persist and hydrate successfully on a one-to-one relation owner side", () => Promise.all(connections.map(async connection => { + + // create some fake posts and categories to make sure that there are several post ids in the db + const fakePosts: Post[] = []; + for (let i = 0; i < 8; i++) { + const fakePost = new Post(); + fakePost.title = "post #" + i; + fakePost.text = "post #" + i; + fakePosts.push(fakePost); + } + await connection.entityManager.persist(fakePosts); + + const fakeCategories: Category[] = []; + for (let i = 0; i < 30; i++) { + const fakeCategory = new Category(); + fakeCategory.name = "category #" + i; + fakeCategories.push(fakeCategory); + } + await connection.entityManager.persist(fakeCategories); + + const category = new Category(); + category.name = "category of great post"; + await connection.entityManager.persist(category); + + const post = new Post(); + post.title = "post with great category"; + post.text = "post with great category and great text"; + post.oneCategory = Promise.resolve(category); + await connection.entityManager.persist(post); + + const loadedPost = await connection.entityManager.findOne(Post, { where: { title: "post with great category" } }); + const loadedCategory = await loadedPost!.oneCategory; + + loadedCategory.name.should.be.equal("category of great post"); + }))); + + it("should persist and hydrate successfully on a one-to-one relation inverse side", () => Promise.all(connections.map(async connection => { + + // create some fake posts and categories to make sure that there are several post ids in the db + const fakePosts: Post[] = []; + for (let i = 0; i < 8; i++) { + const fakePost = new Post(); + fakePost.title = "post #" + i; + fakePost.text = "post #" + i; + fakePosts.push(fakePost); + } + await connection.entityManager.persist(fakePosts); + + const fakeCategories: Category[] = []; + for (let i = 0; i < 30; i++) { + const fakeCategory = new Category(); + fakeCategory.name = "category #" + i; + fakeCategories.push(fakeCategory); + } + await connection.entityManager.persist(fakeCategories); + + const category = new Category(); + category.name = "category of great post"; + await connection.entityManager.persist(category); + + const post = new Post(); + post.title = "post with great category"; + post.text = "post with great category and great text"; + post.oneCategory = Promise.resolve(category); + await connection.entityManager.persist(post); + + const loadedCategory = await connection.entityManager.findOne(Category, { where: { name: "category of great post" } }); + const loadedPost = await loadedCategory!.onePost; + loadedPost.title.should.be.equal("post with great category"); + }))); + }); diff --git a/test/github-issues/234/issue-234.ts b/test/github-issues/234/issue-234.ts index 2229bfec9..b01b5104a 100644 --- a/test/github-issues/234/issue-234.ts +++ b/test/github-issues/234/issue-234.ts @@ -6,7 +6,7 @@ import {expect} from "chai"; import {Category} from "./entity/Category"; import {Tag} from "./entity/Tag"; -describe.skip("github issues > #234 and #223 lazy loading does not work correctly from one-to-many and many-to-many sides", () => { +describe("github issues > #234 and #223 lazy loading does not work correctly from one-to-many and many-to-many sides", () => { let connections: Connection[]; before(async () => connections = await createTestingConnections({