SQLAlchemyでのsessionの扱い方

# はじめに
SQLAlchemyでは，sessionを生成したあと，必要に応じて`commit()`や`rollback()`,`close()`を行う必要がある．
ここでは，DB操作を行うクラスを作成し，sessionの受け渡し方についてまとめる．
以下では，下のようなモデルクラスが存在しているとする．

```python:models.py
class User(Base):
    __tablename__ = "user"  # テーブル名を指定
    id = Column(Integer, primary_key=True)
    first_name = Column(String(255))
    last_name = Column(String(255))
```

# sessionの悪い扱い方
以下のソースコードのようにしてしまうと良くない．

```python:wrong_way.py
from models import User

class FirstName(object):
    def update_first_name(self, user_id, first_name):
        session = Session()
        try:
            user = session.query(User).filter(
                User.id == user_id).one()  # id=user_idであるobjを取り出す
            user.first_name = first_name  # first_nameを変更
            session.commit()
        except:
            session.rollback()
            raise

class LastName(object):
    def update_last_name(self, user_id, last_name):
        session = Session()
        try:
            user = session.query(User).filter(
                User.id == user_id).one()  # id=user_idであるobjを取り出す
            user.first_name = first_name  # last_nameを変更
            session.commit()
        except:
            session.rollback()
            raise

def run_my_program():
    FirstName().update_first_name(1, "update_first_name")
    LastName().update_last_name(1, "update_last_name")
```
なぜなら，`run_my_program`中の`update_first_name`と`update_last_name`で，同一sessionが使われていないため，
「first_nameはupdateされたが，last_nameはupdateされなかった」
のようなことが起きてしまうからである．

# sessionの良い扱い方1
そこで，以下のソースコードのようにすれば解決する．

```python:right_way_1.py
from models import User

class FirstName(object):
    def update_first_name(self, user_id, first_name, session):
        user = session.query(User).filter(User.id == user_id).one()
        user.first_name = first_name

class LastName(object):
    def update_last_name(self, user_id, last_name, session):
        user = session.query(User).filter(User.id == user_id).one()
        user.first_name = first_name

def run_my_program():
    session = Session()
    try:
        FirstName().update_first_name(session)
        LastName().update_last_name(session)
        session.commit()
    except:
        session.rollback()
        raise
    finally:
        session.close()
```
こうすることで，`update_first_name`と`update_last_name`で，同一sessionが使われ，
「片方だけが正常に実行された」
のような状況が起こらない．

# sessionの良い扱い方2
次に，`context manager`を用いたsessionの扱い方を紹介する．
なお，context managerについては，以下の記事が大変わかりやすく参考になるのでここでは説明を省略する．
[Pythonのコンテキストマネージャってなんなの？と思って調べた話](https://blog.mtb-production.info/entry/2018/04/10/183000)

```python:right_way_2.py
from models import User
from contextlib import contextmanager

@contextmanager
def session_scope():
    session = Session()  # def __enter__
    try:
        yield session  # with asでsessionを渡す
        session.commit()  # 何も起こらなければcommit()
    except:
        session.rollback()  # errorが起こればrollback()
        raise
    finally:
        session.close()  # どちらにせよ最終的にはclose()

class FirstName(object):
    def update_first_name(self, user_id, first_name, session):
        user = session.query(User).filter(User.id == user_id).one()
        user.first_name = first_name

class LastName(object):
    def update_last_name(self, user_id, last_name, session):
        user = session.query(User).filter(User.id == user_id).one()
        user.first_name = first_name

def run_my_program():
    with session_scope() as session:
        FirstName().update_first_name(session)
        LastName().update_last_name(session)
```
# 参考文献
この記事は以下の情報を参考にして執筆しました．
・[公式ドキュメント(Session Basics)](https://docs.sqlalchemy.org/en/13/orm/session_basics.html)
・[Pythonのコンテキストマネージャってなんなの？と思って調べた話](https://blog.mtb-production.info/entry/2018/04/10/183000)