Skip to content

Commit

Permalink
feat: closure table custom naming (#7120)
Browse files Browse the repository at this point in the history
* add metadata to @Tree

* add simple test

* ref column naming from string to callback(column)=>name

* fix Closure junction metadata builder

* fix ClosureSubjectExecutor columns naming

* ref ClosureSubjectExecutor columns naming

* fix ClosureEntityMetadataBuilder

* add tests

* fix test name

* doc: add closure-table custom naming docs.

* refactor: small code-style changing.

* refactor: minor refactoring during code review

Co-authored-by: alexey2baranov <Ir1n@gh>
  • Loading branch information
alexey2baranov and alexey2baranov committed Jan 12, 2021
1 parent efc2837 commit bcd998b
Show file tree
Hide file tree
Showing 9 changed files with 270 additions and 15 deletions.
18 changes: 14 additions & 4 deletions docs/tree-entities.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ To learn more about hierarchy table take a look at [this awesome presentation by

## Adjacency list

Adjacency list is a simple model with self-referencing.
The benefit of this approach is simplicity,
Adjacency list is a simple model with self-referencing.
The benefit of this approach is simplicity,
drawback is that you can't load big trees in all at once because of join limitations.
To learn more about the benefits and use of Adjacency Lists look at [this article by Matthew Schinckel](http://schinckel.net/2014/09/13/long-live-adjacency-lists/).
Example:
Expand All @@ -38,7 +38,7 @@ export class Category {
@OneToMany(type => Category, category => category.parent)
children: Category[];
}

```

## Nested set
Expand Down Expand Up @@ -98,7 +98,7 @@ export class Category {

## Closure table

Closure table stores relations between parent and child in a separate table in a special way.
Closure table stores relations between parent and child in a separate table in a special way.
It's efficient in both reads and writes.
Example:

Expand All @@ -123,6 +123,16 @@ export class Category {
}
```

You can specify closure table name and / or closure table columns names by setting optional parameter `options` into `@Tree("closure-table", options)`. `ancestorColumnName` and `descandantColumnName` are callback functions, which receive primary column's metadata and return column's name.

```ts
@Tree("closure-table", {
closureTableName: "category_closure",
ancestorColumnName: (column) => "ancestor_" + column.propertyName,
descendantColumnName: (column) => "descendant_" + column.propertyName,
})
```

### Note:
Updating or removing a component's parent has not been implemented yet ([see this issue](https://github.com/typeorm/typeorm/issues/2032)). The closure table will need to be explicitly updated to do either of these operations.

Expand Down
6 changes: 4 additions & 2 deletions src/decorator/tree/Tree.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import {getMetadataArgsStorage} from "../../";
import {TreeMetadataArgs} from "../../metadata-args/TreeMetadataArgs";
import {TreeType} from "../../metadata/types/TreeTypes";
import {ClosureTreeOptions} from "../../metadata/types/ClosureTreeOptions";

/**
* Marks entity to work like a tree.
* Tree pattern that will be used for the tree entity should be specified.
* @TreeParent decorator must be used in tree entities.
* TreeRepository can be used to manipulate with tree entities.
*/
export function Tree(type: TreeType): ClassDecorator {
export function Tree(type: TreeType, options?: ClosureTreeOptions): ClassDecorator {
return function (target: Function) {

getMetadataArgsStorage().trees.push({
target: target,
type: type
type: type,
options: type === "closure-table" ? options : undefined
} as TreeMetadataArgs);
};
}
5 changes: 5 additions & 0 deletions src/metadata-args/TreeMetadataArgs.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {TreeType} from "../metadata/types/TreeTypes";
import {ClosureTreeOptions} from "../metadata/types/ClosureTreeOptions";

/**
* Stores metadata collected for Tree entities.
Expand All @@ -15,4 +16,8 @@ export interface TreeMetadataArgs {
*/
type: TreeType;

/**
* Tree options
*/
options?: ClosureTreeOptions;
}
6 changes: 3 additions & 3 deletions src/metadata-builder/ClosureJunctionEntityMetadataBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export class ClosureJunctionEntityMetadataBuilder {
connection: this.connection,
args: {
target: "",
name: parentClosureEntityMetadata.tableNameWithoutPrefix,
name: parentClosureEntityMetadata.treeOptions && parentClosureEntityMetadata.treeOptions.closureTableName ? parentClosureEntityMetadata.treeOptions.closureTableName : parentClosureEntityMetadata.tableNameWithoutPrefix,
type: "closure-junction"
}
});
Expand All @@ -48,7 +48,7 @@ export class ClosureJunctionEntityMetadataBuilder {
args: {
target: "",
mode: "virtual",
propertyName: primaryColumn.propertyName + "_ancestor", // todo: naming strategy
propertyName: parentClosureEntityMetadata.treeOptions && parentClosureEntityMetadata.treeOptions.ancestorColumnName ? parentClosureEntityMetadata.treeOptions.ancestorColumnName(primaryColumn) : primaryColumn.propertyName + "_ancestor",
options: {
primary: true,
length: primaryColumn.length,
Expand All @@ -64,7 +64,7 @@ export class ClosureJunctionEntityMetadataBuilder {
args: {
target: "",
mode: "virtual",
propertyName: primaryColumn.propertyName + "_descendant",
propertyName: parentClosureEntityMetadata.treeOptions && parentClosureEntityMetadata.treeOptions.descendantColumnName ? parentClosureEntityMetadata.treeOptions.descendantColumnName(primaryColumn) : primaryColumn.propertyName + "_descendant",
options: {
primary: true,
length: primaryColumn.length,
Expand Down
7 changes: 7 additions & 0 deletions src/metadata/EntityMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {RelationMetadata} from "./RelationMetadata";
import {TableType} from "./types/TableTypes";
import {TreeType} from "./types/TreeTypes";
import {UniqueMetadata} from "./UniqueMetadata";
import {ClosureTreeOptions} from "./types/ClosureTreeOptions";

/**
* Contains all entity metadata.
Expand Down Expand Up @@ -191,6 +192,11 @@ export class EntityMetadata {
*/
treeType?: TreeType;

/**
* Indicates if this entity is a tree, what options of tree it has.
*/
treeOptions?: ClosureTreeOptions;

/**
* Checks if this table is a junction table of the closure table.
* This type is for tables that contain junction metadata of the closure tables.
Expand Down Expand Up @@ -503,6 +509,7 @@ export class EntityMetadata {
this.inheritanceTree = options.inheritanceTree || [];
this.inheritancePattern = options.inheritancePattern;
this.treeType = options.tableTree ? options.tableTree.type : undefined;
this.treeOptions = options.tableTree ? options.tableTree.options : undefined;
this.parentClosureEntityMetadata = options.parentClosureEntityMetadata!;
this.tableMetadataArgs = options.args;
this.target = this.tableMetadataArgs.target;
Expand Down
11 changes: 11 additions & 0 deletions src/metadata/types/ClosureTreeOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Tree type.
* Specifies what table pattern will be used for the tree entity.
*/
import {ColumnMetadata} from "../ColumnMetadata";

export interface ClosureTreeOptions {
closureTableName?: string,
ancestorColumnName?: (column: ColumnMetadata) => string,
descendantColumnName?: (column: ColumnMetadata) => string,
}
12 changes: 6 additions & 6 deletions src/persistence/tree/ClosureSubjectExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export class ClosureSubjectExecutor {
/**
* Removes all children of the given subject's entity.
async deleteChildrenOf(subject: Subject) {
async deleteChildrenOf(subject: Subject) {
// const relationValue = subject.metadata.treeParentRelation.getEntityValue(subject.databaseEntity);
// console.log("relationValue: ", relationValue);
// this.queryRunner.manager
Expand Down Expand Up @@ -75,14 +75,14 @@ export class ClosureSubjectExecutor {
firstQueryParameters.push(childEntityIdValues[index]);
return this.queryRunner.connection.driver.createParameter("child_entity_" + column.databaseName, firstQueryParameters.length - 1);
});
const whereCondition = subject.metadata.primaryColumns.map(column => {
const columnName = escape(column.databaseName + "_descendant");
const parentId = column.getEntityValue(parent);
const whereCondition = subject.metadata.closureJunctionTable.descendantColumns.map(column => {
const columnName = escape(column.databaseName);
const parentId = column.referencedColumn!.getEntityValue(parent);
if (!parentId)
throw new CannotAttachTreeChildrenEntityError(subject.metadata.name);

firstQueryParameters.push(parentId);
const parameterName = this.queryRunner.connection.driver.createParameter("parent_entity_" + column.databaseName, firstQueryParameters.length - 1);
const parameterName = this.queryRunner.connection.driver.createParameter("parent_entity_" + column.referencedColumn!.databaseName, firstQueryParameters.length - 1);
return columnName + " = " + parameterName;
}).join(", ");

Expand All @@ -109,4 +109,4 @@ export class ClosureSubjectExecutor {
}


}
}
30 changes: 30 additions & 0 deletions test/github-issues/7068/entity/Category.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {PrimaryGeneratedColumn} from "../../../../src/decorator/columns/PrimaryGeneratedColumn";
import {Column} from "../../../../src/decorator/columns/Column";
import {TreeParent} from "../../../../src/decorator/tree/TreeParent";
import {TreeChildren} from "../../../../src/decorator/tree/TreeChildren";
import {Entity} from "../../../../src/decorator/entity/Entity";
import {Tree} from "../../../../src/decorator/tree/Tree";

@Entity()
@Tree("closure-table", {
closureTableName: "category_xyz_closure",
ancestorColumnName: (column) => "ancestor_xyz_" + column.propertyName,
descendantColumnName: (column) => "descendant_xyz_" + column.propertyName,
})
export class Category {

@PrimaryGeneratedColumn()
id: number;

@Column()
name: string;

@TreeParent()
parentCategory: Category;

@TreeChildren({cascade: true})
childCategories: Category[];

// @TreeLevelColumn()
// level: number;
}

0 comments on commit bcd998b

Please sign in to comment.