Relationships - samchon/safe-typeorm GitHub Wiki

Type safe relationship decorators.

Relationship decorators of safe-typeorm is a little bit different with original typeorm (anyorm), and such different makes the unsafe anyorm to be the real typeorm. Let's see how relationship decorators of safe-typeorm is different with a demo ERD (Entity Relationshiop Diagram).

If you want to see detailed code of model classes, click one of below links:

Entity Relationship Diagram

Belongs.ManyToOne

import * as orm from "typeorm";
import safe from "safe-typeorm";

// composite index, specify foreign key column names
@orm.Index(["bbs_group_id", "bbs_category_id", "created_at"])
@orm.Entity()
export class BbsArticle {
    @safe.Belongs.ManyToOne(
        () => BbsGroup, // parent entity
        "uuid", // parent entity's PK type
        "bbs_group_id", // foreign key field name
        // INDEXED
    )
    public readonly group!: safe.Belongs.ManyToOne<BbsGroup, "uuid">;

    @safe.Belongs.ManyToOne(
        () => BbsCategory, // parent entity
        "uuid", // column type
        "bbs_category_id", // column name
        { index: true, nullable: true }, // nullable option with indexing
    )
    public readonly category!: safe.Belongs.ManyToOne<
        BbsCategory,
        "uuid",
        { nullable: true } // nullable type
    >;

    @orm.Index()
    @orm.CreateDateColumn()
    public readonly created_at!: Date;
}

When declaring M: 1 belongs relationship, you should input 4 parameters into Belongs.ManyToOne() decorator function. Also, its property type declaration requires 2 or 3 generic arguments like below:

  • Decorator function
    • target: function returning target ORM entity class
    • type: PK type of target entity
    • name: name of foreign key column
    • options?: options of the FK column
      • index?: whether the column is indexed or not
      • nullable?: whether the column is nullable or not
  • Type declaration
    • Target: target entity type
    • Type: PK column type of target entity

During definition, do not forget to use index on the foreign key column. It is possible to defnining atomic index by options parameter, and also possible to define composite index by @orm.Index() decorator function with foreign key column name what you've defined.

Also, if you want to define a nullable foreign key column, you should use nullable option both in decorator function and type declaration. Otherwise, you are planning to define a foreign key column as NOT NULL option, you don't need to specify the nullable option.

const article: BbsArticle;

// GET PARENT ENTITY
const group: BbsGroup = await article.group.get();
const category: BbsCategory | null = await article.category.get();

// SET PARENT ENTITY
await article.group.set(group);
await article.category.set(category);
await article.category.set(null);

// GET PARENT ID
const group_id: string = article.group.id;
const category_id: string | null = article.category.id;

After declaration, you can access to the parent entity by get() method. The get() is a type of singleton method that loading parent entity record only one time by caching. In contrary, configuring parent entity by set() method is possible, too. Also, you can access to the foreign key column value by id property.

Belongs.OneToOne

import * as orm from "typeorm";
import safe from "safe-typeorm";

@orm.Entity()
export class BbsAnswerArticle extends safe.Model {
    /* -----------------------------------------------------------
        COLUMNS
    ----------------------------------------------------------- */
    @safe.Belongs.OneToOne(
        () => BbsArticle, // target entity
        (article) => article.answer,
            // accessor from parent entity 
            // enable to omit, but recommend to fill for memoization
        "uuid", // column type
        "id", // colum name
        { primary: true }, // enable to defined as PK
    )
    public readonly base!: safe.Belongs.OneToOne<BbsArticle, "uuid">;

    @safe.Belongs.OneToOne(
        () => BbsQuestionArticle,
        (question) => question.answer,
        "uuid",
        "bbs_question_id",
        // be UK automatically
    )
    public readonly question!: safe.Belongs.OneToOne<
        BbsQuestionArticle,
        "uuid"
    >;
}

When defining 1: 1 belongs relationship, you should input 3 to 5 parameters into Belongs.OneToOne() decorator function. Also, its property type declaration requires 2 generic arguments like below:

  • Decorator function
    • target: function returning target ORM entity class
    • reverse?: accessor from parent entity
    • type: PK type of target entity
    • name: name of foreign key column
    • options?: options of the FK column
      • primary?: whether the column is primary key or not
      • unique?: whether the column is unique key or not
  • Type declaration
    • Target: target entity type
    • Type: PK column type of target entity

During definition, it is possible to define a 1: 1 belongs foreign key column to be a primary key. Otherwise, you do not define both primary and unique options, the foreign key column would be defined as a unique key constraint automatically.

For reference, if you load the parent entity and reverse parameter has been defined in the decoratur function, current entity record would be memoized into the accessor of parent entity. Therefore, current entity would not be duplicated loaded when parent entity calls the reverse accessor. It is the reason why I recommend you not to omit the reverse parameter for such memoization.

const answer: BbsAnswerArticle;

// GET PARENT ENTITY
const question: BbsQuestionArticle = await answer.get();
const article: BbsArticle = await answer.base.get();

// SET PARENT ENTITY
await answer.set(question);
await answer.base.set(article);

// GET PARENT ID
const question_id: string = answer.question.id;
const article_id: string = answer.base.id;

After declaration, you can access to the parent entity by get() method. The get() is a type of singleton method that loading parent entity record only one time by caching. In contrary, configuring parent entity by set() method is possible, too. Also, you can access to the foreign key column value by id property.

Has.OneToOne

import * as orm from "typeorm";
import safe from "safe-typeorm";

@orm.Entity()
export class BbsArticle extends safe.Model {
    @safe.Has.OneToOne(
        () => BbsQuestionArticle, // target child entity
        (question) => question.base, // accessor from child entity
        // when omitted, child entity may not exists
    )
    public readonly question!: safe.Has.OneToOne<BbsQuestionArticle>;

    @safe.Has.OneToOne(
        () => __MvBbsArticleLastContent, 
        (material) => material.article,
        true, // ensure that the child entity record always exists
    )
    public readonly __mv_last!: safe.Has.OneToOne<
        __MvBbsArticleLastContent,
        true, // declare in type declaration, too
    >;
}

When declaring 1: 1 has relationship, you should input 2 to 3 parameters into Has.OneToOne() decorator function. Also, its property type requires 1 or 2 generic arguments like below:

  • Decorator function
    • target: function returning target ORM entity class
    • reverse: accessor from child entity
    • ensure: whether the child entity record always exists or not, default is false
  • Type declaration
    • Target: target entity type
    • Ensure: whether the child entity record always exists or not, default is false

During definition, you can define whether the child entity record always exists or not, through ensure parameter. If you define ensure parameter as true, it means that the child entity record always exists. Otherwise, you define it as false or omit, the child entity record may not exist.

const article: BbsArticle;

// GET CHILD ENTITY
const question: BbsQuestionArticle | null = await article.question.get();
const last: __MvBbsArticleLastContent = await article.__mv_last.get();

// SET CHILD ENTITY
await article.question.set(question);
await article.__mv_last.set(last);

After declaration, you can access to the child entity by get() method. The get() is a type of singleton method that loading child entity record only one time by caching. In contrary, configuring child entity by set() method is possible, too.

Has.OneToMany

import * as orm from "typeorm";
import safe from "safe-typeorm";

@orm.Entity()
export class BbsArticle extends safe.Model {
    @safe.Has.OneToMany(
        () => BbsArticleComment, // child entity
        (comment) => comment.article // accessor from child
    )
    public readonly comments!: safe.Has.OneToMany<BbsArticleComment>;

    @safe.Has.OneToMany(
        () => BbsArticleContent, // child entity 
        (content) => content.article, // accessor from child
        (x, y) => x.created_at.getTime() - y.created_at.getTime(),
            // comparator for sorting
    )
    public readonly contents!: safe.Has.OneToMany<BbsArticleContent>;
}

When defining 1: N has relationship, you should input 2 or 3 parameters into Has.OneToMany() decorator function. Also, its property type declaration requires only one generic argument Target like below:

  • Decorator function
    • target: function returning target ORM entity class
    • reverse: accessor from child entity
    • comparator?: comparator function for sorting
  • Type declaration
    • Target: target entity type
const article: BbsArticle;

// GET CHILDREN ENTITIES
const comments: BbsArticleComment[] = await article.comments.get();
const contents: BbsArticleContent[] = await article.contents.get();

// SET CHILDREN ENTITIES
await article.comments.set(comments);
await article.contents.set(contents);

After declaration, you can access to the children records by get() method. The get() is a type of singleton method that loading children records only one time by caching. In contrary, configuring children records by set() method is possible, too.

For reference, above contents would be sorted by creation time when loading by get() method, because it has a comparator function. However, such sorting would not be automatically worked when you configure children records by set() method.

Has.ManyToMany

import * as orm from "typeorm";
import safe from "safe-typeorm";

export class BbsArticleContent extends safe.Model {
    @safe.Has.ManyToMany(
        () => AttachmentFile, // target entity
        () => BbsArticleContentFile, // router entity
        (router) => router.file, // accessor from router to target
        (router) => router.content, // accessor from router to currnet
        (x, y) => x.router.sequence - y.router.sequence,
            // comparator for sorting
    )
    public readonly files!: safe.Has.ManyToMany<
        AttachmentFile, 
        BbsArticleContentFile
    >;
}

@orm.Unique(["bbs_article_content_id", "attachment_file_id"])
@orm.Entity()
export class BbsArticleContentFile extends safe.Model {
    /* -----------------------------------------------------------
        COLUMNS
    ----------------------------------------------------------- */
    @orm.PrimaryGeneratedColumn("uuid")
    public readonly id!: string;

    @safe.Belongs.ManyToOne(
        () => BbsArticleContent,
        "uuid",
        "bbs_article_content_id",
        // INDEXED
    )
    public readonly content!: safe.Belongs.ManyToOne<BbsArticleContent, "uuid">;

    @safe.Belongs.ManyToOne(
        () => AttachmentFile,
        "uuid",
        "attachment_file_id",
        { index: true },
    )
    public readonly file!: safe.Belongs.ManyToOne<AttachmentFile, "uuid">;

    @orm.Column("int")
    public readonly sequence!: number;
}

When defining M: N has relationship, you have to declare router entity class like above BbsArticleContentFile. After that, you should input 4 or 5 parameters into Has.ManyToMany() decorator function. Also, its property type declaration requires 2 generic arguments like below:

  • Decorator functions
    • target: function returning target ORM entity class
    • router: function returning router ORM entity class
    • targetReverse: accessor from router entity to target entity
    • myReverse: accessor from router entity to current entity
    • comparator?: comparator function for sorting
  • Type declaration
    • Target: target entity type
    • Router: router entity type
const content: BbsArticleContent;

// GET CHILDREN ENTITIES
const files: AttachmentFile[] = await content.files.get();

// SET CHILDREN ENTITIES
await content.files.set(files);

After declaration, you can access to the children records by get() method. The get() is a type of singleton method that loading children records only one time by caching. In contrary, configuring children records by set() method is possible, too.

For reference, above files would be sorted by creation time when loading by get() method, because it has a comparator function. However, such sorting would not be automatically worked when you configure children records by set() method.