PrismaのTypedSQLでMySQLに配列を渡す方法
はじめに
プレビューとなったPrismaのTypedSQLは便利な機能ですが、MySQLで配列を直接引数として渡せないという制限があります。(2024/10月現在)
PostgreSQLではANY演算子を使って実現できますが、MySQLでは異なるアプローチが必要となります。
この記事では、Json型を使ってMySQLでTypedSQLに配列を渡す方法を紹介します!
問題点
シンプルなUserテーブルを例に考えます。IDの配列を受け取り、該当するユーザーを取得したいとします。
SELECT id, email FROM User WHERE id IN (?)
しかしprisma generate --sql
を実行すると、生成されるTypeScriptの関数の引数の型は、配列でなく数値型になってしまいます。(この場合はnumber | bigint
)
export const findUsersByIds: (_0: number | bigint) => $runtime.TypedSql<a.Parameters, a.Result>
// ...(省略)
ちなみにPostgreSQLではANY演算子を利用することで、配列型で受け取れるようになります。
SELECT id, email FROM User WHERE id = ANY($1)
解決策
TypedSQLのJson型を利用する方法でMySQLでも配列を扱えるようになります。具体的には、以下の手順で実現します。
- SQLでJson型の引数を受け取る
- JSONをキーバリュー形式に変換し、
JSON_TABLE
関数でテーブルとして扱う - 生成されたテーブルからIDを抽出し、
IN
句で使用する - TypeScriptで数値配列をJSON形式に変換する
最終的には以下のような実装になります。
SQLの実装
-- @param {Json} $1:userIds
WITH Input AS (
SELECT userId
FROM JSON_TABLE(
JSON_KEYS(?),
'$[*]' COLUMNS( userId INT PATH '$')
) AS jt
)
SELECT id, email FROM User
WHERE
id IN (SELECT userId FROM Input)
;
TypeScriptの実装
import { prisma } from "@/extensions/prisma/client"
import { InputJsonObject } from "@/extensions/prisma/generated/runtime/library"
import { findUsersByIds } from "@/extensions/prisma/generated/sql"
const withFindUsersByIds = async (userIds: number[]): Promise<findUsersByIds.Result[]> => {
const jsonObject: InputJsonObject = Object.fromEntries(userIds.map(id => [id, id]))
return prisma.$queryRawTyped(findUsersByIds(jsonObject))
}
解説
SQLでJSON型の引数を受け取る
-- @param {Json} $1:userIds
TypedSQLではSQLに渡す引数の型を指定できます。String型でも同様の機能が実現できますが、今回はより型を厳格に指定できMySQLのサポート関数が豊富なJson型を利用します。
JSON_TABLE
関数でテーブルとして扱う
JSONをキーバリュー形式に変換し、TypedSQLのJson型はInputJsonObject
として定義されています。
export declare type InputJsonObject = {
readonly [Key in string]?: InputJsonValue | null;
};
export declare type InputJsonValue = string | number | boolean | InputJsonObject | InputJsonArray | {
toJSON(): unknown;
};
この型でユーザーIDの配列を表現するため、以下のようにオブジェクトのkeyにユーザーIDを設定するようにします。(オブジェクトのvalueはなんでも良いため"xx"としています。)
{"1": xx, "5": xx, ..., "userId": xx}
MySQLのJSON_KEYS
関数とJSON_TABEL
関数を利用してオブジェクトのkeyをカラムとするテーブルを作成します。
SELECT userId
FROM JSON_TABLE(
JSON_KEYS('{"1": xx, "5": xx }'), -- ["1", "5"]
'$[*]' COLUMNS( userId INT PATH '$')
) AS jt;
結果:
userId |
---|
1 |
5 |
IN
句で使用する
生成されたテーブルからIDを抽出し、JSON_KEYS
関数に引数で指定したJson型を指定し、JSON_TABLE
関数で抽出したユーザーIDをIN句で指定します。
-- @param {Json} $1:userIds
WITH Input AS (
SELECT userId
FROM JSON_TABLE(
JSON_KEYS(?), -- Json型の引数を渡す
'$[*]' COLUMNS( userId INT PATH '$')
) AS jt
)
SELECT id, email FROM User
WHERE
id IN (SELECT userId FROM Input) -- IN句に指定する
;
これでSQLの実装は完了です。次にTypeScriptの実装をしていきます。
TypeScriptで数値配列をJSON形式に変換する
TypeScriptではユーザーIDがKeyとなるオブジェクトを生成し、自動生成したfindUsersByIds
関数に渡します。
const withFindUsersByIds = async (userIds: number[]): Promise<findUsersByIds.Result[]> => {
// ユーザーIDがKeyとなるオブジェクトを生成する
const jsonObject: InputJsonObject = Object.fromEntries(userIds.map(id => [id, id]))
return prisma.$queryRawTyped(findUsersByIds(jsonObject))
}
まとめ
MySQLでPrismaのTypedSQLに配列を渡す方法を紹介しました!
Json型とJSON_TABLE
関数を活用することで、PostgreSQLのANY演算子のような機能を実現できます。
この方法を利用すれば、より柔軟なクエリを記述することができると思います。
参考にしていただければ幸いです!
レバテック開発部の公式テックブログです! レバテック開発部 Advent Calendar 2024 実施中: qiita.com/advent-calendar/2024/levtech
Discussion