PrismaはfindManyやfindFirstはWhere句のクエリがundefinedの場合条件なしで検索する仕様なので注意

### 2024/10/17 更新

こちらの仕様は Prisma 5.20.0 以上からフラグが設定可能となり、仕様を変えることができるようになった。このおかげでupdateやdeleteは指定されていないデータを更新・削除するという挙動はなくなった。ただしfindFirst/findManyの挙動は変わらずなので注意が必要。


> Prior to Prisma ORM 5.20.0, undefined was treated as a special value that would not be included in the generated query. This behavior could lead to unexpected results and data loss. If you are using an older version of Prisma ORM, we strongly recommend updating to 5.20.0 or later to take advantage of the new strictUndefinedChecks feature.
> 
> For documentation on the legacy behavior, see Legacy behavior.

https://www.prisma.io/docs/orm/prisma-client/special-fields-and-types/null-and-undefined

https://www.prisma.io/docs/orm/prisma-client/special-fields-and-types/null-and-undefined#undefined-1


従来通りの挙動は以下。

```
// Before: This would delete all users
prisma.user.deleteMany({
  where: {
    id: undefined
  }
})
```


---

最近バックエンドをTypeScript+Express+Prismaを使って書いているのだが、Prismaの仕様で気になるものに遭遇したので回避策含めて記述する。

## PrismaのfindManyとActiveRecordのwhereとの比較

RubyのActiveRecordを使って以下のようなクエリを書くことがあると思う。

例）登録ユーザーを名前で検索するクエリ
```ruby
name = prams[:name] # ←パラメーターを指定していない場合 nil
User.where(user_name: name)
```

上記のクエリはパラメーターが指定されていない場合は __「user_nameがnull(nil)のデータをUsersテーブルから検索する」__ という結果となる。

同じことをTypeScript + Prismaで書いてみる。

```TypeScript
const userName = params.name // undefined または null

const result = prisma.users.findMany({
  where: {
    userName
  }
});
```

実際にこのクエリを実行すると条件によって処理が分かれる。

prams.nameがnullの場合、ActiveRecordと同じく __「user_nameがnull(nil)のデータをUsersテーブルから検索する」__ となる。
しかし、params.nameがundefinedの場合、__nameを指定せずUsersテーブルから検索__ という結果となる。

つまり、__nameがundefinedとなるような実装をしてしまうと、全データが返ってきてしまうという鬼畜仕様になっている。__
一般的なデータ参照ならともかく、たとえば毎回ユーザーの所属識別用のIDや、論理削除フラグなど何かしらの条件をWhere句に入れている場合、ユーザーに見えてほしくないデータも返ってきてしまう。

Webサービスを実装するうえではコードレビューとかで気をつければよいしある程度ユースケースが決まっているのでWhere句の指定を忘れるということはほぼないはずだが、ユーザー公開用のAPIを作成するとか、上記の例のように任意項目での自由検索を作るという場合に問題となりやすい。（入力されるであろうクエリの条件や入力値を事前にパターン網羅することが難しいため）

## 回避策

Ruby（ActiveRecord）慣れしたエンジニアにとって、どう考えてもいつか事故る設計なのでこの仕様の回避策を考えたいところである。
さすがにこの仕様についてはすでにPrismaのGithub Issueで取り上げられている。

https://github.com/prisma/prisma/issues/5149

Prisma公式ドキュメントにもちゃんと記載がある。

https://www.prisma.io/docs/orm/prisma-client/special-fields-and-types/null-and-undefined

しかし、問題は会議室ではなく現場で起きるのが世の常である。
現場（開発）での回避策については上記のGithub Issueにコメントのある仕組みを使うとよい。

```TypeScript
import { PrismaClient } from '.prisma/client';

const prisma = new PrismaClient();
prisma.$use(async (params, next) => {
  if (hasUndefinedValue(params.args?.where))
    throw new Error(
      `Invalid where: ${JSON.stringify(params.args.where)}`
    );
  return await next(params);
});

export default prisma;

function hasUndefinedValue<T>(obj: T): boolean {
  if (typeof obj !== 'object' || obj === null) return false;
  for (const key in obj) {
    if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
    const value = obj[key];
    if (value === undefined) return true;
    if (typeof value === 'object' && !Array.isArray(value))
      if (hasUndefinedValue(value)) return true;
  }
  return false;
}
```

しかし、Prismaの最近のバージョンだと `$use` は非推奨メソッドとなっているためこれを `$extends` で書き換える必要がある。

```TypeScript
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient().$extends({
    query: {
      async $allOperations({ model, operation, args, query }){
        if (hasUndefinedValue(args?.where)){
          throw new Error(
            `Invalid where: ${JSON.stringify(args.where)}`
          );
        }
        return await query(args);
      }
    }
  }) as PrismaClient;

// 以下は参考の実装と同様hasUndefinedValueを定義する
// function hasUndefinedValue<T>(obj: T): xxx
```

`$allOperations` はすべてのPrismaのクエリに対して実行されるので、今回のように `findMany`や`findFirst`だけでよいという場合はさらにif文で分岐させる。

```TypeScript
  async $allOperations({ model, operation, args, query }){
    if (operation === 'findFirst' || operation === 'findMany') {
      if (hasUndefinedValue(args?.where)){ 
      　xxx
      }
    }
    return await query(args);
  }
  ...
```

as PrismaClientを使っているのは、別のクラスやライブラリがPrismaのこのクライアントを呼び出すときに型エラーとなるのを防ぐためである。
より良い書き方があれば是非ご教示いただきたい。

### 他の回避策

ActiveRecordでもnullの場合はそもそも検索条件に入れたくないパターンもあるだろう。
そういうときは以下のように記述できる。

```ruby
# 検索条件として考えられるparamsから必要なキーのみを抽出
search_conditions = params.slice(:user_name, :company_name, :company_role)

# nilの値を持つキーを削除
filtered_conditions = search_conditions.compact

# 残った条件で検索
@users = User.where(filtered_conditions)
```

同じことをPrisma+TypeScriptで書ければ、特定のユースケースであればわざわざ`$extends`を利用せずとも回避可能かもしれない。