Skip to content

Commit

Permalink
fix: STI types on children in joins (#3160)
Browse files Browse the repository at this point in the history
* Use most specific matching relation type

This allows using a single table for multiple entities without using a
type column. Some setups infer the type from the context of how/where
the row is loaded.

* Use entity's relation metadata for joins

If an STI child overrides a parent property and uses a different type
doing a query on the parent type will skip the relation. This happens
because the query's join is built using the parent relation but when
mapping to an entity the child's relation is fetched so they don't
match. Instead we now use the relation's propertyPath to allow using
relations that reference the same property. We also copy the child's
relation type into the join to ensure we get the right type in the child
as well.
  • Loading branch information
amaranth committed May 29, 2021
1 parent ee3c00a commit 60a6c5d
Show file tree
Hide file tree
Showing 8 changed files with 200 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,15 @@ export class RawSqlResultsToEntityTransformer {

// this check need to avoid setting properties than not belong to entity when single table inheritance used. (todo: check if we still need it)
// const metadata = metadata.childEntityMetadatas.find(childEntityMetadata => discriminatorValue === childEntityMetadata.discriminatorValue);
if (join.relation && !metadata.relations.find(relation => relation === join.relation))
return;
if (join.relation) {
const relation = metadata.relations.find(relation => relation.propertyPath === join.relation!.propertyPath);
if (!relation)
return;

// Use current entity's type metadata, join might be from an STI parent with a different type
if (relation.inverseEntityMetadata)
join.alias.metadata = relation.inverseEntityMetadata;
}

// some checks to make sure this join is for current alias
if (join.mapToProperty) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import "reflect-metadata";
import {
closeTestingConnections,
createTestingConnections,
reloadTestingDatabases
} from "../../../../../utils/test-utils";
import {Connection} from "../../../../../../src";
import {Teacher} from "./entity/Teacher";
import {Accountant} from "./entity/Accountant";
import {Person} from "./entity/Person";
import {Specialization} from "./entity/Specialization";
import {Department} from "./entity/Department";

describe("table-inheritance > single-table > relations > child-relation-type", () => {

let connections: Connection[];
before(async () => connections = await createTestingConnections({
entities: [__dirname + "/entity/*{.js,.ts}"]
}));
beforeEach(() => reloadTestingDatabases(connections));
after(() => closeTestingConnections(connections));

it("should work correctly with OneToMany relations", () => Promise.all(connections.map(async connection => {

// -------------------------------------------------------------------------
// Create
// -------------------------------------------------------------------------

const specialization1 = new Specialization();
specialization1.name = "Geography";
await connection.getRepository(Specialization).save(specialization1);

const specialization2 = new Specialization();
specialization2.name = "Economist";
await connection.getRepository(Specialization).save(specialization2);

const teacher = new Teacher();
teacher.name = "Mr. Garrison";
teacher.data = [specialization1, specialization2];
await connection.getRepository(Teacher).save(teacher);

const department1 = new Department();
department1.name = "Bookkeeping";
await connection.getRepository(Department).save(department1);

const department2 = new Department();
department2.name = "HR";
await connection.getRepository(Department).save(department2);

const accountant = new Accountant();
accountant.name = "Mr. Burns";
accountant.data = [department1, department2];
await connection.getRepository(Accountant).save(accountant);

// -------------------------------------------------------------------------
// Select
// -------------------------------------------------------------------------

let loadedTeacher = await connection.manager
.createQueryBuilder(Teacher, "teacher")
.leftJoinAndSelect("teacher.data", "data")
.where("teacher.name = :name", { name: "Mr. Garrison" })
.orderBy("teacher.id, data.id")
.getOne();

loadedTeacher!.should.have.all.keys("id", "name", "data");
loadedTeacher!.id.should.equal(1);
loadedTeacher!.name.should.equal("Mr. Garrison");
loadedTeacher!.data.length.should.equal(2);
loadedTeacher!.data[0].should.be.instanceOf(Specialization);
loadedTeacher!.data[0].name.should.be.equal("Geography");
loadedTeacher!.data[1].name.should.be.equal("Economist");

let loadedAccountant = await connection.manager
.createQueryBuilder(Accountant, "accountant")
.leftJoinAndSelect("accountant.data", "data")
.where("accountant.name = :name", { name: "Mr. Burns" })
.orderBy("accountant.id, data.id")
.getOne();

loadedAccountant!.should.have.all.keys("id", "name", "data");
loadedAccountant!.id.should.equal(2);
loadedAccountant!.name.should.equal("Mr. Burns");
loadedAccountant!.data.length.should.equal(2);
loadedAccountant!.data[0].should.be.instanceOf(Department);
loadedAccountant!.data[0].name.should.be.equal("Bookkeeping");
loadedAccountant!.data[1].name.should.be.equal("HR");

const loadedPersons = await connection.manager
.createQueryBuilder(Person, "person")
.leftJoinAndSelect("person.data", "data")
.orderBy("person.id, data.id")
.getMany();

loadedPersons[0].should.have.all.keys("id", "name", "data");
loadedPersons[0].should.be.instanceof(Teacher);
loadedPersons[0].id.should.equal(1);
loadedPersons[0].name.should.equal("Mr. Garrison");
loadedPersons[0].data[0].should.be.instanceOf(Specialization);
(loadedPersons[0] as Teacher).data.length.should.equal(2);
(loadedPersons[0] as Teacher).data[0].name.should.be.equal("Geography");
(loadedPersons[0] as Teacher).data[1].name.should.be.equal("Economist");
loadedPersons[1].should.have.all.keys("id", "name", "data");
loadedPersons[1].should.be.instanceof(Accountant);
loadedPersons[1].id.should.equal(2);
loadedPersons[1].name.should.equal("Mr. Burns");
loadedPersons[1].data[0].should.be.instanceOf(Department);
(loadedPersons[1] as Accountant).data.length.should.equal(2);
(loadedPersons[1] as Accountant).data[0].name.should.be.equal("Bookkeeping");
(loadedPersons[1] as Accountant).data[1].name.should.be.equal("HR");

})));

});
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {ChildEntity, OneToMany} from "../../../../../../../src";
import {Department} from "./Department";
import {Person} from "./Person";

@ChildEntity()
export class Accountant extends Person {

@OneToMany(() => Department, department => department.person)
data: Department[];

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {Column, Entity, ManyToOne, PrimaryGeneratedColumn} from "../../../../../../../src";
import {Person} from "./Person";

@Entity("data")
export abstract class Data {

@PrimaryGeneratedColumn()
id: number;

@Column()
name: string;

@ManyToOne(() => Person, person => person.data)
person: Person;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {Entity, ManyToOne} from "../../../../../../../src";
import {Accountant} from "./Accountant";
import {Data} from "./Data";

@Entity("data")
export class Department extends Data {

@ManyToOne(() => Accountant, accountant => accountant.data)
person: Accountant;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {Column, Entity, OneToMany, PrimaryGeneratedColumn, TableInheritance} from "../../../../../../../src";
import {Data} from "./Data";

@Entity()
@TableInheritance({column: {name: "type", type: "varchar"}})
export class Person {

@PrimaryGeneratedColumn()
id: number;

@Column()
name: string;

@OneToMany(() => Data, data => data.person)
data: Data[];

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {Entity, ManyToOne} from "../../../../../../../src";
import {Teacher} from "./Teacher";
import {Data} from "./Data";

@Entity("data")
export class Specialization extends Data {

@ManyToOne(() => Teacher, teacher => teacher.data)
person: Teacher;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {ChildEntity, OneToMany} from "../../../../../../../src";
import {Specialization} from "./Specialization";
import {Person} from "./Person";

@ChildEntity()
export class Teacher extends Person {

@OneToMany(() => Specialization, specialization => specialization.person)
data: Specialization[];

}

0 comments on commit 60a6c5d

Please sign in to comment.