What are many-to-many relations
Many-to-many is a relation where A contains multiple instances of B, and B contain multiple instances of A. Let's take for example Question
and Category
entities. Question can have multiple categories, and each category can have multiple questions.
Copy import {Entity , PrimaryGeneratedColumn , Column} from "typeorm" ;
@ Entity ()
export class Category {
@ PrimaryGeneratedColumn ()
id : number ;
@ Column ()
name : string ;
}
Copy import {Entity , PrimaryGeneratedColumn , Column , ManyToMany , JoinTable} from "typeorm" ;
import {Category} from "./Category" ;
@ Entity ()
export class Question {
@ PrimaryGeneratedColumn ()
id : number ;
@ Column ()
title : string ;
@ Column ()
text : string ;
@ ManyToMany (() => Category)
@ JoinTable ()
categories : Category [];
}
@JoinTable()
is required for @ManyToMany
relations. You must put @JoinTable
on one (owning) side of relation.
This example will produce following tables:
Copy +-------------+--------------+----------------------------+
| category |
+-------------+--------------+----------------------------+
| id | int(11) | PRIMARY KEY AUTO_INCREMENT |
| name | varchar(255) | |
+-------------+--------------+----------------------------+
+-------------+--------------+----------------------------+
| question |
+-------------+--------------+----------------------------+
| id | int(11) | PRIMARY KEY AUTO_INCREMENT |
| title | varchar(255) | |
| text | varchar(255) | |
+-------------+--------------+----------------------------+
+-------------+--------------+----------------------------+
| question_categories_category |
+-------------+--------------+----------------------------+
| questionId | int(11) | PRIMARY KEY FOREIGN KEY |
| categoryId | int(11) | PRIMARY KEY FOREIGN KEY |
+-------------+--------------+----------------------------+
Saving many-to-many relations
With cascades enabled you can save this relation with only one save
call.
Copy const category1 = new Category ();
category1 .name = "animals" ;
await connection . manager .save (category1);
const category2 = new Category ();
category2 .name = "zoo" ;
await connection . manager .save (category2);
const question = new Question ();
question .title = "dogs" ;
question .text = "who let the dogs out?" ;
question .categories = [category1 , category2];
await connection . manager .save (question);
Deleting many-to-many relations
With cascades enabled you can delete this relation with only one save
call.
To delete a many-to-many relationship between two records, remove it from the corresponding field and save the record.
Copy const question = getRepository (Question);
question .categories = question . categories .filter (category => {
category .id !== categoryToRemove .id
})
await connection . manager .save (question)
This will only remove the record in the join table. The question
and categoryToRemove
records will still exist.
Soft Deleting a relationship with cascade
This example show what the cascading soft deletes behaves
Copy const category1 = new Category ();
category1 .name = "animals" ;
const category2 = new Category ();
category2 .name = "zoo" ;
const question = new Question ();
question .categories = [category1 , category2];
const newQuestion = await connection . manager .save (question);
await connection . manager .softRemove (newQuestion);
As you can see in this example we did not call save or softRemove for category1 and category2. But They will be automatically saved and soft-deleted when the cascade of relation options is set to true like this:
Copy import {Entity , PrimaryGeneratedColumn , Column , ManyToMany , JoinTable} from "typeorm" ;
import {Category} from "./Category" ;
@ Entity ()
export class Question {
@ PrimaryGeneratedColumn ()
id : number ;
@ ManyToMany (() => Category , category => category .questions , {
cascade : true
})
@ JoinTable ()
categories : Category [];
}
Loading many-to-many relations
To load question with categories inside you must specify relation in FindOptions
:
Copy const questionRepository = connection .getRepository (Question);
const questions = await questionRepository .find ({ relations : [ "categories" ] });
Or using QueryBuilder
you can join them:
Copy const questions = await connection
.getRepository (Question)
.createQueryBuilder ( "question" )
.leftJoinAndSelect ( "question.categories" , "category" )
.getMany ();
When using FindOptions
you don't need to specify eager relations - they are always automatically loaded.
bi-directional relations
Relations can be uni-directional and bi-directional. Uni-directional are relations with a relation decorator only on one side. Bi-directional are relations with decorators on both sides of a relation.
We just created a uni-directional relation. Let's make it bi-directional:
Copy import {Entity , PrimaryGeneratedColumn , Column , ManyToMany} from "typeorm" ;
import {Question} from "./Question" ;
@ Entity ()
export class Category {
@ PrimaryGeneratedColumn ()
id : number ;
@ Column ()
name : string ;
@ ManyToMany (() => Question , question => question .categories)
questions : Question [];
}
Copy import {Entity , PrimaryGeneratedColumn , Column , ManyToMany , JoinTable} from "typeorm" ;
import {Category} from "./Category" ;
@ Entity ()
export class Question {
@ PrimaryGeneratedColumn ()
id : number ;
@ Column ()
title : string ;
@ Column ()
text : string ;
@ ManyToMany (() => Category , category => category .questions)
@ JoinTable ()
categories : Category [];
}
We just made our relation bi-directional. Note, the inverse relation does not have a @JoinTable
. @JoinTable
must be only on one side of the relation.
Bi-directional relations allow you to join relations from both sides using QueryBuilder
:
Copy const categoriesWithQuestions = await connection
.getRepository (Category)
.createQueryBuilder ( "category" )
.leftJoinAndSelect ( "category.questions" , "question" )
.getMany ();
many-to-many relations with custom properties
In case you need to have additional properties to your many-to-many relationship you have to create a new entity yourself. For example if you would like entities Post
and Category
to have a many-to-many relationship with additional order
column, you need to create entity PostToCategory
with two ManyToOne
relations pointing in both directions and custom columns in it:
Copy import { Entity , Column , ManyToOne , PrimaryGeneratedColumn } from "typeorm" ;
import { Post } from "./post" ;
import { Category } from "./category" ;
@ Entity ()
export class PostToCategory {
@ PrimaryGeneratedColumn ()
public postToCategoryId !: number ;
@ Column ()
public postId !: number ;
@ Column ()
public categoryId !: number ;
@ Column ()
public order !: number ;
@ ManyToOne (() => Post , post => post .postToCategories)
public post !: Post ;
@ ManyToOne (() => Category , category => category .postToCategories)
public category !: Category ;
}
Additionally you will have to add a relationship like the following to Post
and Category
:
Copy // category.ts
...
@ OneToMany (() => PostToCategory , postToCategory => postToCategory .category)
public postToCategories ! : PostToCategory[];
// post.ts
...
@ OneToMany (() => PostToCategory , postToCategory => postToCategory .post)
public postToCategories ! : PostToCategory[];