SQLAlchemy1.3から1.4に移行

https://docs.sqlalchemy.org/en/14/changelog/migration_14.html

SQLAlchemy1.4が2021/3/17にリリースされた。
1.3.19 -> 1.4.1　に変更する際に調査したことをメモする。

## Sessionがコンテキストマネージャに

### 1.3では
Sessionを使用する際に、close()することを忘れてはいけない。
そのため、try~finallyや以下のようなコンテキストマネージャを作成して、with構文で使用するようにしていた。

https://docs.sqlalchemy.org/en/13/orm/session_basics.html#when-do-i-construct-a-session-when-do-i-commit-it-and-when-do-i-close-it

```python
from contextlib import contextmanager

@contextmanager
def session_scope():
    """Provide a transactional scope around a series of operations."""
    session = Session()
    try:
        yield session
        session.commit()
    except:
        session.rollback()
        raise
    finally:
        session.close()
```

### 1.4では
Sessionにコンテキストマネージャが実装されているのでそのままwith構文で使用でき、closeが行われる。

https://docs.sqlalchemy.org/en/14/orm/session_basics.html#framing-out-a-begin-commit-rollback-block

```python
with Session(engine) as session:
    try:
        session.add(some_object)
        session.add(some_other_object)
    except:
        session.rollback()
        raise
    else:
        session.commit()
```

ちなみにsession.begin()はcommitやrollbackを実装しているので以下のようにするとさらに便利。

```python
# create session and add objects
with Session(engine) as session:
    with session.begin():
      session.add(some_object)
      session.add(some_other_object)
    # inner context calls session.commit(), if there were no exceptions
# outer context calls session.close()
```

```python
# create session and add objects
with Session(engine) as session, session.begin():
    session.add(some_object)
    session.add(some_other_object)
# inner context calls session.commit(), if there were no exceptions
# outer context calls session.close()
```

##アノテーションでのマッピングが可能に

### 1.3では
以下2つの方法でマッピングが行えた。

**Declarative Mapping**

```python

Base = declarative_base()

class User(Base):
    __tablename__ = 'user'

    id = Column(Integer, primary_key=True)
    name = Column(String)
    fullname = Column(String)
    nickname = Column(String)
```

**Classical Mapping**

```python

metadata = MetaData()

user = Table('user', metadata,
            Column('id', Integer, primary_key=True),
            Column('name', String(50)),
            Column('fullname', String(50)),
            Column('nickname', String(12))
        )

class User(object):
    def __init__(self, name, fullname, nickname):
        self.name = name
        self.fullname = fullname
        self.nickname = nickname

mapper(User, user)
```


私のプロジェクトでは以下のように実装していたが、mypyエラーに引っ掛かっていた。
UserとUserModelの属性の型が異なるというエラーである。

```python
@dataclass
class User:
    id: int = field(init=False)
    name: Optional[str] = None
    fullname: Optional[str] = None
    nickname: Optional[str] = None


Base = declarative_base()

class UserModel(Base, User):
    __tablename__ = 'user'

    id = Column(Integer, primary_key=True)
    name = Column(String)
    fullname = Column(String)
    nickname = Column(String)
```


### 1.4では
**Declarative Mapping**
@mapper_registry.mappedというアノテーションを利用した方法が追加された。

https://docs.sqlalchemy.org/en/14/orm/mapping_styles.html#declarative-mapping-using-a-decorator-no-declarative-base

```python
@mapper_registry.mapped
class UserModel(User):
    __table__ = Table(
        "user",
        mapper_registry.metadata,
        Column("id", Integer, primary_key=True),
        Column("name", String(50)),
        Column("fullname", String(50)),
        Column("nickname", String(12)),
    )
    id: int = field(init=False)
    name: Optional[str] = None
    fullname: Optional[str] = None
    nickname: Optional[str] = None
```

このアノテーションを使用することで1.3の時に起きていたmypyエラーがなくなった。理由としては

- @dataclassがマッピングを実行する前に処理される。
- 内部で実装されているmapperも@dataclassの固有の属性を検出し、マッピング時にそれらを置き換えるようになった。


**Classical Mapping**

https://docs.sqlalchemy.org/en/14/orm/mapping_styles.html#imperative-a-k-a-classical-mappings

mapperを使用することが非推奨となった。代わりにmap_imperativelyを使用する。

```python
mapper_registry = registry()

user_table = Table(
    'user',
    mapper_registry.metadata,
    Column('id', Integer, primary_key=True),
    Column('name', String(50)),
    Column('fullname', String(50)),
    Column('nickname', String(12))
)

class User:
    pass

mapper_registry.map_imperatively(User, user_table)
```


##おまけ

https://docs.sqlalchemy.org/en/14/orm/mapping_styles.html#creating-an-explicit-base-non-dynamically-for-use-with-mypy-similar

sqlalchemy2-stubsがデフォルトで内部に組み込まれた。
そのため1.3の時のようにerror: Invalid base class "Base"というmypyエラーを回避するために
sqlalchemy-stubsなどのスタブをわざわざインストールする必要もなくなった。

https://pypi.org/project/sqlalchemy2-stubs/