SQLAlchemy の .where(User.id =="123") のUser.id =="123"ってBoolにならないの?
クロスマートアドベントカレンダー 20 日目の記事です!(大遅刻!ごめんなさい!)
今回は、以前発表させていただいた、こちらの資料を改めてブログに認め直しました!
はじめに
SQLAlchemy を使っていると、ふと疑問に思います。
select(User).where(User.id == "123")
という表記がありますが、
User.id == "123"
の部分。Boolean に変換されてしまうのではないでしょうか。
しかし、実際には以下の SQL 相当の処理として機能します。
SELECT * FROM `User` WHERE id = '123';
なぜこのように動くのでしょうか。
この処理の間で、どのようなことが起きているのでしょうか。
Python における演算子のオーバーロード
この挙動を理解するために、Python の演算子オーバーロードという機能について考えます。
演算子オーバーロードとは、Python に用意されている演算子である+, /, -などの挙動を自由に書き換えることができる文法のことを言います。
例えば、以下の例では、独自の String 型である MyStr を定義し、その足し算の挙動を変更しています。
class MyStr:
def __init__(self, value: str):
self.value = value
def __add__(self, other):
if isinstance(other, MyStr):
return MyStr(self.value + "たす" + other.value)
else:
raise ValueError("Error")
sample1 = MyStr("A")
sample2 = MyStr("B")
combined = sample1 + sample2
print(combined.value)
#-> "AたすB"
python 標準の文字列型であれば、文字列 A と文字列 B を足し算の演算子"+"を通した場合、"AB"が出力されるはずですが、MyStr の場合は、__add__の定義を、「"たす"で文字列同士を繋げる」と定義しているため、出力がAたすBとなっています。
SQLAlchemy での例
演算子オーバーロードについて知ることができたところで、本題に戻ります。
今回の例では、select(User).where(User.id =="123")についてでした。お気づきだとは思いますが、この SQLAlchemy の記法も、等価演算子である"=="の上書きとして理解することができます。
以下に__eq__を用いて"=="の上書きを行うことで、Python のオブジェクトから SQL の"="の文に相当する変換を行う処理を示します。
class Column:
def __init__(self, name: str):
self.name = name
def __eq__(self, other: str):
return f"{self.name} = {other}"
id_column = Column("Id")
print("output: ", id_column == "123")
#-> output: ID = 123
この例では、DB モデルのカラム相当として Column クラスを定義し、column クラスの__eq__メソッドを独自定義のものに書き換えています。
このことにより、Column 型の id_column は、==という演算子に対して bool を返すのではなく、"ID = 123"という文字列を返すことができています。
実際の Sqlalchemy の column を表す Mapped などは、今回の例のように、直接文字列を返却するわけではなく、クエリの状態を保持したオブジェクトを返すようになっていますが、原理的には近い形で実装されています。
演算子オーバーロードの普段の使い道
ところで、このような演算子オーバーロード、普段使っている範囲にも出てくることが稀にあります。それを紹介します。
pathlib の例
例えば、python で path などを扱うときに OS の差分などを吸収してくれる pathlib では、path を示すために以下のような記法をサポートしています。
from pathlb import Path
p = Path("/parent")
q = p / "child1" / "child2"
print(q)
#-> /parent/child1/child2
本来、"/"は割り算を表す演算子ですが、"/"を用いて、path の結合を表すことができています。
このことにより、(好き嫌いはありそうですが)直感的に、階層構造を示すことができていますね。
オブジェクトの比較の例
また、一般的な活用方法としては、同じオブジェクトかどうかの判定のために__eq__を自分で定義などをしたりしていました。
例えば、以下の例だと User クラスは、全て同じフィールドで構成されていたとしても、==で評価された場合に False になります。
class User:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
user1 = User("User1", 10)
user2 = User("User1", 10)
print(user1 == user2)
#-> False
これは、python のデフォルトの class は等価かどうかの判定を、同じメモリに割り当てられているオブジェクトかで判定しているため起きていることです。そのため、同じフィールドを持っていた場合に同じオブジェクトとしてみなしたいというルールを適用したい場合は、以下のように書くことでそれを達成することができます。
class User:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
def __eq__(self, other: "User"):
is_name_same = self.name == other.name
is_age_same = self.age == other.age
return is_name_same and is_age_same
user1 = User("User1", 10)
user2 = User("User1", 10)
print(user1 == user2)
#-> True
このようにすることで、フィールドが同じオブジェクトかの情報を直感的に記述することができるようになりました。
ただ、これについては、python の dataclass を用いることで、__eq__の上書きなどのことをしなくても、デフォルトで==がフィールド同士の比較として定義されています。
from dataclasses import dataclass
@dataclass
class User:
name: str
age: int
user1 = User("User1", 10)
user2 = User("User1", 10)
print(user1 == user2)
#-> True
特に特殊なユースケースではない場合は、dataclass を使うのがいいですね。
終わりに
今回は、SQLAlchemy のクエリの投げ方についての疑問を深掘り、演算子オーバーロードについて深掘りました!
これからも小さな疑問は逐一調べていきたいものです 🍵
おしまい!
Discussion