Life is Really Short, Have Your Life!!

ござ先輩の主に技術的なメモ

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になるので、激重になった。

複雑な検索条件に基づいてフェッチする場合は、joincontains_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には慣れてきたな。ドキュメント、じっくり読もう。どこかで。