repairing single table inheritance;

working on single table inheritance tests;
This commit is contained in:
Zotov Dmitry 2017-05-26 13:12:01 +05:00
parent 1c496edca5
commit eb51cb3ef3
13 changed files with 355 additions and 30 deletions

View File

@ -74,37 +74,39 @@ export class EntityMetadataBuilder {
entityMetadatas.forEach(entityMetadata => this.computeInverseProperties(entityMetadata, entityMetadatas));
// go through all entity metadatas and create foreign keys / junction entity metadatas for their relations
entityMetadatas.forEach(entityMetadata => {
entityMetadatas
.filter(entityMetadata => entityMetadata.tableType !== "single-table-child")
.forEach(entityMetadata => {
// create entity's relations join columns (for many-to-one and one-to-one owner)
entityMetadata.relations.filter(relation => relation.isOneToOne || relation.isManyToOne).forEach(relation => {
const joinColumns = this.metadataArgsStorage.filterJoinColumns(relation.target, relation.propertyName);
const foreignKey = this.relationJoinColumnBuilder.build(joinColumns, relation); // create a foreign key based on its metadata args
if (foreignKey) {
relation.registerForeignKeys(foreignKey); // push it to the relation and thus register there a join column
entityMetadata.foreignKeys.push(foreignKey);
}
});
// create entity's relations join columns (for many-to-one and one-to-one owner)
entityMetadata.relations.filter(relation => relation.isOneToOne || relation.isManyToOne).forEach(relation => {
const joinColumns = this.metadataArgsStorage.filterJoinColumns(relation.target, relation.propertyName);
const foreignKey = this.relationJoinColumnBuilder.build(joinColumns, relation); // create a foreign key based on its metadata args
if (foreignKey) {
relation.registerForeignKeys(foreignKey); // push it to the relation and thus register there a join column
entityMetadata.foreignKeys.push(foreignKey);
}
});
// create junction entity metadatas for entity many-to-many relations
entityMetadata.relations.filter(relation => relation.isManyToMany).forEach(relation => {
const joinTable = this.metadataArgsStorage.findJoinTable(relation.target, relation.propertyName);
if (!joinTable) return; // no join table set - no need to do anything (it means this is many-to-many inverse side)
// create junction entity metadatas for entity many-to-many relations
entityMetadata.relations.filter(relation => relation.isManyToMany).forEach(relation => {
const joinTable = this.metadataArgsStorage.findJoinTable(relation.target, relation.propertyName);
if (!joinTable) return; // no join table set - no need to do anything (it means this is many-to-many inverse side)
// here we create a junction entity metadata for a new junction table of many-to-many relation
const junctionEntityMetadata = this.junctionEntityMetadataBuilder.build(relation, joinTable);
relation.registerForeignKeys(...junctionEntityMetadata.foreignKeys);
relation.registerJunctionEntityMetadata(junctionEntityMetadata);
// here we create a junction entity metadata for a new junction table of many-to-many relation
const junctionEntityMetadata = this.junctionEntityMetadataBuilder.build(relation, joinTable);
relation.registerForeignKeys(...junctionEntityMetadata.foreignKeys);
relation.registerJunctionEntityMetadata(junctionEntityMetadata);
// compute new entity metadata properties and push it to entity metadatas pool
this.computeEntityMetadata(junctionEntityMetadata);
this.computeInverseProperties(junctionEntityMetadata, entityMetadatas);
entityMetadatas.push(junctionEntityMetadata);
});
// compute new entity metadata properties and push it to entity metadatas pool
this.computeEntityMetadata(junctionEntityMetadata);
this.computeInverseProperties(junctionEntityMetadata, entityMetadatas);
entityMetadatas.push(junctionEntityMetadata);
});
// update entity metadata depend properties
entityMetadata.relationsWithJoinColumns = entityMetadata.relations.filter(relation => relation.isWithJoinColumn);
entityMetadata.hasNonNullableRelations = entityMetadata.relationsWithJoinColumns.some(relation => !relation.isNullable || relation.isPrimary);
// update entity metadata depend properties
entityMetadata.relationsWithJoinColumns = entityMetadata.relations.filter(relation => relation.isWithJoinColumn);
entityMetadata.hasNonNullableRelations = entityMetadata.relationsWithJoinColumns.some(relation => !relation.isNullable || relation.isPrimary);
});
// generate closure junction tables for all closure tables
@ -125,7 +127,7 @@ export class EntityMetadataBuilder {
const inheritanceTree: any[] = entityMetadata.target instanceof Function
? MetadataUtils.getInheritanceTree(entityMetadata.target)
: [entityMetadata.target];
const parentMetadata = entityMetadatas.find(metadata => {
return inheritanceTree.find(inheritance => inheritance === metadata.target) && metadata.inheritanceType === "single-table";
});

View File

@ -751,8 +751,8 @@ export class SubjectBuilder<Entity extends ObjectLiteral> {
*/
private async buildJunctionOperations(options: { insert: boolean, remove: boolean }): Promise<void> {
const promises = this.operateSubjects.filter(subject => subject.hasEntity).map(subject => {
const promises = subject.metadata.manyToManyRelations.map(async relation => {
const metadata = subject.metadata.parentEntityMetadata ? subject.metadata.parentEntityMetadata : subject.metadata;
const promises = metadata.manyToManyRelations.map(async relation => {
// if subject marked to be removed then all its junctions must be removed
if (subject.mustBeRemoved && options.remove) {
// load from db all relation ids of inverse entities that are "bind" to the currently persisted entity

View File

@ -119,7 +119,10 @@ export class JoinAttribute {
return undefined;
const relationOwnerSelection = this.queryExpressionMap.findAliasByName(this.parentAlias!);
const relation = relationOwnerSelection.metadata.findRelationWithPropertyPath(this.relationPropertyPath!);
const metadata = relationOwnerSelection.metadata.parentEntityMetadata
? relationOwnerSelection.metadata.parentEntityMetadata
: relationOwnerSelection.metadata;
const relation = metadata.findRelationWithPropertyPath(this.relationPropertyPath!);
if (!relation)
throw new Error(`Relation with property path ${this.relationPropertyPath} in entity was not found.`);
return relation;

View File

@ -111,12 +111,22 @@ export class RawSqlResultsToEntityTransformer {
*/
protected transformJoins(rawResults: any[], entity: ObjectLiteral, alias: Alias) {
let hasData = false;
let discriminatorValue: string = "";
if (alias.metadata.discriminatorColumn)
discriminatorValue = rawResults[0][alias.name + "_" + alias.metadata.discriminatorColumn!.databaseName];
this.joinAttributes.forEach(join => {
// skip joins without metadata
if (!join.metadata)
return;
// this check need to avoid setting properties than not belong to entity when single table inheritance used.
const metadata = alias.metadata.childEntityMetadatas.find(childEntityMetadata => discriminatorValue === childEntityMetadata.discriminatorValue);
if (metadata && join.relation && metadata.target !== join.relation.target)
return;
// some checks to make sure this join is for current alias
if (join.mapToProperty) {
if (join.mapToPropertyParentAlias !== alias.name)

View File

@ -0,0 +1,14 @@
import {SingleEntityChild} from "../../../../../../../src/decorator/entity/SingleEntityChild";
import {ManyToMany} from "../../../../../../../src/decorator/relations/ManyToMany";
import {JoinTable} from "../../../../../../../src/decorator/relations/JoinTable";
import {Employee} from "./Employee";
import {Department} from "./Department";
@SingleEntityChild()
export class Accountant extends Employee {
@ManyToMany(type => Department, department => department.accountants)
@JoinTable()
departments: Department[];
}

View File

@ -0,0 +1,19 @@
import {Column} from "../../../../../../../src/decorator/columns/Column";
import {Entity} from "../../../../../../../src/decorator/entity/Entity";
import {PrimaryGeneratedColumn} from "../../../../../../../src/decorator/columns/PrimaryGeneratedColumn";
import {ManyToMany} from "../../../../../../../src/decorator/relations/ManyToMany";
import {Accountant} from "./Accountant";
@Entity()
export class Department {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@ManyToMany(type => Accountant, accountant => accountant.departments)
accountants: Accountant[];
}

View File

@ -0,0 +1,11 @@
import {Column} from "../../../../../../../src/decorator/columns/Column";
import {SingleEntityChild} from "../../../../../../../src/decorator/entity/SingleEntityChild";
import {Person} from "./Person";
@SingleEntityChild()
export class Employee extends Person {
@Column()
salary: number;
}

View File

@ -0,0 +1,19 @@
import {Column} from "../../../../../../../src/decorator/columns/Column";
import {Entity} from "../../../../../../../src/decorator/entity/Entity";
import {PrimaryGeneratedColumn} from "../../../../../../../src/decorator/columns/PrimaryGeneratedColumn";
import {ManyToMany} from "../../../../../../../src/decorator/relations/ManyToMany";
import {Student} from "./Student";
@Entity()
export class Faculty {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@ManyToMany(type => Student, student => student.faculties)
students: Student[];
}

View File

@ -0,0 +1,18 @@
import {Column} from "../../../../../../../src/decorator/columns/Column";
import {TableInheritance} from "../../../../../../../src/decorator/entity/TableInheritance";
import {DiscriminatorColumn} from "../../../../../../../src/decorator/columns/DiscriminatorColumn";
import {Entity} from "../../../../../../../src/decorator/entity/Entity";
import {PrimaryGeneratedColumn} from "../../../../../../../src/decorator/columns/PrimaryGeneratedColumn";
@Entity()
@TableInheritance("single-table")
@DiscriminatorColumn({ name: "type", type: "string" })
export class Person {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
}

View File

@ -0,0 +1,19 @@
import {Column} from "../../../../../../../src/decorator/columns/Column";
import {Entity} from "../../../../../../../src/decorator/entity/Entity";
import {PrimaryGeneratedColumn} from "../../../../../../../src/decorator/columns/PrimaryGeneratedColumn";
import {ManyToMany} from "../../../../../../../src/decorator/relations/ManyToMany";
import {Teacher} from "./Teacher";
@Entity()
export class Specialization {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@ManyToMany(type => Teacher, teacher => teacher.specializations)
teachers: Teacher[];
}

View File

@ -0,0 +1,14 @@
import {SingleEntityChild} from "../../../../../../../src/decorator/entity/SingleEntityChild";
import {ManyToMany} from "../../../../../../../src/decorator/relations/ManyToMany";
import {Person} from "./Person";
import {Faculty} from "./Faculty";
import {JoinTable} from "../../../../../../../src/decorator/relations/JoinTable";
@SingleEntityChild()
export class Student extends Person {
@ManyToMany(type => Faculty, faculty => faculty.students)
@JoinTable()
faculties: Faculty[];
}

View File

@ -0,0 +1,14 @@
import {SingleEntityChild} from "../../../../../../../src/decorator/entity/SingleEntityChild";
import {ManyToMany} from "../../../../../../../src/decorator/relations/ManyToMany";
import {JoinTable} from "../../../../../../../src/decorator/relations/JoinTable";
import {Employee} from "./Employee";
import {Specialization} from "./Specialization";
@SingleEntityChild()
export class Teacher extends Employee {
@ManyToMany(type => Specialization, specialization => specialization.teachers)
@JoinTable()
specializations: Specialization[];
}

View File

@ -0,0 +1,182 @@
import "reflect-metadata";
import {
closeTestingConnections,
createTestingConnections,
reloadTestingDatabases
} from "../../../../../utils/test-utils";
import {Connection} from "../../../../../../src/connection/Connection";
import {Student} from "./entity/Student";
import {Teacher} from "./entity/Teacher";
import {Accountant} from "./entity/Accountant";
import {Employee} from "./entity/Employee";
import {Person} from "./entity/Person";
import {Faculty} from "./entity/Faculty";
import {Specialization} from "./entity/Specialization";
import {Department} from "./entity/Department";
describe("table-inheritance > single-table > relations > many-to-many", () => {
let connections: Connection[];
before(async () => connections = await createTestingConnections({
entities: [__dirname + "/entity/*{.js,.ts}"],
schemaCreate: true,
dropSchemaOnConnection: true,
}));
beforeEach(() => reloadTestingDatabases(connections));
after(() => closeTestingConnections(connections));
describe("owner side", () => {
it("should work correctly with ManyToMany relations", () => Promise.all(connections.map(async connection => {
// -------------------------------------------------------------------------
// Create
// -------------------------------------------------------------------------
const faculty1 = new Faculty();
faculty1.name = "Economics";
await connection.getRepository(Faculty).persist(faculty1);
const faculty2 = new Faculty();
faculty2.name = "Programming";
await connection.getRepository(Faculty).persist(faculty2);
const student = new Student();
student.name = "Alice";
student.faculties = [faculty1, faculty2];
await connection.getRepository(Student).persist(student);
const specialization1 = new Specialization();
specialization1.name = "Geography";
await connection.getRepository(Specialization).persist(specialization1);
const specialization2 = new Specialization();
specialization2.name = "Economist";
await connection.getRepository(Specialization).persist(specialization2);
const teacher = new Teacher();
teacher.name = "Mr. Garrison";
teacher.specializations = [specialization1, specialization2];
teacher.salary = 2000;
await connection.getRepository(Teacher).persist(teacher);
const department1 = new Department();
department1.name = "Bookkeeping";
await connection.getRepository(Department).persist(department1);
const department2 = new Department();
department2.name = "HR";
await connection.getRepository(Department).persist(department2);
const accountant = new Accountant();
accountant.name = "Mr. Burns";
accountant.departments = [department1, department2];
accountant.salary = 3000;
await connection.getRepository(Accountant).persist(accountant);
// -------------------------------------------------------------------------
// Select
// -------------------------------------------------------------------------
let loadedStudent = await connection.manager
.createQueryBuilder(Student, "student")
.leftJoinAndSelect("student.faculties", "faculty")
.where("student.name = :name", { name: "Alice" })
.getOne();
loadedStudent!.should.have.all.keys("id", "name", "faculties");
loadedStudent!.id.should.equal(1);
loadedStudent!.name.should.equal("Alice");
loadedStudent!.faculties.length.should.equal(2);
loadedStudent!.faculties[0].name.should.be.equal("Economics");
loadedStudent!.faculties[1].name.should.be.equal("Programming");
let loadedTeacher = await connection.manager
.createQueryBuilder(Teacher, "teacher")
.leftJoinAndSelect("teacher.specializations", "specialization")
.where("teacher.name = :name", { name: "Mr. Garrison" })
.getOne();
loadedTeacher!.should.have.all.keys("id", "name", "specializations", "salary");
loadedTeacher!.id.should.equal(2);
loadedTeacher!.name.should.equal("Mr. Garrison");
loadedTeacher!.specializations.length.should.equal(2);
loadedTeacher!.specializations[0].name.should.be.equal("Geography");
loadedTeacher!.specializations[1].name.should.be.equal("Economist");
loadedTeacher!.salary.should.equal(2000);
let loadedAccountant = await connection.manager
.createQueryBuilder(Accountant, "accountant")
.leftJoinAndSelect("accountant.departments", "department")
.where("accountant.name = :name", { name: "Mr. Burns" })
.getOne();
loadedAccountant!.should.have.all.keys("id", "name", "departments", "salary");
loadedAccountant!.id.should.equal(3);
loadedAccountant!.name.should.equal("Mr. Burns");
loadedAccountant!.departments.length.should.equal(2);
loadedAccountant!.departments[0].name.should.be.equal("Bookkeeping");
loadedAccountant!.departments[1].name.should.be.equal("HR");
loadedAccountant!.salary.should.equal(3000);
const loadedEmployees = await connection.manager
.createQueryBuilder(Employee, "employee")
.leftJoinAndSelect("employee.specializations", "specialization")
.leftJoinAndSelect("employee.departments", "department")
.orderBy("employee.id, specialization.id, department.id")
.getMany();
loadedEmployees[0].should.have.all.keys("id", "name", "salary", "specializations");
loadedEmployees[0].should.be.instanceof(Teacher);
loadedEmployees[0].id.should.equal(2);
loadedEmployees[0].name.should.equal("Mr. Garrison");
(loadedEmployees[0] as Teacher).specializations.length.should.equal(2);
(loadedEmployees[0] as Teacher).specializations[0].name.should.be.equal("Geography");
(loadedEmployees[0] as Teacher).specializations[1].name.should.be.equal("Economist");
loadedEmployees[0].salary.should.equal(2000);
loadedEmployees[1].should.have.all.keys("id", "name", "salary", "departments");
loadedEmployees[1].should.be.instanceof(Accountant);
loadedEmployees[1].id.should.equal(3);
loadedEmployees[1].name.should.equal("Mr. Burns");
(loadedEmployees[1] as Accountant).departments.length.should.equal(2);
(loadedEmployees[1] as Accountant).departments[0].name.should.be.equal("Bookkeeping");
(loadedEmployees[1] as Accountant).departments[1].name.should.be.equal("HR");
loadedEmployees[1].salary.should.equal(3000);
const loadedPersons = await connection.manager
.createQueryBuilder(Person, "person")
.leftJoinAndSelect("person.faculties", "faculty")
.leftJoinAndSelect("person.specializations", "specialization")
.leftJoinAndSelect("person.departments", "department")
.orderBy("person.id, specialization.id, department.id")
.getMany();
loadedPersons[0].should.have.all.keys("id", "name", "faculties");
loadedPersons[0].should.be.instanceof(Student);
loadedPersons[0].id.should.equal(1);
loadedPersons[0].name.should.equal("Alice");
(loadedPersons[0] as Student).faculties.length.should.equal(2);
(loadedPersons[0] as Student).faculties[0].name.should.be.equal("Economics");
(loadedPersons[0] as Student).faculties[1].name.should.be.equal("Programming");
loadedPersons[1].should.have.all.keys("id", "name", "salary", "specializations");
loadedPersons[1].should.be.instanceof(Teacher);
loadedPersons[1].id.should.equal(2);
loadedPersons[1].name.should.equal("Mr. Garrison");
(loadedPersons[1] as Teacher).specializations.length.should.equal(2);
(loadedPersons[1] as Teacher).specializations[0].name.should.be.equal("Geography");
(loadedPersons[1] as Teacher).specializations[1].name.should.be.equal("Economist");
(loadedPersons[1] as Teacher).salary.should.equal(2000);
loadedPersons[2].should.have.all.keys("id", "name", "salary", "departments");
loadedPersons[2].should.be.instanceof(Accountant);
loadedPersons[2].id.should.equal(3);
loadedPersons[2].name.should.equal("Mr. Burns");
(loadedPersons[2] as Accountant).departments.length.should.equal(2);
(loadedPersons[2] as Accountant).departments[0].name.should.be.equal("Bookkeeping");
(loadedPersons[2] as Accountant).departments[1].name.should.be.equal("HR");
(loadedPersons[2] as Accountant).salary.should.equal(3000);
})));
});
});