---
# System prepended metadata

title: Prisma Model and Relation

---

本文會介紹 prisma 這個非常好用的 ORM，比起 TypeORM 的語法，prisma 可謂是非常簡單易懂，程式碼的可讀性也相當之高。以下將會介紹 prisma model 的寫法和不同 relation 的 CRUD。
## One To Many
情境如下:
在遊戲攻略平台中，發文前需要選定一個遊戲版，而遊戲版需要由版主來創建，每個貼文需要歸屬於一個遊戲版，一個遊戲版可以有多個文章，因此relation為一對多

1. 版主創建遊戲版
2. 選擇某個遊戲版發文
#### Game model (parent)
```typescript=
model Game {
  id Int @default(autoincrement()) @id
  name String
  posts Post[]
}
```
#### Post model (children)
```typescript=
model Post {
  id Int @default(autoincrement()) @id
  name String
  body String
  game Game @relation(fields: [gameId], references: [id])
  gameId Int
}
```
#### Create game
版主創建遊戲版
```typescript=
async createGame(data: CreateGameDto) {
    const game = await this.prismaService.game.create({
      data
    })
    return game
}
```
#### Create Post
選擇某個遊戲版發文
```typescript=
async createPost(data: CreatePostDto) {
    const result = await this.prisma.post.create({
      data });
    return result;
  }
```

#### CreatePostDto
從 DTO 中可以看到我們是通過 post model 中的 fields 去進行關聯，且我們定義 fiels 為 gameId，因此在create 和 update 時透過 gameId 這個屬性進行關聯操作即可。
authorId 可以從 jwt token中解析
```typescript=
class CreatePostDto {
  name: string;

  body: string;

  authorId: number;
  
  gameId: number;
}
```
#### Get Post
```typescript=
async getPosts() {
    const result = await this.prisma.post.findMany({
      include: {
        game: {
          select: {
            name: true
          }
        }
      },
}
```
#### Update Post
```typescript=
async updatePostById(id: number, data: UpdatePostDto) {
    const result = await this.prisma.post.update({
      where: { id },
      data: {
        ...data,
      },
    });
    return result;
  }
```

## Many To Many
情境如下:
在遊戲攻略平台中，使用者可以發布文章，並且可以對文章加註tag，如 movie、foods 等等。一個 post 可以有多個 tag ，一個 tag 也可以在多個文章中出現，因此 relation 為多對多。在 prisma 中有兩種設計模式，一種是在兩個 table 中再使用一個多對多 table 作為中介將兩者關聯起來，另外也可以在 model 中直接用 [ ] 代表兩者為多對多關係，寫起來甚至比一對多更簡潔。
1. 創建文章時加上兩個tag
1. 透過 tag 或 name 來查詢文章
2. 其他使用者想要為文章新增 tag
3. 後來後悔了想要刪掉其中一個 tag
#### Post
```typescript=
model Post {
  id Int @default(autoincrement()) @id
  name String
  body String
  tags Tag[]
}
```
#### Tag
```typescript=
model Tag {
  id Int @default(autoincrement()) @id
  name String @unique
  posts Post[]
  Games Game[]
}
```
#### Create Post
創建文章時就指定 tag
透過以下的寫法便可以將data中的tag和post給關聯起來，connectOrCreate在tag不存在時可以創建tag並建立關聯，存在時則直接關聯，因為結構比較複雜，所以在create之前先將data 中的tag array給整理成規定的格式。
```typescript=
async createPost(data: CreatePostDto) {
    const result = await this.prisma.post.create({
      data: {
        ...data,
        tags: {
          connectOrCreate: data.tags.map((tag) => {
              return {
                    where: { name: tag },
                    create: { name: tag }
              };
    })
        },
      },
    });
    return result;
  }
```
這些程式在背後執行的SQL長這樣:
重要ㄟ，next generation orm，prisma會幫忙檢查tag是否存在
```sql=
// 輸入的tag存在時
BEGIN
INSERT INTO "public"."Post" ("name","status","body","slug","gameId","authorId","views") VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING "public"."Post"."id"
SELECT "public"."Tag"."id" FROM "public"."Tag" WHERE ("public"."Tag"."name" = $1 AND 1=1) OFFSET $2
INSERT INTO "public"."_PostToTag" ("A","B") VALUES ($1,$2) ON CONFLICT DO NOTHING
SELECT "public"."Post"."id", "public"."Post"."name", "public"."Post"."status", "public"."Post"."body", "public"."Post"."slug", "public"."Post"."gameId", "public"."Post"."authorId", "public"."Post"."views" FROM "public"."Post" WHERE "public"."Post"."id" = $1 LIMIT $2 OFFSET $3
COMMIT
```
```sql=
// 輸入的tag不存在時
BEGIN
INSERT INTO "public"."Post" ("name","status","body","slug","gameId","authorId","views") VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING "public"."Post"."id"
SELECT "public"."Tag"."id" FROM "public"."Tag" WHERE ("public"."Tag"."name" = $1 AND 1=1) OFFSET $2
INSERT INTO "public"."Tag" ("name") VALUES ($1) RETURNING "public"."Tag"."id"
INSERT INTO "public"."_PostToTag" ("A","B") VALUES ($1,$2) ON CONFLICT DO NOTHING
SELECT "public"."Post"."id", "public"."Post"."name", "public"."Post"."status", "public"."Post"."body", "public"."Post"."slug", "public"."Post"."gameId", "public"."Post"."authorId", "public"."Post"."views" FROM "public"."Post" WHERE "public"."Post"."id" = $1 LIMIT $2 OFFSET $3
COMMIT
```
#### Get posts
透過 tag 或 name 來查詢文章
1. post `name` like postName，
2. tag 為 whereIn [ 'tag1' , 'tag2']

透過這邊的例子就可以看出 prisma 能使用相對SQL及TypeORM來說，可讀性較高的語法來完成複雜的查詢。在撰寫語句時也可以直接查看source code的 input type 來撰寫。
```typescript=
async getPosts(tags: Array<string>, postName) {
    const result = await this.prisma.post.findMany({
      include: {
        tags: true,
      },
      where: {
        name:{
          contains: postName // LIKE %postName%
        },
        tags: {
          some: {    // orWhere  andWhere  
            name: {
              in: tags, // whereIn 
            },
          },
        },
      },
    });
    }
    return result;
 }
```
raw sql
```sql=
SELECT "public"."Post"."id", "public"."Post"."name", "public"."Post"."status", "public"."Post"."body", "public"."Post"."slug", "public"."Post"."gameId", "public"."Post"."authorId", "public"."Post"."views" FROM "public"."Post" WHERE ("public"."Post"."name"::text LIKE $1 AND ("public"."Post"."id") IN (SELECT "t0"."A" FROM "public"."_PostToTag" AS "t0" INNER JOIN "public"."Tag" AS "j0" ON ("j0"."id") = ("t0"."B") WHERE ("j0"."name" IN ($2) AND "t0"."A" IS NOT NULL))) OFFSET $3
SELECT "public"."_PostToTag"."A", "public"."_PostToTag"."B" FROM "public"."_PostToTag" WHERE "public"."_PostToTag"."A" IN ($1)
SELECT "public"."Tag"."id", "public"."Tag"."name" FROM "public"."Tag" WHERE "public"."Tag"."id" IN ($1,$2) OFFSET $3
SELECT "public"."Game"."id", "public"."Game"."name" FROM "public"."Game" WHERE "public"."Game"."id" IN ($1) OFFSET $2
```
#### Add tags
其他使用者想要為文章新增 tag
其實跟create post大同小異，也是用createOrCreate來和tag進行關聯。
```typescript=
async addTagToPost(id: number, tags) {
    const tagsData = tags.map((tag) => {
      return {
        where: { name: tag },
        create: { name: tag }
      };
    });

    const result = await this.prisma.post.update({
      where: { id },
      data: {
        tags: {
          connectOrCreate: tagsData,
        },
      },
    });

    return result;
  }
```
raw sql
```sql=
BEGIN
SELECT "public"."Post"."id" FROM "public"."Post" WHERE ("public"."Post"."id" = $1 AND 1=1)
SELECT "public"."Tag"."id" FROM "public"."Tag" WHERE ("public"."Tag"."name" = $1 AND 1=1) OFFSET $2
INSERT INTO "public"."Tag" ("name") VALUES ($1) RETURNING "public"."Tag"."id"
INSERT INTO "public"."_PostToTag" ("A","B") VALUES ($1,$2) ON CONFLICT DO NOTHING
SELECT "public"."Post"."id", "public"."Post"."name", "public"."Post"."status", "public"."Post"."body", "public"."Post"."slug", "public"."Post"."gameId", "public"."Post"."authorId", "public"."Post"."views" FROM "public"."Post" WHERE "public"."Post"."id" = $1 LIMIT $2 OFFSET $3
COMMIT
```
#### Update
後悔了想要刪掉其中一個tag，這邊採用的做法是直接把tag刪光再重新加回去
```typescript=
async updatePostById(id: number, data: UpdatePostDto) {
    const tagsData = data.tags.map((tag) => {
      return {
        where: { name: tag },
        create: { name: tag }
      };
    });
    const result = await this.prisma.post.update({
      where: { id },
      data: {
        ...data,
        tags: {
          disconnect: { name: ... },
          connectOrCreate: tagsData,
        },
      },
    });
    return result;
  }
                                                 
```
使用prisma語法時也可以直接查看input type來查找適合的語法，prisma的可讀性很高，因此透過這個方式幾乎不用去查文檔。

![](https://hackmd.io/_uploads/rJRvVDy_2.png)
    