added basic support for tree tables - closure tables, nested set, materialized path

This commit is contained in:
Umed Khudoiberdiev 2017-12-17 22:01:11 +05:00
parent 2fd585f901
commit 42d6c88031
39 changed files with 1004 additions and 315 deletions

View File

@ -17,7 +17,6 @@ feel free to ask us and community.
* now relation id can be set directly to relation, e.g. `Post { @ManyToOne(type => Tag) tag: Tag|number }` with `post.tag = 1` usage.
* now you can disable persistence on any relation by setting `@OneToMany(type => Post, post => tag, { persistence: false })`. This can dramatically improve entity save performance.
* `loadAllRelationIds` method of `QueryBuilder` now accepts list of relation paths that needs to be loaded, also `disableMixedMap` option is now by default set to false, but you can enable it via new method parameter `options`
* lot of changes affect closure table pattern which is planned for fix in 0.3.0
* now `returning` and `output` statements of `InsertQueryBuilder` support array of columns as argument
* now when many-to-many and one-to-many relation set to `null` all items from that relation are removed, just like it would be set to empty array
* fixed issues with relation updation from one-to-one non-owner side
@ -53,6 +52,8 @@ Use `findOne(id)` method instead now.
* `skipSync` in entity options has been renamed to `synchronize`. Now if it set to false schema synchronization for the entity will be disabled.
By default its true.
* now array initializations for relations are forbidden and ORM throws an error if there are entities with initialized relation arrays.
* `@ClosureEntity` decorator has been removed. Instead `@Entity` + `@Tree("closure-table")` must be used
* added support for nested set and materialized path tree hierarchy patterns
## 0.1.10

View File

@ -749,6 +749,6 @@ Learn more about [custom entity repositories](working-with-entity-manager.md).
----
Note: some decorators (like `@ClosureEntity`, `@SingleEntityChild`, `@ClassEntityChild`, `@DiscriminatorColumn`, etc.) aren't
Note: some decorators (like `@Tree`, `@ChildEntity`, etc.) aren't
documented in this reference because they are treated as experimental at the moment.
Expect to see their documentation in the future.

View File

@ -516,9 +516,10 @@ To learn more about closure table take a look at [this awesome presentation by B
Example:
```typescript
import {ClosureEntity, Column, PrimaryGeneratedColumn, TreeChildren, TreeParent, TreeLevelColumn} from "typeorm";
import {Entity, Tree, Column, PrimaryGeneratedColumn, TreeChildren, TreeParent, TreeLevelColumn} from "typeorm";
@ClosureEntity()
@Entity()
@Tree("closure-table")
export class Category {
@PrimaryGeneratedColumn()

View File

@ -204,78 +204,7 @@ await repository.clear();
## `TreeRepository` API
* `findTrees` - Gets complete tree for all roots in the table.
```typescript
const treeCategories = await repository.findTrees();
// returns root categories with sub categories inside
```
* `findRoots` - Roots are entities that have no ancestors. Finds them all.
Does not load children leafs.
```typescript
const rootCategories = await repository.findRoots();
// returns root categories without sub categories inside
```
* `findDescendants` - Gets all children (descendants) of the given entity. Returns them all in a flat array.
```typescript
const childrens = await repository.findDescendants(parentCategory);
// returns all direct subcategories (without its nested categories) of a parentCategory
```
* `findDescendantsTree` - Gets all children (descendants) of the given entity. Returns them in a tree - nested into each other.
```typescript
const childrensTree = await repository.findDescendantsTree(parentCategory);
// returns all direct subcategories (with its nested categories) of a parentCategory
```
* `createDescendantsQueryBuilder` - Creates a query builder used to get descendants of the entities in a tree.
```typescript
const childrens = await repository
.createDescendantsQueryBuilder("category", "categoryClosure", parentCategory)
.andWhere("category.type = 'secondary'")
.getMany();
```
* `countDescendants` - Gets number of descendants of the entity.
```typescript
const childrenCount = await repository.countDescendants(parentCategory);
```
* `findAncestors` - Gets all parent (ancestors) of the given entity. Returns them all in a flat array.
```typescript
const parents = await repository.findAncestors(childCategory);
// returns all direct childCategory's parent categories (without "parent of parents")
```
* `findAncestorsTree` - Gets all parent (ancestors) of the given entity. Returns them in a tree - nested into each other.
```typescript
const parentsTree = await repository.findAncestorsTree(childCategory);
// returns all direct childCategory's parent categories (with "parent of parents")
```
* `createAncestorsQueryBuilder` - Creates a query builder used to get ancestors of the entities in a tree.
```typescript
const parents = await repository
.createAncestorsQueryBuilder("category", "categoryClosure", childCategory)
.andWhere("category.type = 'secondary'")
.getMany();
```
* `countAncestors` - Gets the number of ancestors of the entity.
```typescript
const parentsCount = await repository.countAncestors(childCategory);
```
For `TreeRepository` API refer to [the Tree Entities documentation](./tree-entities.md#working-with-tree-entities).
## `MongoRepository` API

View File

@ -4,12 +4,9 @@ See what amazing new features we are expecting to land in the next TypeORM versi
## Note on 1.0.0 release
We are planning to release a final stable `1.0.0` version somewhere in summer 2018.
We are planning to release a final stable `1.0.0` version in summer 2018.
However TypeORM is already actively used in number of big production systems.
Main API is already very stable, there are only few issues currently we have in following areas:
`class and single table inheritance`, `naming strategy`, `subscribers`, `tree tables`.
All issues in those areas are planning to be fixed in next minor versions.
Your donations and contribution play a big role in achieving this goal.
Main API is already very stable.
TypeORM follows a semantic versioning and until `1.0.0` breaking changes may appear in `0.x.x` versions,
however since API is already quite stable we don't expect too much breaking changes.
@ -23,15 +20,11 @@ npm i typeorm@next
## 0.3.0
- [ ] fix Oracle driver issues and make oracle stable and ready for production use
- [ ] add `@Select` and `@Where` decorators
- [ ] add `addSelectAndMap` functionality to `QueryBuilder`
- [ ] research NativeScript support
- [ ] research internationalization features
- [ ] implement soft deletion
- [ ] research ability to create one-to-many relations without inverse sides
- [ ] research ability to create a single relation with multiple entities at once
- [ ] add more tree-table features: nested set and materialized path; more repository methods
- [ ] cli: create database backup command
- [ ] extend `query` method functionality
- [ ] better support for entity schemas, support inheritance, add xml and yml formats support
@ -41,6 +34,10 @@ npm i typeorm@next
## 0.2.0
- [ ] research NativeScript support
- [x] implement soft deletion
- [x] add more tree-table features: nested set and materialized path; more repository methods
- [ ] fix Oracle driver issues and make oracle stable and ready for production use
- [ ] implement migrations generator for all drivers
- [ ] create example how to use TypeORM in Electron apps
- [ ] finish naming strategy implementation

View File

@ -1,11 +1,15 @@
# Tree Entities
TypeORM supports the Adjacency list and Closure table patterns for storing tree structures.
To learn more about hierarchy table take a look at [this awesome presentation by Bill Karwin](https://www.slideshare.net/billkarwin/models-for-hierarchical-data).
* [Adjacency list](#adjacency-list)
* [Nested set](#nested-set)
* [Materialized Path (aka Path Enumeration)](#nested-set-aka-path-enumeration)
* [Closure table](#closure-table)
* [Working with tree entities](#working-with-tree-entities)
### Adjacency list
## Adjacency list
Adjacency list is a simple model with self-referencing.
The benefit of this approach is simplicity,
@ -36,18 +40,18 @@ export class Category {
```
### Closure table
## Nested set
Closure table stores relations between parent and child in a separate table in a special way.
Its efficient in both reads and writes.
To learn more about closure table take a look at [this awesome presentation by Bill Karwin](https://www.slideshare.net/billkarwin/models-for-hierarchical-data).
Nested set is another pattern of storing tree structures in the database.
Its very efficient for reads, but bad for writes.
You cannot have multiple roots in nested set.
Example:
```typescript
import {ClosureEntity, Column, PrimaryGeneratedColumn, TreeChildren, TreeParent, TreeLevelColumn} from "typeorm";
import {Entity, Tree, Column, PrimaryGeneratedColumn, TreeChildren, TreeParent, TreeLevelColumn} from "typeorm";
@ClosureEntity()
@Entity()
@Tree("nested-set")
export class Category {
@PrimaryGeneratedColumn()
@ -56,16 +60,201 @@ export class Category {
@Column()
name: string;
@TreeChildren()
children: Category[];
@TreeParent()
parent: Category;
}
```
## Materialized Path (aka Path Enumeration)
Materialized Path (also called Path Enumeration) is another pattern of storing tree structures in the database.
Its simple and effective.
Example:
```typescript
import {Entity, Tree, Column, PrimaryGeneratedColumn, TreeChildren, TreeParent, TreeLevelColumn} from "typeorm";
@Entity()
@Tree("materialized-path")
export class Category {
@PrimaryGeneratedColumn()
id: number;
@Column()
description: string;
name: string;
@TreeChildren()
children: Category[];
@TreeParent()
parent: Category;
@TreeLevelColumn()
level: number;
}
```
## Closure table
Closure table stores relations between parent and child in a separate table in a special way.
Its efficient for both reads and writes.
Example:
```typescript
import {Entity, Tree, Column, PrimaryGeneratedColumn, TreeChildren, TreeParent, TreeLevelColumn} from "typeorm";
@Entity()
@Tree("closure-table")
export class Category {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@TreeChildren()
children: Category[];
@TreeParent()
parent: Category;
}
```
## Working with tree entities
To make bind tree entities to each other its important to set to children entities their parent and save them,
for example:
```typescript
const manager = getManager();
const a1 = new Category("a1");
a1.name = "a1";
await manager.save(a1);
const a11 = new Category();
a11.name = "a11";
a11.parent = a1;
await manager.save(a11);
const a12 = new Category();
a12.name = "a12";
a12.parent = a1;
await manager.save(a12);
const a111 = new Category();
a111.name = "a111";
a111.parent = a11;
await manager.save(a111);
const a112 = new Category();
a112.name = "a112";
a112.parent = a11;
await manager.save(a112);
```
To load such a tree use `TreeRepository`:
```typescript
const manager = getManager();
const trees = await manager.getTreeRepository(Category).findTrees();
```
`trees` will be following:
```json
[{
"id": 1,
"name": "a1",
"children": [{
"id": 2,
"name": "a11",
"children": [{
"id": 4,
"name": "a111"
}, {
"id": 5,
"name": "a112"
}]
}, {
"id": 3,
"name": "a12"
}]
}]
```
There are other special methods to work with tree entities thought `TreeRepository`:
* `findTrees` - Returns all trees in the database with all their children, children of children, etc.
```typescript
const treeCategories = await repository.findTrees();
// returns root categories with sub categories inside
```
* `findRoots` - Roots are entities that have no ancestors. Finds them all.
Does not load children leafs.
```typescript
const rootCategories = await repository.findRoots();
// returns root categories without sub categories inside
```
* `findDescendants` - Gets all children (descendants) of the given entity. Returns them all in a flat array.
```typescript
const childrens = await repository.findDescendants(parentCategory);
// returns all direct subcategories (without its nested categories) of a parentCategory
```
* `findDescendantsTree` - Gets all children (descendants) of the given entity. Returns them in a tree - nested into each other.
```typescript
const childrensTree = await repository.findDescendantsTree(parentCategory);
// returns all direct subcategories (with its nested categories) of a parentCategory
```
* `createDescendantsQueryBuilder` - Creates a query builder used to get descendants of the entities in a tree.
```typescript
const childrens = await repository
.createDescendantsQueryBuilder("category", "categoryClosure", parentCategory)
.andWhere("category.type = 'secondary'")
.getMany();
```
* `countDescendants` - Gets number of descendants of the entity.
```typescript
const childrenCount = await repository.countDescendants(parentCategory);
```
* `findAncestors` - Gets all parent (ancestors) of the given entity. Returns them all in a flat array.
```typescript
const parents = await repository.findAncestors(childCategory);
// returns all direct childCategory's parent categories (without "parent of parents")
```
* `findAncestorsTree` - Gets all parent (ancestors) of the given entity. Returns them in a tree - nested into each other.
```typescript
const parentsTree = await repository.findAncestorsTree(childCategory);
// returns all direct childCategory's parent categories (with "parent of parents")
```
* `createAncestorsQueryBuilder` - Creates a query builder used to get ancestors of the entities in a tree.
```typescript
const parents = await repository
.createAncestorsQueryBuilder("category", "categoryClosure", childCategory)
.andWhere("category.type = 'secondary'")
.getMany();
```
* `countAncestors` - Gets the number of ancestors of the entity.
```typescript
const parentsCount = await repository.countAncestors(childCategory);

View File

@ -19,6 +19,6 @@ await userRepository.save(user);
There are 3 types of repositories:
* `Repository` - Regular repository for any entity
* `TreeRepository` - Repository, extensions of `Repository` used for tree-entities
(like entities marked with `@ClosureEntity` decorator).
(like entities marked with `@Tree` decorator).
Has special methods to work with tree structures.
* `MongoRepository` - Repository with special functions used only with MongoDB.

View File

@ -40,12 +40,6 @@ exports.Column = Column;
}
exports.CreateDateColumn = CreateDateColumn;
/* export */ function DiscriminatorColumn(discriminatorOptions) {
return function (object, propertyName) {
};
}
exports.DiscriminatorColumn = DiscriminatorColumn;
/* export */
function ObjectIdColumn(typeOrOptions, options) {
return function (object, propertyName) {
@ -185,35 +179,11 @@ exports.RelationId = RelationId;
// entities
/* export */ function AbstractEntity() {
/* export */ function ChildEntity(tableName, options) {
return function (object) {
};
}
exports.AbstractEntity = AbstractEntity;
/* export */ function ClassEntityChild(tableName, options) {
return function (object) {
};
}
exports.ClassEntityChild = ClassEntityChild;
/* export */ function ClosureEntity(name, options) {
return function (object) {
};
}
exports.ClosureEntity = ClosureEntity;
/* export */ function EmbeddableEntity() {
return function (object) {
};
}
exports.EmbeddableEntity = EmbeddableEntity;
/* export */ function SingleEntityChild() {
return function (object) {
};
}
exports.SingleEntityChild = SingleEntityChild;
exports.ChildEntity = ChildEntity;
/* export */ function Entity(name, options) {
return function (object) {
@ -227,46 +197,14 @@ exports.Entity = Entity;
}
exports.TableInheritance = TableInheritance;
// tables (deprecated)
/* export */ function AbstractTable() {
return function (object) {
};
}
exports.AbstractTable = AbstractTable;
/* export */ function ClassTableChild(tableName, options) {
return function (object) {
};
}
exports.ClassTableChild = ClassTableChild;
/* export */ function ClosureTable(name, options) {
return function (object) {
};
}
exports.ClosureTable = ClosureTable;
/* export */ function EmbeddableTable() {
return function (object) {
};
}
exports.EmbeddableTable = EmbeddableTable;
/* export */ function SingleTableChild() {
return function (object) {
};
}
exports.SingleTableChild = SingleTableChild;
/* export */ function Table(name, options) {
return function (object) {
};
}
exports.Table = Table;
// tree
/* export */ function Tree(name, options) {
return function (object) {
};
}
exports.Tree = Tree;
/* export */ function TreeChildren(options) {
return function (object, propertyName) {
class_transformer_1.Type(typeFunction)(object, propertyName);
@ -274,6 +212,13 @@ exports.Table = Table;
}
exports.TreeChildren = TreeChildren;
/* export */ function TreeChildrenCount(options) {
return function (object, propertyName) {
class_transformer_1.Type(typeFunction)(object, propertyName);
};
}
exports.TreeChildrenCount = TreeChildrenCount;
/* export */ function TreeLevelColumn() {
return function (object, propertyName) {
};
@ -289,11 +234,11 @@ exports.TreeParent = TreeParent;
// other
/* export */ function DiscriminatorValue(options) {
/* export */ function Generated(options) {
return function (object, propertyName) {
};
}
exports.DiscriminatorValue = DiscriminatorValue;
exports.Generated = Generated;
/* export */ function Index() {
return function (object, propertyName) {

View File

@ -35,12 +35,6 @@ exports.Column = Column;
}
exports.CreateDateColumn = CreateDateColumn;
/* export */ function DiscriminatorColumn(discriminatorOptions) {
return function (object, propertyName) {
};
}
exports.DiscriminatorColumn = DiscriminatorColumn;
/* export */ function ObjectIdColumn(columnOptions) {
return function (object, propertyName) {
};
@ -73,35 +67,11 @@ exports.VersionColumn = VersionColumn;
// entities
/* export */ function AbstractEntity() {
/* export */ function ChildEntity(tableName, options) {
return function (object) {
};
}
exports.AbstractEntity = AbstractEntity;
/* export */ function ClassEntityChild(tableName, options) {
return function (object) {
};
}
exports.ClassEntityChild = ClassEntityChild;
/* export */ function ClosureEntity(name, options) {
return function (object) {
};
}
exports.ClosureEntity = ClosureEntity;
/* export */ function EmbeddableEntity() {
return function (object) {
};
}
exports.EmbeddableEntity = EmbeddableEntity;
/* export */ function SingleEntityChild() {
return function (object) {
};
}
exports.SingleEntityChild = SingleEntityChild;
exports.ChildEntity = ChildEntity;
/* export */ function Entity(name, options) {
return function (object) {
@ -215,52 +185,26 @@ exports.RelationCount = RelationCount;
}
exports.RelationId = RelationId;
// tables (deprecated)
/* export */ function AbstractTable() {
return function (object) {
};
}
exports.AbstractTable = AbstractTable;
/* export */ function ClassTableChild(tableName, options) {
return function (object) {
};
}
exports.ClassTableChild = ClassTableChild;
/* export */ function ClosureTable(name, options) {
return function (object) {
};
}
exports.ClosureTable = ClosureTable;
/* export */ function EmbeddableTable() {
return function (object) {
};
}
exports.EmbeddableTable = EmbeddableTable;
/* export */ function SingleTableChild() {
return function (object) {
};
}
exports.SingleTableChild = SingleTableChild;
/* export */ function Table(name, options) {
return function (object) {
};
}
exports.Table = Table;
// tree
/* export */ function Tree(name, options) {
return function (object) {
};
}
exports.Tree = Tree;
/* export */ function TreeChildren(options) {
return function (object, propertyName) {
};
}
exports.TreeChildren = TreeChildren;
/* export */ function TreeChildrenCount(options) {
return function (object, propertyName) {
};
}
exports.TreeChildrenCount = TreeChildrenCount;
/* export */ function TreeLevelColumn() {
return function (object, propertyName) {
};
@ -275,11 +219,11 @@ exports.TreeParent = TreeParent;
// other
/* export */ function DiscriminatorValue(options) {
/* export */ function Generated(options) {
return function (object, propertyName) {
};
}
exports.DiscriminatorValue = DiscriminatorValue;
exports.Generated = Generated;
/* export */ function Index(options) {
return function (object, propertyName) {

View File

@ -1,10 +1,12 @@
import {Column, PrimaryGeneratedColumn} from "../../../src/index";
import {TreeLevelColumn} from "../../../src/decorator/tree/TreeLevelColumn";
import {ClosureEntity} from "../../../src/decorator/entity/ClosureEntity";
import {TreeParent} from "../../../src/decorator/tree/TreeParent";
import {TreeChildren} from "../../../src/decorator/tree/TreeChildren";
import {Tree} from "../../../src/decorator/tree/Tree";
import {Entity} from "../../../src/decorator/entity/Entity";
@ClosureEntity("sample22_category")
@Entity("sample22_category")
@Tree("closure-table")
export class Category {
@PrimaryGeneratedColumn()
@ -23,7 +25,7 @@ export class Category {
level: number;
// todo:
// @RelationsCountColumn()
// @TreeChildrenCount()
// categoriesCount: number;
}

View File

@ -315,7 +315,7 @@ export class Connection {
/**
* Gets tree repository for the given entity class or name.
* Only tree-type entities can have a TreeRepository, like ones decorated with @ClosureEntity decorator.
* Only tree-type entities can have a TreeRepository, like ones decorated with @Tree decorator.
*/
getTreeRepository<Entity>(target: ObjectType<Entity>|string): TreeRepository<Entity> {
return this.manager.getTreeRepository(target);

View File

@ -1,19 +0,0 @@
import {getMetadataArgsStorage} from "../../index";
import {TableMetadataArgs} from "../../metadata-args/TableMetadataArgs";
import {EntityOptions} from "../options/EntityOptions";
/**
* Used on a entities that stores its children in a tree using closure design pattern.
*/
export function ClosureEntity(name?: string, options?: EntityOptions) {
return function (target: Function) {
const args: TableMetadataArgs = {
target: target,
name: name,
type: "closure",
orderBy: options && options.orderBy ? options.orderBy : undefined,
synchronize: options && options.synchronize === false ? false : true
};
getMetadataArgsStorage().tables.push(args);
};
}

View File

@ -0,0 +1,16 @@
import {getMetadataArgsStorage} from "../../index";
import {TreeMetadataArgs} from "../../metadata-args/TreeMetadataArgs";
import {TreeType} from "../../metadata/types/TreeTypes";
/**
* Marks entity to work like a tree.
*/
export function Tree(type: TreeType): Function {
return function (target: Function) {
const args: TreeMetadataArgs = {
target: target,
type: type
};
getMetadataArgsStorage().trees.push(args);
};
}

View File

@ -92,6 +92,8 @@ export interface Driver {
/**
* Escapes a table name, column name or an alias.
*
* todo: probably escape should be able to handle dots in the names and automatically escape them
*/
escape(name: string): string;

View File

@ -7,7 +7,7 @@ export class RepositoryNotTreeError extends Error {
constructor(entityClass: Function|string) {
super();
const targetName = typeof entityClass === "function" && (<any> entityClass).name ? (<any> entityClass).name : entityClass;
this.message = `Repository of the "${targetName}" class is not a TreeRepository. Try to use @ClosureEntity decorator instead of @Entity.`;
this.message = `Repository of the "${targetName}" class is not a TreeRepository. Try to apply @Tree decorator on your entity.`;
this.stack = new Error().stack;
}

View File

@ -55,7 +55,6 @@ export * from "./decorator/relations/OneToOne";
export * from "./decorator/relations/RelationCount";
export * from "./decorator/relations/RelationId";
export * from "./decorator/entity/Entity";
export * from "./decorator/entity/ClosureEntity";
export * from "./decorator/entity/ChildEntity";
export * from "./decorator/entity/TableInheritance";
export * from "./decorator/transaction/Transaction";
@ -64,6 +63,7 @@ export * from "./decorator/transaction/TransactionRepository";
export * from "./decorator/tree/TreeLevelColumn";
export * from "./decorator/tree/TreeParent";
export * from "./decorator/tree/TreeChildren";
export * from "./decorator/tree/Tree";
export * from "./decorator/Index";
export * from "./decorator/Generated";
export * from "./decorator/EntityRepository";

View File

@ -17,6 +17,7 @@ import {TransactionEntityMetadataArgs} from "./TransactionEntityMetadataArgs";
import {TransactionRepositoryMetadataArgs} from "./TransactionRepositoryMetadataArgs";
import {MetadataUtils} from "../metadata-builder/MetadataUtils";
import {GeneratedMetadataArgs} from "./GeneratedMetadataArgs";
import {TreeMetadataArgs} from "./TreeMetadataArgs";
/**
* Storage all metadatas args of all available types: tables, columns, subscribers, relations, etc.
@ -30,6 +31,7 @@ export class MetadataArgsStorage {
// -------------------------------------------------------------------------
readonly tables: TableMetadataArgs[] = [];
readonly trees: TreeMetadataArgs[] = [];
readonly entityRepositories: EntityRepositoryMetadataArgs[] = [];
readonly transactionEntityManagers: TransactionEntityMetadataArgs[] = [];
readonly transactionRepositories: TransactionRepositoryMetadataArgs[] = [];
@ -72,6 +74,12 @@ export class MetadataArgsStorage {
});
}
findTree(target: (Function|string)|(Function|string)[]): TreeMetadataArgs|undefined {
return this.trees.find(tree => {
return (target instanceof Array ? target.indexOf(tree.target) !== -1 : tree.target === target);
});
}
filterRelations(target: Function|string): RelationMetadataArgs[];
filterRelations(target: (Function|string)[]): RelationMetadataArgs[];
filterRelations(target: (Function|string)|(Function|string)[]): RelationMetadataArgs[] {

View File

@ -0,0 +1,18 @@
import {TreeType} from "../metadata/types/TreeTypes";
/**
* Stores metadata collected for Tree entities.
*/
export interface TreeMetadataArgs {
/**
* Entity to which tree is applied.
*/
target: Function|string;
/**
* Tree type.
*/
type: TreeType;
}

View File

@ -42,10 +42,12 @@ export class ClosureJunctionEntityMetadataBuilder {
entityMetadata.ownColumns.push(new ColumnMetadata({
connection: this.connection,
entityMetadata: entityMetadata,
closureType: "ancestor",
referencedColumn: primaryColumn,
args: {
target: "",
mode: "virtual",
propertyName: "ancestor", // todo: naming strategy
propertyName: primaryColumn.propertyName + "_ancestor", // todo: naming strategy
options: {
length: primaryColumn.length,
type: primaryColumn.type,
@ -55,10 +57,12 @@ export class ClosureJunctionEntityMetadataBuilder {
entityMetadata.ownColumns.push(new ColumnMetadata({
connection: this.connection,
entityMetadata: entityMetadata,
closureType: "descendant",
referencedColumn: primaryColumn,
args: {
target: "",
mode: "virtual",
propertyName: "descendant",
propertyName: primaryColumn.propertyName + "_descendant",
options: {
length: primaryColumn.length,
type: primaryColumn.type,

View File

@ -136,7 +136,7 @@ export class EntityMetadataBuilder {
// generate closure junction tables for all closure tables
entityMetadatas
.filter(metadata => metadata.isClosure)
.filter(metadata => metadata.treeType === "closure-table")
.forEach(entityMetadata => {
const closureJunctionEntityMetadata = this.closureJunctionEntityMetadataBuilder.build(entityMetadata);
entityMetadata.closureJunctionTable = closureJunctionEntityMetadata;
@ -211,6 +211,7 @@ export class EntityMetadataBuilder {
: [tableArgs.target]; // todo: implement later here inheritance for string-targets
const tableInheritance = this.metadataArgsStorage.findInheritanceType(tableArgs.target);
const tableTree = this.metadataArgsStorage.findTree(tableArgs.target);
// if single table inheritance used, we need to copy all children columns in to parent table
let singleTableChildrenTargets: any[];
@ -227,6 +228,7 @@ export class EntityMetadataBuilder {
connection: this.connection,
args: tableArgs,
inheritanceTree: inheritanceTree,
tableTree: tableTree,
inheritancePattern: tableInheritance ? tableInheritance.pattern : undefined
});
}
@ -280,7 +282,7 @@ export class EntityMetadataBuilder {
mode: "virtual",
propertyName: discriminatorColumnName,
options: entityInheritance.column || {
name: "type",
name: discriminatorColumnName,
type: "varchar",
nullable: false
}
@ -303,6 +305,60 @@ export class EntityMetadataBuilder {
}
}
// check if tree is used then we need to add extra columns for specific tree types
if (entityMetadata.treeType === "materialized-path") {
entityMetadata.ownColumns.push(new ColumnMetadata({
connection: this.connection,
entityMetadata: entityMetadata,
materializedPath: true,
args: {
target: entityMetadata.target,
mode: "virtual",
propertyName: "mpath",
options: /*tree.column || */ {
name: "mpath",
type: "varchar",
nullable: false,
default: ""
}
}
}));
} else if (entityMetadata.treeType === "nested-set") {
entityMetadata.ownColumns.push(new ColumnMetadata({
connection: this.connection,
entityMetadata: entityMetadata,
nestedSetLeft: true,
args: {
target: entityMetadata.target,
mode: "virtual",
propertyName: "nsleft",
options: /*tree.column || */ {
name: "nsleft",
type: "integer",
nullable: false,
default: 1
}
}
}));
entityMetadata.ownColumns.push(new ColumnMetadata({
connection: this.connection,
entityMetadata: entityMetadata,
nestedSetRight: true,
args: {
target: entityMetadata.target,
mode: "virtual",
propertyName: "nsright",
options: /*tree.column || */ {
name: "nsright",
type: "integer",
nullable: false,
default: 2
}
}
}));
}
entityMetadata.ownRelations = this.metadataArgsStorage.filterRelations(entityMetadata.inheritanceTree).map(args => {
// for single table children we reuse relations created for their parents
@ -403,6 +459,8 @@ export class EntityMetadataBuilder {
entityMetadata.indices = entityMetadata.embeddeds.reduce((columns, embedded) => columns.concat(embedded.indicesFromTree), entityMetadata.ownIndices);
entityMetadata.primaryColumns = entityMetadata.columns.filter(column => column.isPrimary);
entityMetadata.nonVirtualColumns = entityMetadata.columns.filter(column => !column.isVirtual);
entityMetadata.ancestorColumns = entityMetadata.columns.filter(column => column.closureType === "ancestor");
entityMetadata.descendantColumns = entityMetadata.columns.filter(column => column.closureType === "descendant");
entityMetadata.hasMultiplePrimaryKeys = entityMetadata.primaryColumns.length > 1;
entityMetadata.generatedColumns = entityMetadata.columns.filter(column => column.isGenerated || column.isObjectId);
entityMetadata.hasUUIDGeneratedColumns = entityMetadata.columns.filter(column => column.isGenerated || column.generationStrategy === "uuid").length > 0;
@ -411,6 +469,9 @@ export class EntityMetadataBuilder {
entityMetadata.versionColumn = entityMetadata.columns.find(column => column.isVersion);
entityMetadata.discriminatorColumn = entityMetadata.columns.find(column => column.isDiscriminator);
entityMetadata.treeLevelColumn = entityMetadata.columns.find(column => column.isTreeLevel);
entityMetadata.nestedSetLeftColumn = entityMetadata.columns.find(column => column.isNestedSetLeft);
entityMetadata.nestedSetRightColumn = entityMetadata.columns.find(column => column.isNestedSetRight);
entityMetadata.materializedPathColumn = entityMetadata.columns.find(column => column.isMaterializedPath);
entityMetadata.objectIdColumn = entityMetadata.columns.find(column => column.isObjectId);
entityMetadata.foreignKeys.forEach(foreignKey => foreignKey.build(this.connection.namingStrategy));
entityMetadata.propertiesMap = entityMetadata.createPropertiesMap();

View File

@ -215,6 +215,30 @@ export class ColumnMetadata {
*/
transformer?: ValueTransformer;
/**
* Column type in the case if this column is in the closure table.
* Column can be ancestor or descendant in the closure tables.
*/
closureType?: "ancestor"|"descendant";
/**
* Indicates if this column is nested set's left column.
* Used only in tree entities with nested-set type.
*/
isNestedSetLeft: boolean = false;
/**
* Indicates if this column is nested set's right column.
* Used only in tree entities with nested-set type.
*/
isNestedSetRight: boolean = false;
/**
* Indicates if this column is materialized path's path column.
* Used only in tree entities with materialized path type.
*/
isMaterializedPath: boolean = false;
// ---------------------------------------------------------------------
// Constructor
// ---------------------------------------------------------------------
@ -224,7 +248,11 @@ export class ColumnMetadata {
entityMetadata: EntityMetadata,
embeddedMetadata?: EmbeddedMetadata,
referencedColumn?: ColumnMetadata,
args: ColumnMetadataArgs
args: ColumnMetadataArgs,
closureType?: "ancestor"|"descendant",
nestedSetLeft?: boolean,
nestedSetRight?: boolean,
materializedPath?: boolean,
}) {
this.entityMetadata = options.entityMetadata;
this.embeddedMetadata = options.embeddedMetadata!;
@ -303,6 +331,14 @@ export class ColumnMetadata {
}
if (this.isVersion)
this.type = options.connection.driver.mappedDataTypes.version;
if (options.closureType)
this.closureType = options.closureType;
if (options.nestedSetLeft)
this.isNestedSetLeft = options.nestedSetLeft;
if (options.nestedSetRight)
this.isNestedSetRight = options.nestedSetRight;
if (options.materializedPath)
this.isMaterializedPath = options.materializedPath;
}
// ---------------------------------------------------------------------

View File

@ -17,6 +17,8 @@ import {SqlServerDriver} from "../driver/sqlserver/SqlServerDriver";
import {PostgresConnectionOptions} from "../driver/postgres/PostgresConnectionOptions";
import {SqlServerConnectionOptions} from "../driver/sqlserver/SqlServerConnectionOptions";
import {CannotCreateEntityIdMapError} from "../error/CannotCreateEntityIdMapError";
import {TreeType} from "./types/TreeTypes";
import {TreeMetadataArgs} from "../metadata-args/TreeMetadataArgs";
/**
* Contains all entity metadata.
@ -167,10 +169,9 @@ export class EntityMetadata {
isJunction: boolean = false;
/**
* Checks if this table is a closure table.
* Closure table is one of the tree-specific tables that supports closure database pattern.
* Indicates if this entity is a tree, what type of tree it is.
*/
isClosure: boolean = false;
treeType?: TreeType;
/**
* Checks if this table is a junction table of the closure table.
@ -204,6 +205,16 @@ export class EntityMetadata {
*/
columns: ColumnMetadata[] = [];
/**
* Ancestor columns used only in closure junction tables.
*/
ancestorColumns: ColumnMetadata[] = [];
/**
* Descendant columns used only in closure junction tables.
*/
descendantColumns: ColumnMetadata[] = [];
/**
* All columns except for virtual columns.
*/
@ -256,6 +267,24 @@ export class EntityMetadata {
*/
treeLevelColumn?: ColumnMetadata;
/**
* Nested set's left value column.
* Used only in tree entities with nested set pattern applied.
*/
nestedSetLeftColumn?: ColumnMetadata;
/**
* Nested set's right value column.
* Used only in tree entities with nested set pattern applied.
*/
nestedSetRightColumn?: ColumnMetadata;
/**
* Materialized path column.
* Used only in tree entities with materialized path pattern applied.
*/
materializedPathColumn?: ColumnMetadata;
/**
* Gets the primary columns.
*/
@ -423,12 +452,14 @@ export class EntityMetadata {
connection: Connection,
inheritanceTree?: Function[],
inheritancePattern?: "STI"/*|"CTI"*/,
tableTree?: TreeMetadataArgs,
parentClosureEntityMetadata?: EntityMetadata,
args: TableMetadataArgs
}) {
this.connection = options.connection;
this.inheritanceTree = options.inheritanceTree || [];
this.inheritancePattern = options.inheritancePattern;
this.treeType = options.tableTree ? options.tableTree.type : undefined;
this.parentClosureEntityMetadata = options.parentClosureEntityMetadata!;
this.tableMetadataArgs = options.args;
this.target = this.tableMetadataArgs.target;
@ -701,7 +732,6 @@ export class EntityMetadata {
this.isJunction = this.tableMetadataArgs.type === "closure-junction" || this.tableMetadataArgs.type === "junction";
this.isClosureJunction = this.tableMetadataArgs.type === "closure-junction";
this.isClosure = this.tableMetadataArgs.type === "closure";
}
/**

View File

@ -0,0 +1,4 @@
/**
* Tree type.
*/
export type TreeType = "adjacency-list"|"closure-table"|"nested-set"|"materialized-path";

View File

@ -12,6 +12,10 @@ import {ObjectLiteral} from "../common/ObjectLiteral";
import {SaveOptions} from "../repository/SaveOptions";
import {RemoveOptions} from "../repository/RemoveOptions";
import {BroadcasterResult} from "../subscriber/BroadcasterResult";
import {OracleDriver} from "../driver/oracle/OracleDriver";
import {NestedSetSubjectExecutor} from "./tree/NestedSetSubjectExecutor";
import {ClosureSubjectExecutor} from "./tree/ClosureSubjectExecutor";
import {MaterializedPathSubjectExecutor} from "./tree/MaterializedPathSubjectExecutor";
/**
* Executes all database operations (inserts, updated, deletes) that must be executed
@ -221,7 +225,9 @@ export class SubjectExecutor {
});
} else {
subjects.forEach(subject => {
if (subject.changeMaps.length === 0) {
if (subject.changeMaps.length === 0 ||
subject.metadata.treeType ||
this.queryRunner.connection.driver instanceof OracleDriver) {
singleInsertSubjects.push(subject);
} else {
@ -268,22 +274,34 @@ export class SubjectExecutor {
// insert subjects which must be inserted in separate requests (all default values)
if (singleInsertSubjects.length > 0) {
await Promise.all(singleInsertSubjects.map(subject => {
const updatedEntity = {}; // important to have because query builder sets inserted values into it
return this.queryRunner
await Promise.all(singleInsertSubjects.map(async subject => {
subject.insertedValueSet = subject.createValueSetAndPopChangeMap(); // important to have because query builder sets inserted values into it
// for nested set we execute additional queries
if (subject.metadata.treeType === "nested-set")
await new NestedSetSubjectExecutor(this.queryRunner).insert(subject);
await this.queryRunner
.manager
.createQueryBuilder()
.insert()
.into(subject.metadata.target)
.values(updatedEntity)
.values(subject.insertedValueSet)
.updateEntity(this.options && this.options.reload === false ? false : true)
.callListeners(false)
.execute()
.then(insertResult => {
subject.identifier = insertResult.identifiers[0];
subject.generatedMap = insertResult.generatedMaps[0];
subject.insertedValueSet = updatedEntity;
});
// for tree tables we execute additional queries
if (subject.metadata.treeType === "closure-table") {
await new ClosureSubjectExecutor(this.queryRunner).insert(subject);
} else if (subject.metadata.treeType === "materialized-path") {
await new MaterializedPathSubjectExecutor(this.queryRunner).insert(subject);
}
}));
}
}

View File

@ -0,0 +1,76 @@
import {Subject} from "../Subject";
import {QueryRunner} from "../../query-runner/QueryRunner";
import {ObjectLiteral} from "../../common/ObjectLiteral";
/**
* Executes subject operations for closure entities.
*/
export class ClosureSubjectExecutor {
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
constructor(protected queryRunner: QueryRunner) {
}
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
/**
* Executes operations when subject is being inserted.
*/
async insert(subject: Subject): Promise<void> {
// create values to be inserted into the closure junction
const closureJunctionInsertMap: ObjectLiteral = {};
subject.metadata.closureJunctionTable.ancestorColumns.forEach(column => {
closureJunctionInsertMap[column.databaseName] = subject.identifier;
});
subject.metadata.closureJunctionTable.descendantColumns.forEach(column => {
closureJunctionInsertMap[column.databaseName] = subject.identifier;
});
// insert values into the closure junction table
await this.queryRunner
.manager
.createQueryBuilder()
.insert()
.into(subject.metadata.closureJunctionTable.tablePath)
.values(closureJunctionInsertMap)
.updateEntity(false)
.callListeners(false)
.execute();
const parent = subject.metadata.treeParentRelation!.getEntityValue(subject.entity!);
if (parent) {
const escape = (alias: string) => this.queryRunner.connection.driver.escape(alias);
const tableName = escape(subject.metadata.closureJunctionTable.tablePath); // todo: make sure to properly escape table path, not just a table name
const ancestorColumnNames = subject.metadata.closureJunctionTable.ancestorColumns.map(column => {
return escape(column.databaseName);
});
const descendantColumnNames = subject.metadata.closureJunctionTable.descendantColumns.map(column => {
return escape(column.databaseName);
});
const firstQueryParameters: any[] = [];
const childEntityIdValues = subject.metadata.primaryColumns.map(column => column.getEntityValue(subject.insertedValueSet!));
const childEntityIds1 = subject.metadata.primaryColumns.map((column, index) => {
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");
firstQueryParameters.push(column.getEntityValue(parent));
const parameterName = this.queryRunner.connection.driver.createParameter("parent_entity_" + column.databaseName, firstQueryParameters.length - 1);
return columnName + " = " + parameterName;
}).join(", ");
await this.queryRunner.query(
`INSERT INTO ${tableName} (${[...ancestorColumnNames, ...descendantColumnNames].join(", ")}) ` +
`SELECT ${ancestorColumnNames.join(", ")}, ${childEntityIds1.join(", ")} FROM ${tableName} WHERE ${whereCondition}`,
firstQueryParameters
);
}
}
}

View File

@ -0,0 +1,52 @@
import {Subject} from "../Subject";
import {QueryRunner} from "../../query-runner/QueryRunner";
/**
* Executes subject operations for materialized-path tree entities.
*/
export class MaterializedPathSubjectExecutor {
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
constructor(protected queryRunner: QueryRunner) {
}
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
/**
* Executes operations when subject is being inserted.
*/
async insert(subject: Subject): Promise<void> {
const parent = subject.metadata.treeParentRelation!.getEntityValue(subject.entity!);
const parentId = subject.metadata.getEntityIdMap(parent);
let parentPath: string = "";
if (parentId) {
parentPath = await this.queryRunner.manager
.createQueryBuilder()
.select(subject.metadata.targetName + "." + subject.metadata.materializedPathColumn!.propertyPath, "path")
.from(subject.metadata.target, subject.metadata.targetName)
.whereInIds(parentId)
.getRawOne()
.then(result => result ? result["path"] : undefined);
}
const insertedEntityId = subject.metadata.treeParentRelation!.joinColumns.map(joinColumn => {
return joinColumn.referencedColumn!.getEntityValue(subject.insertedValueSet!);
}).join("_");
await this.queryRunner.manager
.createQueryBuilder()
.update(subject.metadata.target)
.set({
[subject.metadata.materializedPathColumn!.propertyPath]: parentPath + insertedEntityId + "."
})
.where(subject.identifier!)
.execute();
}
}

View File

@ -0,0 +1,63 @@
import {Subject} from "../Subject";
import {QueryRunner} from "../../query-runner/QueryRunner";
import {OrmUtils} from "../../util/OrmUtils";
/**
* Executes subject operations for nested set tree entities.
*/
export class NestedSetSubjectExecutor {
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
constructor(protected queryRunner: QueryRunner) {
}
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
/**
* Executes operations when subject is being inserted.
*/
async insert(subject: Subject): Promise<void> {
const escape = (alias: string) => this.queryRunner.connection.driver.escape(alias);
const tableName = escape(subject.metadata.tablePath);
const leftColumnName = escape(subject.metadata.nestedSetLeftColumn!.databaseName);
const rightColumnName = escape(subject.metadata.nestedSetRightColumn!.databaseName);
const parent = subject.metadata.treeParentRelation!.getEntityValue(subject.entity!);
const parentId = subject.metadata.getEntityIdMap(parent);
let parentNsRight: number|undefined = undefined;
if (parentId) {
parentNsRight = await this.queryRunner.manager
.createQueryBuilder()
.select(subject.metadata.targetName + "." + subject.metadata.nestedSetRightColumn!.propertyPath, "right")
.from(subject.metadata.target, subject.metadata.targetName)
.whereInIds(parentId)
.getRawOne()
.then(result => result ? result["right"] : undefined);
}
if (parentNsRight !== undefined) {
await this.queryRunner.query(`UPDATE ${tableName} SET ` +
`${leftColumnName} = CASE WHEN ${leftColumnName} > ${parentNsRight} THEN ${leftColumnName} + 2 ELSE ${leftColumnName} END,` +
`${rightColumnName} = ${rightColumnName} + 2 ` +
`WHERE ${rightColumnName} >= ${parentNsRight}`);
OrmUtils.mergeDeep(
subject.insertedValueSet,
subject.metadata.nestedSetLeftColumn!.createValueMap(parentNsRight),
subject.metadata.nestedSetRightColumn!.createValueMap(parentNsRight + 1),
);
} else {
OrmUtils.mergeDeep(
subject.insertedValueSet,
subject.metadata.nestedSetLeftColumn!.createValueMap(1),
subject.metadata.nestedSetRightColumn!.createValueMap(2),
);
}
}
}

View File

@ -352,6 +352,18 @@ export class InsertQueryBuilder<Entity> extends QueryBuilder<Entity> {
if (column.isVersion) {
expression += "1";
// } else if (column.isNestedSetLeft) {
// const tableName = this.connection.driver.escape(column.entityMetadata.tablePath);
// const rightColumnName = this.connection.driver.escape(column.entityMetadata.nestedSetRightColumn!.databaseName);
// const subQuery = `(SELECT c.max + 1 FROM (SELECT MAX(${rightColumnName}) as max from ${tableName}) c)`;
// expression += subQuery;
//
// } else if (column.isNestedSetRight) {
// const tableName = this.connection.driver.escape(column.entityMetadata.tablePath);
// const rightColumnName = this.connection.driver.escape(column.entityMetadata.nestedSetRightColumn!.databaseName);
// const subQuery = `(SELECT c.max + 2 FROM (SELECT MAX(${rightColumnName}) as max from ${tableName}) c)`;
// expression += subQuery;
} else if (column.isDiscriminator) {
this.expressionMap.nativeParameters["discriminator_value"] = this.expressionMap.mainAlias!.metadata.discriminatorValue;
expression += this.connection.driver.createParameter("discriminator_value", parametersCount);

View File

@ -20,7 +20,7 @@ export class RepositoryFactory {
*/
create(manager: EntityManager, metadata: EntityMetadata, queryRunner?: QueryRunner): Repository<any> {
if (metadata.isClosure) {
if (metadata.treeType) {
// NOTE: dynamic access to protected properties. We need this to prevent unwanted properties in those classes to be exposed,
// however we need these properties for internal work of the class
const repository = new TreeRepository<any>();

View File

@ -1,5 +1,7 @@
import {Repository} from "./Repository";
import {SelectQueryBuilder} from "../query-builder/SelectQueryBuilder";
import {ObjectLiteral} from "../common/ObjectLiteral";
import {AbstractSqliteDriver} from "../driver/sqlite-abstract/AbstractSqliteDriver";
/**
* Repository with additional functions to work with trees.
@ -20,10 +22,7 @@ export class TreeRepository<Entity> extends Repository<Entity> {
*/
async findTrees(): Promise<Entity[]> {
const roots = await this.findRoots();
await Promise.all(roots.map(async root => {
await this.findDescendantsTree(root);
}));
await Promise.all(roots.map(root => this.findDescendantsTree(root)));
return roots;
}
@ -83,11 +82,59 @@ export class TreeRepository<Entity> extends Repository<Entity> {
// create shortcuts for better readability
const escape = (alias: string) => this.manager.connection.driver.escape(alias);
const joinCondition = `${escape(alias)}.${escape(this.metadata.primaryColumns[0].databaseName)}=${escape(closureTableAlias)}.${escape("descendant")}`;
return this.createQueryBuilder(alias)
.innerJoin(this.metadata.closureJunctionTable.tableName, closureTableAlias, joinCondition)
.where(`${escape(closureTableAlias)}.${escape("ancestor")}=${this.metadata.getEntityIdMap(entity)![this.metadata.primaryColumns[0].propertyName]}`);
if (this.metadata.treeType === "closure-table") {
const joinCondition = this.metadata.closureJunctionTable.descendantColumns.map(column => {
return escape(closureTableAlias) + "." + escape(column.propertyPath) + " = " + escape(alias) + "." + escape(column.referencedColumn!.propertyPath);
}).join(" AND ");
const parameters: ObjectLiteral = {};
const whereCondition = this.metadata.closureJunctionTable.ancestorColumns.map(column => {
parameters[column.referencedColumn!.propertyName] = column.referencedColumn!.getEntityValue(entity);
return escape(closureTableAlias) + "." + escape(column.propertyPath) + " = :" + column.referencedColumn!.propertyName;
}).join(" AND ");
return this
.createQueryBuilder(alias)
.innerJoin(this.metadata.closureJunctionTable.tableName, closureTableAlias, joinCondition)
.where(whereCondition)
.setParameters(parameters);
} else if (this.metadata.treeType === "nested-set") {
const whereCondition = alias + "." + this.metadata.nestedSetLeftColumn!.propertyPath + " BETWEEN " +
"joined." + this.metadata.nestedSetLeftColumn!.propertyPath + " AND joined." + this.metadata.nestedSetRightColumn!.propertyPath;
const parameters: ObjectLiteral = {};
const joinCondition = this.metadata.treeParentRelation!.joinColumns.map(joinColumn => {
const parameterName = joinColumn.referencedColumn!.propertyPath.replace(".", "_");
parameters[parameterName] = joinColumn.referencedColumn!.getEntityValue(entity);
return "joined." + joinColumn.referencedColumn!.propertyPath + " = :" + parameterName;
}).join(" AND ");
return this
.createQueryBuilder(alias)
.innerJoin(this.metadata.targetName, "joined", whereCondition)
.where(joinCondition, parameters);
} else if (this.metadata.treeType === "materialized-path") {
return this
.createQueryBuilder(alias)
.where(qb => {
const subQuery = qb.subQuery()
.select(this.metadata.materializedPathColumn!.propertyPath, "path")
.from(this.metadata.target, this.metadata.targetName)
.whereInIds(this.metadata.getEntityIdMap(entity));
qb.setNativeParameters(subQuery.expressionMap.nativeParameters);
if (this.manager.connection.driver instanceof AbstractSqliteDriver) {
return this.metadata.materializedPathColumn!.propertyPath + " LIKE " + subQuery.getQuery() + " || '%'";
} else {
return this.metadata.materializedPathColumn!.propertyPath + " LIKE CONCAT(" + subQuery.getQuery() + ", '%')";
}
});
}
throw new Error(`Supported only in tree entities`);
}
/**
@ -129,13 +176,63 @@ export class TreeRepository<Entity> extends Repository<Entity> {
createAncestorsQueryBuilder(alias: string, closureTableAlias: string, entity: Entity): SelectQueryBuilder<Entity> {
// create shortcuts for better readability
const escapeAlias = (alias: string) => this.manager.connection.driver.escape(alias);
const escapeColumn = (column: string) => this.manager.connection.driver.escape(column);
const escape = (alias: string) => this.manager.connection.driver.escape(alias);
const joinCondition = `${escapeAlias(alias)}.${escapeColumn(this.metadata.primaryColumns[0].databaseName)}=${escapeAlias(closureTableAlias)}.${escapeColumn("ancestor")}`;
return this.createQueryBuilder(alias)
.innerJoin(this.metadata.closureJunctionTable.tableName, closureTableAlias, joinCondition)
.where(`${escapeAlias(closureTableAlias)}.${escapeColumn("descendant")}=${this.metadata.getEntityIdMap(entity)![this.metadata.primaryColumns[0].propertyName]}`);
if (this.metadata.treeType === "closure-table") {
const joinCondition = this.metadata.closureJunctionTable.ancestorColumns.map(column => {
return escape(closureTableAlias) + "." + escape(column.propertyPath) + " = " + escape(alias) + "." + escape(column.referencedColumn!.propertyPath);
}).join(" AND ");
const parameters: ObjectLiteral = {};
const whereCondition = this.metadata.closureJunctionTable.descendantColumns.map(column => {
parameters[column.referencedColumn!.propertyName] = column.referencedColumn!.getEntityValue(entity);
return escape(closureTableAlias) + "." + escape(column.propertyPath) + " = :" + column.referencedColumn!.propertyName;
}).join(" AND ");
return this
.createQueryBuilder(alias)
.innerJoin(this.metadata.closureJunctionTable.tableName, closureTableAlias, joinCondition)
.where(whereCondition)
.setParameters(parameters);
} else if (this.metadata.treeType === "nested-set") {
const joinCondition = "joined." + this.metadata.nestedSetLeftColumn!.propertyPath + " BETWEEN " +
alias + "." + this.metadata.nestedSetLeftColumn!.propertyPath + " AND " + alias + "." + this.metadata.nestedSetRightColumn!.propertyPath;
const parameters: ObjectLiteral = {};
const whereCondition = this.metadata.treeParentRelation!.joinColumns.map(joinColumn => {
const parameterName = joinColumn.referencedColumn!.propertyPath.replace(".", "_");
parameters[parameterName] = joinColumn.referencedColumn!.getEntityValue(entity);
return "joined." + joinColumn.referencedColumn!.propertyPath + " = :" + parameterName;
}).join(" AND ");
return this
.createQueryBuilder(alias)
.innerJoin(this.metadata.targetName, "joined", joinCondition)
.where(whereCondition, parameters);
} else if (this.metadata.treeType === "materialized-path") {
// example: SELECT * FROM category WHERE (SELECT mpath FROM `category` WHERE id = 2) LIKE CONCAT(mpath, '%');
return this
.createQueryBuilder(alias)
.where(qb => {
const subQuery = qb.subQuery()
.select(this.metadata.materializedPathColumn!.propertyPath, "path")
.from(this.metadata.target, this.metadata.targetName)
.whereInIds(this.metadata.getEntityIdMap(entity));
qb.setNativeParameters(subQuery.expressionMap.nativeParameters);
if (this.manager.connection.driver instanceof AbstractSqliteDriver) {
return subQuery.getQuery() + " LIKE " + this.metadata.materializedPathColumn!.propertyPath + " || '%'";
} else {
return subQuery.getQuery() + " LIKE CONCAT(" + this.metadata.materializedPathColumn!.propertyPath + ", '%')";
}
});
}
throw new Error(`Supported only in tree entities`);
}
/**

View File

@ -1,11 +1,13 @@
import {ClosureEntity} from "../../../../src/decorator/entity/ClosureEntity";
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 {TreeLevelColumn} from "../../../../src/decorator/tree/TreeLevelColumn";
import {Entity} from "../../../../src/decorator/entity/Entity";
import {Tree} from "../../../../src/decorator/tree/Tree";
@ClosureEntity("CaTeGoRy")
@Entity("CaTeGoRy")
@Tree("closure-table")
export class Category {
@PrimaryGeneratedColumn()

View File

@ -3,21 +3,55 @@ import {Category} from "./entity/Category";
import {Connection} from "../../../../src/connection/Connection";
import {closeTestingConnections, createTestingConnections, reloadTestingDatabases} from "../../../utils/test-utils";
// todo: uncomment test once closure tables functionality is back
describe.skip("closure-table", () => {
describe("tree tables > closure-table", () => {
let connections: Connection[];
before(async () => connections = await createTestingConnections({
entities: [Category],
entities: [Category]
}));
beforeEach(() => reloadTestingDatabases(connections));
after(() => closeTestingConnections(connections));
it("should work correctly when saving using parent category", () => Promise.all(connections.map(async connection => {
it("attach should work properly", () => Promise.all(connections.map(async connection => {
const categoryRepository = connection.getTreeRepository(Category);
const a1 = new Category();
a1.name = "a1";
await categoryRepository.save(a1);
const a11 = new Category();
a11.name = "a11";
a11.parentCategory = a1;
await categoryRepository.save(a11);
const a12 = new Category();
a12.name = "a12";
a12.parentCategory = a1;
await categoryRepository.save(a12);
const rootCategories = await categoryRepository.findRoots();
rootCategories.should.be.eql([{
id: 1,
name: "a1"
}]);
const a11Parent = await categoryRepository.findAncestors(a11);
a11Parent.length.should.be.equal(2);
a11Parent.should.include({ id: 1, name: "a1" });
a11Parent.should.include({ id: 2, name: "a11" });
const a1Children = await categoryRepository.findDescendants(a1);
a1Children.length.should.be.equal(3);
a1Children.should.include({ id: 1, name: "a1" });
a1Children.should.include({ id: 2, name: "a11" });
a1Children.should.include({ id: 3, name: "a12" });
})));
it.skip("should work correctly when saving using parent category", () => Promise.all(connections.map(async connection => {
// await categoryRepository.attach(a1, a11);
/*const a1 = new Category();
a1.name = "a1";
const b1 = new Category();
b1.name = "b1";
@ -79,11 +113,11 @@ describe.skip("closure-table", () => {
level: 2,
childCategories: []
}]
});
});*/
})));
it("should work correctly when saving using children categories", () => Promise.all(connections.map(async connection => {
it.skip("should work correctly when saving using children categories", () => Promise.all(connections.map(async connection => {
const categoryRepository = connection.getTreeRepository(Category);
const a1 = new Category();
@ -153,7 +187,7 @@ describe.skip("closure-table", () => {
})));
it("should be able to retrieve the whole tree", () => Promise.all(connections.map(async connection => {
it.skip("should be able to retrieve the whole tree", () => Promise.all(connections.map(async connection => {
const categoryRepository = connection.getTreeRepository(Category);
const a1 = new Category();

View File

@ -1,11 +1,12 @@
import {ClosureEntity} from "../../../../../src/decorator/entity/ClosureEntity";
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 {TreeLevelColumn} from "../../../../../src/decorator/tree/TreeLevelColumn";
import {Entity} from "../../../../../src/decorator/entity/Entity";
import {Tree} from "../../../../../src/decorator/tree/Tree";
@ClosureEntity()
@Entity()
@Tree("closure-table")
export class Category {
@PrimaryGeneratedColumn()
@ -20,7 +21,7 @@ export class Category {
@TreeChildren({ cascade: true })
childCategories: Category[];
@TreeLevelColumn()
level: number;
// @TreeLevelColumn()
// level: number;
}

View File

@ -0,0 +1,27 @@
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("materialized-path")
export class Category {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@TreeParent({ cascade: true })
parentCategory: Category;
@TreeChildren({ cascade: true })
childCategories: Category[];
// @TreeLevelColumn()
// level: number;
}

View File

@ -0,0 +1,56 @@
import "reflect-metadata";
import {Category} from "./entity/Category";
import {Connection} from "../../../../src/connection/Connection";
import {closeTestingConnections, createTestingConnections, reloadTestingDatabases} from "../../../utils/test-utils";
describe("tree tables > materialized-path", () => {
let connections: Connection[];
before(async () => connections = await createTestingConnections({
entities: [Category],
}));
beforeEach(() => reloadTestingDatabases(connections));
after(() => closeTestingConnections(connections));
it("attach should work properly", () => Promise.all(connections.map(async connection => {
const categoryRepository = connection.getTreeRepository(Category);
const a1 = new Category();
a1.name = "a1";
await categoryRepository.save(a1);
const a11 = new Category();
a11.name = "a11";
a11.parentCategory = a1;
await categoryRepository.save(a11);
const a111 = new Category();
a111.name = "a111";
a111.parentCategory = a11;
await categoryRepository.save(a111);
const a12 = new Category();
a12.name = "a12";
a12.parentCategory = a1;
await categoryRepository.save(a12);
const rootCategories = await categoryRepository.findRoots();
rootCategories.should.be.eql([{
id: 1,
name: "a1"
}]);
const a11Parent = await categoryRepository.findAncestors(a11);
a11Parent.length.should.be.equal(2);
a11Parent.should.contain({ id: 1, name: "a1" });
a11Parent.should.contain({ id: 2, name: "a11" });
const a1Children = await categoryRepository.findDescendants(a1);
a1Children.length.should.be.equal(4);
a1Children.should.contain({ id: 1, name: "a1" });
a1Children.should.contain({ id: 2, name: "a11" });
a1Children.should.contain({ id: 3, name: "a111" });
a1Children.should.contain({ id: 4, name: "a12" });
})));
});

View File

@ -0,0 +1,27 @@
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("nested-set")
export class Category {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@TreeParent({ cascade: true })
parentCategory: Category;
@TreeChildren({ cascade: true })
childCategories: Category[];
// @TreeLevelColumn()
// level: number;
}

View File

@ -0,0 +1,56 @@
import "reflect-metadata";
import {Category} from "./entity/Category";
import {Connection} from "../../../../src/connection/Connection";
import {closeTestingConnections, createTestingConnections, reloadTestingDatabases} from "../../../utils/test-utils";
describe("tree tables > nested-set", () => {
let connections: Connection[];
before(async () => connections = await createTestingConnections({
entities: [Category]
}));
beforeEach(() => reloadTestingDatabases(connections));
after(() => closeTestingConnections(connections));
it("attach should work properly", () => Promise.all(connections.map(async connection => {
const categoryRepository = connection.getTreeRepository(Category);
const a1 = new Category();
a1.name = "a1";
await categoryRepository.save(a1);
const a11 = new Category();
a11.name = "a11";
a11.parentCategory = a1;
await categoryRepository.save(a11);
const a111 = new Category();
a111.name = "a111";
a111.parentCategory = a11;
await categoryRepository.save(a111);
const a12 = new Category();
a12.name = "a12";
a12.parentCategory = a1;
await categoryRepository.save(a12);
const rootCategories = await categoryRepository.findRoots();
rootCategories.should.be.eql([{
id: 1,
name: "a1"
}]);
const a11Parent = await categoryRepository.findAncestors(a11);
a11Parent.length.should.be.equal(2);
a11Parent.should.contain({ id: 1, name: "a1" });
a11Parent.should.contain({ id: 2, name: "a11" });
const a1Children = await categoryRepository.findDescendants(a1);
a1Children.length.should.be.equal(4);
a1Children.should.contain({ id: 1, name: "a1" });
a1Children.should.contain({ id: 2, name: "a11" });
a1Children.should.contain({ id: 3, name: "a111" });
a1Children.should.contain({ id: 4, name: "a12" });
})));
});

View File

@ -1,12 +1,12 @@
import {ClosureEntity} from "../../../../src/decorator/entity/ClosureEntity";
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")
@ClosureEntity()
@Entity("sample22_category")
@Tree("closure-table")
export class Category {
@PrimaryGeneratedColumn()