SQLAlchemyのEager Loadingをいい感じにやる
SQLAlchemy==1.4.37
N+1のlazy fetch
をしたくない場合、eager load
というJOIN時に予めデータを持ってくる方式があります。SQLAlchemyでも、Railsでも、この辺は一緒。Alchemyの場合、孫のテーブルをJOINした時もそれらが出来る。
数時間苦戦したので、Eager Loadingの設定をメモる。
filterする対象のテーブルが自分だけ
この場合は、joinedload
でいけた。
session.query(UserTable) .options( joinedload(UserTable.attention), joinedload(UserTable.friend_type), joinedload(UserTable.bookmark), ) .filter(UserTable.user_id == user_id)
UserTableを軸に、Where句で引いてくるのがUserTableのカラムのみ。
filterする対象のテーブルが複数ある
この場合は、以下のようなjoinedload
を使うと、えらいことになった。
session.query(UserTable) .options( joinedload(UserTable.attention), joinedload(UserTable.friend_type), joinedload(UserTable.bookmark), ) .filter(UserTable.user_id != user_id) .filter(UserFriendTypeTable.friend_type.in_(["AAA","BBB"])
えらいことになるというのは、filterで指定したテーブルが OUTER JOINではなくCROSS JOINされていたため。FROM user_table, user_friend_type_table というSQLになるので、激重になった。
複雑な検索条件に基づいてフェッチする場合は、join
とcontains_eager
の合わせ技が良いみたい。
session.query(UserTable) .outerjoin(UserBookmarkTable, UserBookmarkTable.user_id == UserTable.id) .join(UserFriendTypeTable, UserFriendTypeTable.user_id == UserTable.id) .options( contains_eager(UserTable.bookmark), contains_eager(UserTable.friend_type), ) .filter(UserTable.user_id != user_id) .filter(UserFriendTypeTable.friend_type.in_(["AAA","BBB"])
自分はいつもwith_entities
でカラムを指定してSELECT文を投げることが多かった。一覧画面に表示するためのSQLが多いので、JOIN先がN件でも親に合わせるためにカラムを指定してた。ただ、今回は、要件が特殊でカラムを指定するのではなく、N+1せずに結合先のレコードをリストで保持して置く必要があったので、苦戦したのだった。
これでだいぶRelationshipには慣れてきたな。ドキュメント、じっくり読もう。どこかで。