SQLAlchemyでまとめてupsert

きっかけ

SQLAlchmey(ver 1.3)において、まとめてupsert(bulk upsert) するのはどうすればいいのか学んだので、備忘録で書いとこうと思いました。

その前に・・・・

upsertについて

みなさん、upsertって知ってますか?

そんなの基本だと言われるかもしれませんが、なんとなくSQLしてきた人からすると、

selectとinsertとupdateとdeleteくらいしか、実は知らなかったりします。(私だけかな・・・

upsertとは?

upsertとは、対象のデータがDBになければ追加、あれば更新してくれる命令

update と insert を組み合わせて、upsertと呼ばれています

upsertを使うメリット

upsertは select してレコードがあったら、update命令にして、

レコードがなければ、insert命令にするって感じでアプリ側で操作することで実現することも可能

ただし、selectとupdaete もしくは insert の組み合わせでは、

DBに2回SQLの問い合わせが必ず発生する。

それなら1回の問い合わせで済むupsertを使った方が、DBの負担は少ない。

DBへの負荷はなるだけ、少なく済ませるのが、やはり暗黙の了解でしょうか・・・

参考:https://web-academia.org/482/

本題

公式ドキュメントはこちら

https://docs.sqlalchemy.org/en/14/dialects/postgresql.html#insert-on-conflict-upsert

単発、つまり1レコードについての書き方は以下のサイトがわかりやすかったのでこちらを参考にする

https://qiita.com/nsuhara/items/86570f789093222252b1#upsertの実装

上記のサイトをベースに

複数レコードについては、書き方はこんな感じになる(Python)

from sqlalchemy.dialects import postgresql

class FeedbackNew(db.Model):
    __tablename__ = 't_feedback_new'

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    title = db.Column(db.String(255), nullable=False)

def bulk_upsert(record_lists) -> None:

    # stmt = ステートメント らしい
    stmt = postgresql.insert(FeedbackNew).values([
      dict(
        id=x.id,
        title=x.title,
      ) for x in record_lists
    ])

    do_stmt = stmt.on_conflict_do_update(
      index_elements=['id'],
      # set_ に渡せるのはdict型のみなので注意
            # 更新対象を渡す
      set_=dict(
          title=stmt.excluded.title,
      ),
    )

    # sessionの渡してるところとかは割愛します
    # とりあえず、どこかで session.executeするのを忘れずに・・・・
    session.execute(do_stmt)

ちなみに・・・

  • これは PostgreSQL ver.9.5以降 の機能らしい。
  • on_conflict_do_update は、SQLAlchemyが提供するメソッド
  • on_conflict_do_update は、PostgreSQL固有のinsert()関数内のON CONFLICT をサポートしている
  • ステートメントは、1つのSQLの命令を指す、(select文, insert文とか)

https://style.potepan.com/articles/26487.html

細かいことは調べてないが、とりあえず、こんな書き方で一気にupsertすることができるらしい 🧚‍♂️