SQLAlchemy flushとcommitの使い方

## まとめ(結論だけ知りたい人用)
以下の使い方をすると便利です。

- commit
  - 一連のデータ操作の最後に**一回だけ**実行するというルールを守る
- flush
  - `sessionmaker()`に`autoflush=True`をセットし、ライブラリ側にflush処理を委譲する

***
便利な理由を理解したい方は記事本文をどうぞ

## 概要
SQLAlchemyは暫定的データ更新内容をDBに確定させるメソッドとして

- commit
- flush

を提供しています。

この記事では、この二つのメソッドの使い方を見ていきます。


## 性質の違い
commitとflushは以下の異なる性質を持っています。
- commit
  - データの更新内容の**恒久的**な確定
  - ROLLBACK不可

- flush
  - データの更新内容の**一時的**な確定
  - ROLLBACK可能

この性質の違いにより、データを確定させるという同じようなメソッドであっても、
副次的な意味が全く異なるものとなっています。

## commitの使い方
使い方は下記の通りです。

- 一連のデータ操作の最後に**一回だけ**実行する

理由としてはcommitが複数箇所で実行されると**トランザクションのACID特性が破綻する**
恐れがあるためです。

破綻する例は下記リンク先参照
[SQLAlchemyにおけるcommitの誤用によるACID特性の破綻例](https://qiita.com/GuriTech/items/5bcd5b5a7d83b4c64f0f)

コードとしては以下のような使い方になります。
```python
SessionClass = sessionmaker(engine, autoflush=False, autocommit=False)
session = SessionClass()

# ~~~ 一連のデータ操作 ~~~

session.commit()  # 最後に一回だけcommitする
```

## flushの使い方
使い方は以下の通りです。

- データの更新を明示的に行いたい場合に実行する

具体的な例が無いとわかりにくいため、実際にflushが必要となる場面を見ていきます。

### flushが無いことで起こるバグ
以下のコードを実行してみます。
```python:flushが無いことでエラーが発生するコード
SessionClass = sessionmaker(engine, autoflush=False)
session = SessionClass()

user = User(user_id="1", name="Johndoe", email="johndoe@example.com")
session.add(user)
# session.flush()

print(session.query(User).filter(User.user_id == "1").one())

session.commit()
session.close()
```

実行すると`sqlalchemy.exc.NoResultFound`が発生します。

コードを見る限りuser_id="1"のレコードが存在しているため、
`NoResultFound`が発生するようには見えません。

実際に実行されるSQLクエリは以下の通りとなっています。
```sql:MySQLのログから抜粋
Query     SELECT user.id AS user_id, user.name AS user_name, user.email AS user_email FROM user WHERE user.id = '1'
```
INSERTが実行されていません。

実はSQLAlchemyではflushかcommitが実行されないとINSERT/UPDATEといった
データ内容の変更を伴うクエリが実行されません。
そのためflushを実行し、INSERTが実行されるようなコードに修正する必要があります。

```python:flushを有効化したコード
SessionClass = sessionmaker(engine, autoflush=False)
session = SessionClass()

user = User(user_id="1", name="Johndoe", email="johndoe@example.com")
session.add(user)
session.flush()

print(session.query(User).filter(User.user_id == "1").one())

session.commit()
session.close()
```
このコードを実行した際のクエリは以下の通り
```sql:MySQLのログから抜粋
Query     INSERT INTO user (id, name, email) VALUES ('1', 'Johndoe', 'johndoe@example.com')
Query     SELECT user.id AS user_id, user.name AS user_name, user.email AS user_email FROM user WHERE user.id = '1'
Query     COMMIT
```
ちゃんとINSERTが実行され、`sqlalchemy.exc.NoResultFound`が発生しなくなりました。

## 実務上でcommitとflushを使う
ここまでメソッド単体としてのcommitとflushの使い方を見てきました。
メソッドを明示的に実行するとコードとしては読みやすくなります。
しかし残念なことに実際にそれで実装すると、無駄なクエリが発行されてしまう
という問題が発生します。
そのため、実務では明示的実行以外の美味しい方法を使って問題を回避しなければいけません。

ここからは実務上での美味しい使い方を見ていきます。

### commit
commitに関しては特に美味しい方法はありません。
記事の冒頭に記した[メソッド単体での使い方](commitの使い方)と同じく

- 一連のデータ操作の最後に一回だけ実行する

というルールを守るだけです。

### flush
凄く端的に言えば

- sessionmaker(autoflush=True)を使い、ライブラリ側にflush処理を委譲する。

というものになります。

flushを明示的に実行すると無駄なクエリが発生します。
例えば下記のようなコードでは無駄なINSERTが発行されます。
```python:flushを明示的に実行した場合
SessionClass = sessionmaker(engine, autoflush=False)
session = SessionClass()

user = User(user_id="1", name="Johndoe", email="johndoe@example.com")
user2 = User(user_id="2", name="Johndoe", email="johndoe@example.com")
session.add(user)
session.flush()
session.add(user2)
session.flush()

print(session.query(User).all())

session.commit()
session.close()
```
以下のクエリが発行されます。
```sql:MySQLのログから抜粋
Query     INSERT INTO user (id, name, email) VALUES ('1', 'Johndoe', 'johndoe@example.com')
Query     INSERT INTO user (id, name, email) VALUES ('2', 'Johndoe', 'johndoe@example.com')
Query     SELECT user.id AS user_id, user.name AS user_name, user.email AS user_email FROM user
Query     COMMIT
```
INSERTの部分で無駄なINSERTが発行されています。

この次に`autoflush=True`を有効化した場合を見ていきましょう。

```python
SessionClass = sessionmaker(engine, autoflush=True)
session = SessionClass()

user = User(user_id="1", name="Johndoe", email="johndoe@example.com")
user2 = User(user_id="2", name="Johndoe", email="johndoe@example.com")
session.add(user)
session.add(user2)

print(session.query(User).all())

session.commit()
session.close()
```
実行すると以下のクエリがDB上で実行されます。
```sql:MySQLのログから抜粋
Query     INSERT INTO user (id, name, email) VALUES ('1', 'Johndoe', 'johndoe@example.com'),('2', 'Johndoe', 'johndoe@example.com')
Query     SELECT user.id AS user_id, user.name AS user_name, user.email AS user_email FROM user
Query     COMMIT
```
INSERT文が1つにまとめられていることが分かります。

サンプルは少ないですが、これらの事実からflushのライブラリ側への委譲は有用であると考えられます。