implemented multiple primary columns in LazyRelationsWrapper

This commit is contained in:
Zotov Dmitry 2017-04-24 18:40:39 +05:00
parent 807c819322
commit c9280ed21c
6 changed files with 230 additions and 82 deletions

View File

@ -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<any>) {

View File

@ -578,7 +578,7 @@ export class SubjectBuilder<Entity extends ObjectLiteral> {
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<Entity extends ObjectLiteral> {
.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];

View File

@ -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<Post>;
@ManyToMany(type => Post, post => post.twoSideCategories)
twoSidePosts: Promise<Post[]>;

View File

@ -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<Category>;
@OneToOne(type => Category, category => category.onePost)
@JoinColumn()
oneCategory: Promise<Category>;
@ManyToOne(type => Category, category => category.twoSidePosts2)
twoSideCategory: Promise<Category>;

View File

@ -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");
})));
});

View File

@ -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({