repairing single table inheritance;

This commit is contained in:
Zotov Dmitry 2017-05-24 12:52:08 +05:00
parent cdae9fb31c
commit 00d131be58
7 changed files with 101 additions and 30 deletions

View File

@ -14,6 +14,7 @@ import {InheritanceMetadataArgs} from "./InheritanceMetadataArgs";
import {DiscriminatorValueMetadataArgs} from "./DiscriminatorValueMetadataArgs";
import {EntityRepositoryMetadataArgs} from "./EntityRepositoryMetadataArgs";
import {TransactionEntityMetadataArgs} from "./TransactionEntityMetadataArgs";
import {MetadataUtils} from "../metadata-builder/MetadataUtils";
/**
* Storage all metadatas args of all available types: tables, columns, subscribers, relations, etc.
@ -147,4 +148,20 @@ export class MetadataArgsStorage {
});
}
filterSingleTableChildren(target: Function|string): TableMetadataArgs[] {
return this.tables.filter(table => {
return table.target instanceof Function
&& target instanceof Function
&& MetadataUtils.isInherited(table.target, target)
&& table.type === "single-table-child";
});
}
findInheritanceType(target: Function|string): InheritanceMetadataArgs|undefined {
return this.inheritances.find(inheritance => inheritance.target === target)
}
findDiscriminatorValue(target: Function|string): DiscriminatorValueMetadataArgs|undefined {
return this.discriminatorValues.find(discriminatorValue => discriminatorValue.target === target)
}
}

View File

@ -62,7 +62,7 @@ export class EntityMetadataBuilder {
const allTables = entityClasses ? this.metadataArgsStorage.filterTables(entityClasses) : this.metadataArgsStorage.tables;
// filter out table metadata args for those we really create entity metadatas and tables in the db
const realTables = allTables.filter(table => table.type === "regular" || table.type === "closure" || table.type === "class-table-child");
const realTables = allTables.filter(table => table.type === "regular" || table.type === "closure" || table.type === "class-table-child" || table.type === "single-table-child");
// create entity metadatas for a user defined entities (marked with @Entity decorator or loaded from entity schemas)
const entityMetadatas = realTables.map(tableArgs => this.createEntityMetadata(tableArgs));
@ -118,9 +118,27 @@ export class EntityMetadataBuilder {
entityMetadatas.push(closureJunctionEntityMetadata);
});
// after all metadatas created we set parent entity metadata for class-table inheritance
entityMetadatas
.filter(metadata => metadata.tableType === "single-table-child")
.forEach(entityMetadata => {
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";
});
if (parentMetadata) {
entityMetadata.parentEntityMetadata = parentMetadata;
entityMetadata.tableName = parentMetadata.tableName;
}
});
// generate keys for tables with single-table inheritance
entityMetadatas
.filter(metadata => metadata.inheritanceType === "single-table")
.filter(metadata => metadata.inheritanceType === "single-table" && metadata.discriminatorColumn)
.forEach(entityMetadata => this.createKeysForTableInheritance(entityMetadata));
// build all indices (need to do it after relations and their join columns are built)
@ -155,11 +173,26 @@ export class EntityMetadataBuilder {
// we take all "inheritance tree" from a target entity to collect all stored metadata args
// (by decorators or inside entity schemas). For example for target Post < ContentModel < Unit
// it will be an array of [Post, ContentModel, Unit] and we can then get all metadata args of those classes
const inheritanceTree = tableArgs.target instanceof Function
const inheritanceTree: any[] = tableArgs.target instanceof Function
? MetadataUtils.getInheritanceTree(tableArgs.target)
: [tableArgs.target]; // todo: implement later here inheritance for string-targets
// if single table inheritance used, we need to copy all children columns in to parent table
const singleTableChildrenTargets: any[] = this.metadataArgsStorage
.filterSingleTableChildren(tableArgs.target)
.map(args => args.target)
.filter(target => target instanceof Function);
inheritanceTree.push(...singleTableChildrenTargets);
const entityMetadata = new EntityMetadata({ connection: this.connection, args: tableArgs });
const inheritanceType = this.metadataArgsStorage.findInheritanceType(tableArgs.target);
entityMetadata.inheritanceType = inheritanceType ? inheritanceType.type : undefined;
const discriminatorValue = this.metadataArgsStorage.findDiscriminatorValue(tableArgs.target);
entityMetadata.discriminatorValue = discriminatorValue ? discriminatorValue.value : (tableArgs.target as any).name; // todo: pass this to naming strategy to generate a name
entityMetadata.embeddeds = this.createEmbeddedsRecursively(entityMetadata, this.metadataArgsStorage.filterEmbeddeds(inheritanceTree));
entityMetadata.ownColumns = this.metadataArgsStorage.filterColumns(inheritanceTree).map(args => {
return new ColumnMetadata({ entityMetadata, args });

View File

@ -22,6 +22,16 @@ export class MetadataUtils {
return tree;
}
/**
* Checks if this table is inherited from another table.
*/
static isInherited(target1: Function, target2: Function) {
// we cannot use instanceOf in this method, because we need order of inherited tables, to ensure that
// properties get inherited in a right order. To achieve it we can only check a first parent of the class
// return this.target.prototype instanceof anotherTable.target;
return Object.getPrototypeOf(target1.prototype).constructor === target2;
}
/**
* Filters given array of targets by a given classes.
* If classes are not given, then it returns array itself.

View File

@ -36,14 +36,12 @@ import {RelationCountMetadataToAttributeTransformer} from "./relation-count/Rela
// todo: add selectAndMap
// todo: tests for:
// todo: entityOrProperty can be a table name. implement if its a table
// todo: entityOrProperty can be target name. implement proper behaviour if it is.
// todo: think about subselect in joins syntax
// todo: create multiple representations of QueryBuilder: UpdateQueryBuilder, DeleteQueryBuilder
// qb.update() returns UpdateQueryBuilder
// qb.delete() returns DeleteQueryBuilder
// qb.select() returns SelectQueryBuilder
// todo: tests for leftJoinAndMap...
// todo: COMPLETELY COVER QUERY BUILDER WITH TESTS
// todo: SUBSELECT IMPLEMENTATION
@ -51,8 +49,6 @@ import {RelationCountMetadataToAttributeTransformer} from "./relation-count/Rela
// todo: also create qb.createSubQueryBuilder()
// todo: check in persistment if id exist on object and throw exception (can be in partial selection?)
// todo: STREAMING
// todo: switch to embedded task?
// todo: add test for @JoinColumn({ referencedColumnName })
/**
* Allows to build complex sql queries in a fashion way and execute those queries.
@ -1312,10 +1308,11 @@ export class QueryBuilder<Entity> {
const aliasName = this.expressionMap.mainAlias.name;
if (this.expressionMap.mainAlias.hasMetadata) {
tableName = this.expressionMap.mainAlias.metadata.tableName;
const metadata = this.expressionMap.mainAlias.metadata;
tableName = metadata.tableName;
allSelects.push(...this.buildEscapedEntityColumnSelects(aliasName, this.expressionMap.mainAlias.metadata));
excludedSelects.push(...this.findEntityColumnSelects(aliasName, this.expressionMap.mainAlias.metadata));
allSelects.push(...this.buildEscapedEntityColumnSelects(aliasName, metadata));
excludedSelects.push(...this.findEntityColumnSelects(aliasName, metadata));
} else { // if alias does not have metadata - selections will be from custom table
tableName = this.expressionMap.mainAlias.tableName!;
@ -1337,12 +1334,12 @@ export class QueryBuilder<Entity> {
});
if (!this.expressionMap.ignoreParentTablesJoins && this.expressionMap.mainAlias.hasMetadata) {
if (this.expressionMap.mainAlias!.metadata.parentEntityMetadata && this.expressionMap.mainAlias!.metadata.parentIdColumns) {
const alias = "parentIdColumn_" + ea(this.expressionMap.mainAlias!.metadata.parentEntityMetadata.tableName);
this.expressionMap.mainAlias!.metadata.parentEntityMetadata.columns.forEach(column => {
const metadata = this.expressionMap.mainAlias.metadata;
if (metadata.parentEntityMetadata && metadata.parentEntityMetadata.inheritanceType === "class-table" && metadata.parentIdColumns) {
const alias = "parentIdColumn_" + metadata.parentEntityMetadata.tableName;
metadata.parentEntityMetadata.columns.forEach(column => {
// TODO implement partial select
allSelects.push({ selection: ea(alias + "." + column.databaseName), aliasName: alias + "_" + column.databaseName });
// allSelects.push(alias + "." + ec(column.fullName) + " AS " + alias + "_" + ea(column.fullName));
allSelects.push({ selection: ea(alias) + "." + ec(column.databaseName), aliasName: alias + "_" + column.databaseName });
});
}
}
@ -1445,7 +1442,7 @@ export class QueryBuilder<Entity> {
if (this.expressionMap.mainAlias!.hasMetadata) {
const mainMetadata = this.expressionMap.mainAlias!.metadata;
if (mainMetadata.discriminatorColumn)
return ` WHERE ${ conditions.length ? "(" + conditions + ") AND" : "" } ${this.escapeColumn(mainMetadata.discriminatorColumn.databaseName)}=:discriminatorColumnValue`;
return ` WHERE ${ conditions.length ? "(" + conditions + ") AND" : "" } ${this.replacePropertyNames(this.expressionMap.mainAlias!.name + "." + mainMetadata.discriminatorColumn.databaseName)}=:discriminatorColumnValue`;
}
if (!conditions.length)
@ -1563,7 +1560,6 @@ export class QueryBuilder<Entity> {
}).join(" AND ");
}
return " " + joinAttr.direction + " JOIN " + et(junctionTableName) + " " + ea(junctionAlias) + " ON " + this.replacePropertyNames(junctionCondition) +
" " + joinAttr.direction + " JOIN " + et(destinationTableName) + " " + ea(destinationTableAlias) + " ON " + this.replacePropertyNames(destinationCondition + appendedCondition);
@ -1572,13 +1568,13 @@ export class QueryBuilder<Entity> {
if (!this.expressionMap.ignoreParentTablesJoins && this.expressionMap.mainAlias!.hasMetadata) {
const metadata = this.expressionMap.mainAlias!.metadata;
if (metadata.parentEntityMetadata && metadata.parentIdColumns) {
if (metadata.parentEntityMetadata && metadata.parentEntityMetadata.inheritanceType === "class-table" && metadata.parentIdColumns) {
const alias = "parentIdColumn_" + metadata.parentEntityMetadata.tableName;
const parentJoin = " JOIN " + et(metadata.parentEntityMetadata.tableName) + " " + ea(alias) + " ON " +
metadata.parentIdColumns.map(parentIdColumn => {
return this.expressionMap.mainAlias!.name + "." + parentIdColumn.databaseName + "=" + ea(alias) + "." + parentIdColumn.propertyName;
});
joins.push(parentJoin);
const condition = metadata.parentIdColumns.map(parentIdColumn => {
return this.expressionMap.mainAlias!.name + "." + parentIdColumn.databaseName + "=" + ea(alias) + "." + parentIdColumn.propertyName;
}).join(" AND ");
const join = " JOIN " + et(metadata.parentEntityMetadata.tableName) + " " + ea(alias) + " ON " + condition;
joins.push(join);
}
}

View File

@ -94,7 +94,7 @@ export class SchemaBuilder {
// -------------------------------------------------------------------------
protected get entityToSyncMetadatas(): EntityMetadata[] {
return this.entityMetadatas.filter(metadata => !metadata.skipSchemaSync);
return this.entityMetadatas.filter(metadata => !metadata.skipSchemaSync && metadata.tableType !== "single-table-child");
}
/**

View File

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

View File

@ -1,11 +1,11 @@
import "reflect-metadata";
import { createTestingConnections, closeTestingConnections, reloadTestingDatabases } from "../../utils/test-utils";
import { Connection } from "../../../src/connection/Connection";
import { Employee } from "./entity/Employee";
import { expect } from "chai";
import {closeTestingConnections, createTestingConnections, reloadTestingDatabases} from "../../utils/test-utils";
import {Connection} from "../../../src/connection/Connection";
import {expect} from "chai";
import {Student} from "./entity/Student";
import {Employee} from "./entity/Employee";
// unskip once table inheritance is back
describe.skip("github issues > #131 Error with single table inheritance query without additional conditions", () => {
describe("github issues > #131 Error with single table inheritance query without additional conditions", () => {
let connections: Connection[];
before(async () => connections = await createTestingConnections({
@ -13,11 +13,15 @@ describe.skip("github issues > #131 Error with single table inheritance query wi
schemaCreate: true,
dropSchemaOnConnection: true,
}));
beforeEach(() => reloadTestingDatabases(connections));
after(() => closeTestingConnections(connections));
it("should not fail when querying for single table inheritance model without additional conditions", () => Promise.all(connections.map(async connection => {
const employees = await connection.getRepository(Employee).find();
expect(employees).not.to.be.undefined;
const students = await connection.getRepository(Student).find();
expect(students).not.to.be.undefined;
})));
});