Prisma ORM を Python からも利用したくて最近よく Prisma Client Python を使っている。普通に利用する分には何も問題はなく、TypeScript 版 Prisma Client に倣ったインタフェースは非常によくできていてかなり好きなのだけれど、ただ一点「JSON 型が扱いづらい」という問題だけが悩ましい。そもそも RDB で非構造データ型を多用するなという話ではあるので、妙なことをしようとして妙なところでハマっているという様相である。
この問題のスマートな解決策は未だ見つけられていないのだけれど、「とりあえずこうすれば動く」は見つけられたのでまとめておく。
環境
- Prisma 5.17.0
- Python 3.12
- Prisma Client Python 0.15.0
- Pyright
- PostgreSQL 15
例
こういうスキーマでテーブルを作成したとする。
model Log {
id Int @id @default(autoincrement())
meta Json
}
問題1: SELECT
結果の型が合わない
from prisma.models import Log
log = await Log.prisma().find_first()
log.meta # 実態は dict 型だが、型チェッカーは prisma.fields.Json 型と認識している
find
などのメソッドで取得したデータを見ると dict
型が入っており、既に parse されていることがわかる。しかし型チェッカーは str
を継承した prisma.fields.Json
型であると認識してしまう。1
型チェッカーに実態と合った型を認識させるためには、わざわざ cast をする必要がある。
from typing import cast
meta = cast(dict, log.meta) # dict[Unknown, Unknown] 型
ただし dict
だと型がゆるふわ過ぎるので、実際には TypedDict
を使うことが多い。
from typing import NotRequired, TypedDict
class Meta(TypedDict):
count: NotRequired[int]
from typing import cast
meta = cast(Meta, log.meta)
問題2: INSERT / UPDATE 時には Json 型が要求される
meta: Meta = {"count": 123}
await Log.prisma().create(data={"meta": meta})
#=> prisma.errors.DataError:
#=> Invalid argument type. `meta` should be of any of the following types: `Json`
Json 型フィールドを指定して INSERT / UPDATE したいとき、値を dump せずにそのまま指定することはできない。上記の問題と似ているが、ここでも prisma.fields.Json
型が期待される。
from prisma.fields import Json
meta: Meta = {"count": 123}
await Log.prisma().create(data={"meta": Json(meta)}) # 動くが、型エラー
prisma.fields.Json
型にしてやれば動くには動くが、型チェッカーは prisma.fields.Json
のコンストラクタに str
型を期待するので、これだけだと型エラーが出てしまって気持ち悪い。
from typing import cast
from prisma.fields import Json
meta: Meta = {"count": 123}
await Log.prisma().create(data={"meta": Json(cast(str, meta))})
ここまですれば型エラーも消える。
しかしさすがに面倒すぎる。
from typing import cast
from prisma.fields import Json
def encode_to_prisma_json(obj: Any) -> Json:
return Json(cast(str, obj))
meta: Meta = {"count": 123}
await Log.prisma().create(data={"meta": encode_to_prisma_json(meta)})
変換するところだけ関数に切り出すと少しはマシかもしれない。
問題3: Optional な JSON フィールドに NULL を指定できない
model Log {
id Int @id @default(autoincrement())
meta Json?
}
Json 型のフィールドが Optional (Nullable) の場合で、かつ何らかの理由で NULL を明示的に指定したい場合、これがうまくいかない。「IS NULL
でフィルタリングしたい場合」「明示的に NULL を INSERT / UPDATE したい場合」どちらもエラーが出てしまう。
await Log.prisma().create(data={"meta": None})
#=> prisma.errors.MissingRequiredValueError:
#=> `data.meta`: A value is required but not set
この問題は既に Issue が立っていて v0.15.0 で解決されたように見える2 のだけど、自分が動かしてみた限りでは問題が再現してしまう。困った。
この問題は良い解決策が本当に無くて、最終手段として query_raw
を使うことを余儀なくされている。とてもつらい。
TypeScript の Prisma Client では、プリミティブな undefined
null
および Prisma 側で提供されている DbNull
JsonNull
を使うことで様々なケースに対応できる。3 しかし Python においては undefined
null
に相当する値がいずれも None
で区別できず、かつ Prisma Client Python は DbNull
などをサポートしていないため、すべて None
で表現されてしまうことが本質的な課題である気がする。
「そもそも Optional な JSON 型などというややこしいフィールドを使っている時点で設計が悪い」? それはそう...
おわり
こんな話をすると「Prisma Client Python ってヤバいんちゃう」って思われそうだけど、JSON 関連以外は全く問題ないっていうか本当に最高の体験なんや... ぜひ触ってみて欲しい