2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Prisma Client Python で JSON 型を扱うときの Dirty Hack

Posted at

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

こういうスキーマでテーブルを作成したとする。

prisma.schema
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 を指定できない

prisma.schema
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 関連以外は全く問題ないっていうか本当に最高の体験なんや... ぜひ触ってみて欲しい

  1. https://github.com/RobertCraigie/prisma-client-py/blob/v0.15.0/src/prisma/_fields.py#L29

  2. https://github.com/RobertCraigie/prisma-client-py/pull/1000

  3. https://www.prisma.io/docs/orm/prisma-client/special-fields-and-types/working-with-json-fields#using-null-values

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?