【Supabase】多対多のテーブルで中間テーブルの記述を省いてデータを取得すると400エラーになる（Searched for a foreign key relationship between 'users' and 'skills' in the schema 'public', but no matches were found.）

# はじめに

お疲れ様です。

少しハマってしまったので備忘録も兼ねて記事に残しておこうと思います。

# 問題

Supabaseのドキュメントにて、多対多のテーブルの場合、以下のように記載することでリレーション先の情報も合わせて取得可能という記載がありました。

https://supabase.com/docs/guides/database/joins-and-nesting?queryGroups=language&language=js&queryGroups=output-format&output-format=table#many-to-many-joins

> ```react
> const { data, error } = await supabase.from('teams').select(`
>   id, 
>   team_name, 
>   users ( id, name )
>`)
>```

データ取得時に上記の方法を試してみたのですが、後述するエラーが発生してしまい、うまくいきませんでした。

## 設定内容

ユーザーが複数の技術を選択できる状態にするため、以下の3つのテーブルを用意していました。
- `users`
- `skills`
- `user_skill`

<details><summary>設定内容</summary>

- `users`テーブル
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3911933/9f8043cd-bdbd-f858-cf3a-f353f0034125.png)


- `skills`テーブル
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3911933/1041097c-d0cc-4be8-0508-f62c206b550c.png)

- `user_skill`テーブル
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3911933/2cdc5db8-8d01-8166-076e-749e74972672.png)

</details>

データ取得用のソースコードは以下の通りです。
```typescript:src/utils/supabaseFunctions.ts
import { supabase } from './supabase';
import { User } from '@/domain/user';

export const fetchUser = async (id: string): Promise<User> => {
  const { data, error } = await supabase.from('users').select('*,skills(id,name)').eq('user_id', id).limit(1).single();

  if (error) throw new Error(error.message);

  const userData = User.createUser(
    data.user_id,
    data.name,
    data.description,
    data.user_skill,
    data.github_id,
    data.qiita_id,
    data.x_id,
    data.created_at
  );

  return userData;
};
```
## コンソールのエラー内容

ブラウザ上には以下のようなコンソールエラーが表示されました。

```text:コンソールエラー
GET
https://{Supabaseアカウント}.supabase.co/rest/v1/users?select=*%2Cskills%28name%29&user_id=eq.sample-id3&limit=1
400 (Bad Request)
```

## レスポンスのエラー内容

試しに該当のソースコードのレスポンスをコンソールで確認してみたところ、以下の内容が`error`に返却されていました。

```diff_typescript:src/utils/supabaseFunctions.ts
import { supabase } from './supabase';
import { User } from '@/domain/user';

export const fetchUser = async (id: string): Promise<User> => {
  const { data, error } = await supabase.from('users').select('*,skills(name)').eq('user_id', id).limit(1).single();

+ console.log(data);
+ console.log(error);

  if (error) throw new Error(error.message);

  const userData = User.createUser(
    data.user_id,
    data.name,
    data.description,
    data.user_skill,
    data.github_id,
    data.qiita_id,
    data.x_id,
    data.created_at
  );

  return userData;
};
```

```json:レスポンス内容（error）
{
    "code": "PGRST200",
    "details": "Searched for a foreign key relationship between 'users' and 'skills' in the schema 'public', but no matches were found.",
    "hint": "Perhaps you meant 'user_skill' instead of 'skills'.",
    "message": "Could not find a relationship between 'users' and 'skills' in the schema cache"
}
```

# 解決方法

Supabaseのテーブル設定で、中間テーブルのリレーション用のカラム（今回の場合は`user_skill`テーブルの、`user_id`と`skill_id`）をプライマリーキーに指定する必要がありました。

- `user_skill`テーブル
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3911933/9d711fc2-257d-504d-5938-9e7ff9d9aa52.png)

```json:レスポンス内容（data）
{
    "user_id": "sample-id3",
    "name": "テスト三郎",
    "description": "<h3>テスト三郎の自己紹介</h3>",
    "github_id": "",
    "qiita_id": "ritsu21ctws3",
    "x_id": "ritsu21ctws3",
    "created_at": "2025-01-03T03:31:32.036016+00:00",
    "skills": [
        {
            "id": 1,
            "name": "React"
        },
        {
            "id": 2,
            "name": "TypeScript"
        },
        {
            "id": 3,
            "name": "GitHub"
        }
    ]
}
```


# おわりに

まとめると、Supabaseで多対多のテーブルのリレーション先も合わせて（省略記法で）取得するには、中間テーブルのリレーション用のカラムに対して、以下の設定が必要になるというお話でした。
- プライマリーキーの設定
- 外部キーの設定

公式ドキュメントの以下の例にちゃんと書かれていたのですが、完全に見逃していました。。

https://supabase.com/docs/guides/database/joins-and-nesting?queryGroups=language&language=js&queryGroups=output-format&output-format=table#many-to-many-joins

> ```sql
> create table members (
>   "user_id" int references users,
>   "team_id" int references teams,
>   primary key (user_id, team_id)
>);
> ```

## 補足

補足ですが、「問題」セクションの状態のままでも、`select`句で中間テーブル名を記述した場合はデータ取得が成功しました。

```typescript:src/utils/supabaseFunctions.ts
import { supabase } from './supabase';
import { User } from '@/domain/user';

export const fetchUser = async (id: string): Promise<User> => {
  const { data, error } = await supabase.from('users').select('*,user_skill(skills(id,name))').eq('user_id', id).limit(1).single();

  if (error) throw new Error(error.message);

  const userData = User.createUser(
    data.user_id,
    data.name,
    data.description,
    data.user_skill,
    data.github_id,
    data.qiita_id,
    data.x_id,
    data.created_at
  );

  return userData;
};
```

```json:レスポンス内容（data）
{
    "user_id": "sample-id3",
    "name": "テスト三郎",
    "description": "<h3>テスト三郎の自己紹介</h3>",
    "github_id": "",
    "qiita_id": "ritsu21ctws3",
    "x_id": "ritsu21ctws3",
    "created_at": "2025-01-03T03:31:32.036016+00:00",
    "user_skill": [
        {
            "skills": {
                "id": 1,
                "name": "React"
            }
        },
        {
            "skills": {
                "id": 2,
                "name": "TypeScript"
            }
        },
        {
            "skills": {
                "id": 3,
                "name": "GitHub"
            }
        }
    ]
}
```

ただ、中間テーブルを省略した記法の方がレスポンス内容はわかりやすくなるため、今後は中間テーブルを省略した記述方法でデータ取得を行いたいと思います。

# 参考

https://supabase.com/docs/guides/database/joins-and-nesting