PrismaのJOINの挙動を観察

# 注意

結論として、[Relation queries](https://www.prisma.io/docs/concepts/components/prisma-client/relation-queries)で示される関連テーブルのまとめて取得の方法では、JOIN句が生成されずクエリが2つに分けられます。
クエリを1つにまとめる方法は私の力不足で解決できませんでしたので書いておりません。
ご了承の上お読みください。

# はじめに

[Prisma](https://www.prisma.io/)で `inner join` と `left outer join` を行いたいと思いました。

# 環境

[ts-node](https://www.npmjs.com/package/ts-node)を用いて動作させます。
DBは[MySQL](https://www.mysql.com/jp/)を用います。


# prisma schema

以下条件で3つテーブルを作成します

- userテーブルが存在
- user_configテーブルはuserテーブルに対して1対1に紐づく
- postテーブルはuserテーブルに対して1対多で紐づく

```
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

model User {
  id         Int         @id @default(autoincrement())
  createdAt  DateTime    @default(now())
  updateAt   DateTime    @updatedAt
  name       String
  Post       Post[]
  UserConfig UserConfig?
}

model UserConfig {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updateAt  DateTime @updatedAt
  userId    Int      @unique
  user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  category  String   @db.VarChar(2)
}

model Post {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  userId    Int
  user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  message   String   @db.Text
}
```

# [Relation queries](https://www.prisma.io/docs/concepts/components/prisma-client/relation-queries)に沿ってinner joinを試す

userとuser_configをinner joinさせます。
理想的は下記クエリです。

```sql
select
    user.id,
    user.name,
    uc.category
from
    user
    inner join user_config uc
        on uc.user_id = user.id
```

コードを書いてみます。
出力されるクエリもほしいので、 `log: ['query']` オプションをつけます。

```ts
import { PrismaClient } from '@prisma/client';

(async () => {
  const prisma = new PrismaClient({
    log: ['query'],
  });
  const params = await prisma.user.findMany({
    select: {
      id: true,
      name: true,
      UserConfig: {
        select: {
          category: true
        }
      }
    }
  });
  console.log(params);
})();
```

出力結果を見てみます。

```
[
  { id: 1, name: 'red', UserConfig: { category: '01' } },
  { id: 2, name: 'white', UserConfig: { category: '02' } },
  { id: 3, name: 'blue', UserConfig: { category: '03' } },
  { id: 4, name: 'green', UserConfig: { category: '04' } }
]
```

できてそう。
じゃあ次は出力されたクエリを見てみましょう。(冗長な部分を削ってわかりやすく整形します)

```sql
-- prisma:query
SELECT
    id,
    name
FROM
    User;

-- prisma:query
SELECT 
    id,
    category
    userId
FROM
    UserConfig
WHERE
    userId IN (1, 2, 3, 4);
```

ええ……クエリが2つ走っているんですが……

# クエリをまとめたい

調べていたらこんな記事を発見しました。

- [Prismaが発行するSQLを観察してみる（参照系）](https://zenn.dev/gibjapan/articles/5422d2fcf1c46e)

joinがクエリにくっついているケースを見るに、joinされるテーブルのカラムが条件が指定されていればinner joinとして扱われていそう。
やってみます。

```ts
import { PrismaClient } from '@prisma/client';

(async () => {
  const prisma = new PrismaClient({
    log: ['query'],
  });
  const params = await prisma.user.findMany({
    select: {
      id: true,
      name: true,
      UserConfig: {
        select: {
          category: true
        }
      }
    },
    where: {
      UserConfig: {
        category: {
          in: ['01', '02']
        }
      }
    }
  });
  console.log(params);
})();
```

取得結果は想定通り。

```
[
  { id: 1, name: 'red', UserConfig: { category: '01' } },
  { id: 2, name: 'white', UserConfig: { category: '02' } }
]
```

クエリを見てみます。

```sql
-- prisma:query
SELECT
    User.id,
    User.name
FROM
    User
WHERE
    User.id IN (
        SELECT
            User.id
        FROM
            User
            INNER JOIN
                UserConfig
                    ON UserConfig.userId = User.id
        WHERE (
            UserConfig.category IN ('01', '02') AND
            User.id IS NOT NULL
        )
    )

-- prisma:query
SELECT 
    UserConfig.id,
    UserConfig.category,
    UserConfig.userId
FROM
    UserConfig
WHERE
    UserConfig.userId IN ('01', '02')
```

userテーブルの絞り込みのためにjoinは使われているけど、selectでuser_configを取得するには結局クエリ分けなきゃいけないのね。とほほ。

つまりは取得はクエリを分けて、条件にはjoinが使われるというわけですか。
それならouter joinであっても、N+1問題は起きずに済みそうですね。
[prisma/prisma#5745](https://github.com/prisma/prisma/discussions/5745)でもN+1問題は回避していると述べられています。

# outer join

一応確認だけしてみます。

```ts
import { PrismaClient } from '@prisma/client';

(async () => {
  const prisma = new PrismaClient({
    log: ['query'],
  });
  const params = await prisma.user.findMany({
    select: {
      id: true,
      name: true,
      Post: {
        select: {
          message: true
        }
      }
    },
    where: {
      UserConfig: {
        category: '01'
      }
    }
  });
  console.log(JSON.stringify(params, null , '  '));
})();
```

出力結果はよさげ。

```
[
  {
    id: 1,
    name: 'red',
    Post: [
      {
        message: '01-1'
      },
      {
        message: '01-2'
      },
      {
        message: '01-3'
      }
    ]
  }
]
```

1対多なのでleft outer joinで取りたくなる条件を書いてみたのですが、やはりクエリ2つで取ってきていました。そしてクエリが分割されているのでouter joinですらありませんでした。

```sql
-- prisma:query
SELECT
    User.id,
    User.name
FROM
    User
WHERE
    User.id IN (
        SELECT
            User.id
        FROM
            User
            INNER JOIN UserConfig ON UserConfig.userId = User.id
        WHERE (
            UserConfig.category IN ('01') AND
            User.id IS NOT NULL
        )

-- prisma:query
SELECT
    Post.id,
    Post.message,
    Post.userId
FROM
    Post
WHERE
    Post.userId IN (1)
```

私の力不足で、そもそもouter joinをどうやって発生させるかが思いつかないのでここで終わります。

[Are LEFT OUTER JOIN's Possible?? 「left outer joinは可能ですか？」](https://github.com/prisma/prisma/discussions/5745)

こんなタイトルのディスカッションが生まれる一端をみました。

# おわりに

prismaでのjoinの生成されるクエリと挙動を確認しました。
現状、想定通りのクエリを発行してもらうには[raw query](https://www.prisma.io/docs/concepts/components/prisma-client/raw-database-access)しかなさそうですね。
結局、本記事では何も解決していません。

issueを見ていると

- [Optimize number of nested Prisma Client requests](https://github.com/prisma/prisma/issues/4997)

なんてのもあるので、もしかしたら今後はORMで書いた内容が一つのクエリで出力されるかもしれませんね。

今後に期待です。終わり。