SQLAlchemy1.4が2021/3/17にリリースされた。
1.3.19 -> 1.4.1 に変更する際に調査したことをメモする。
Sessionがコンテキストマネージャに
1.3では
Sessionを使用する際に、close()することを忘れてはいけない。
そのため、try~finallyや以下のようなコンテキストマネージャを作成して、with構文で使用するようにしていた。
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が行われる。
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を実装しているので以下のようにするとさらに便利。
# 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()
# 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
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
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の属性の型が異なるというエラーである。
@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というアノテーションを利用した方法が追加された。
@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
mapperを使用することが非推奨となった。代わりにmap_imperativelyを使用する。
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)
##おまけ
sqlalchemy2-stubsがデフォルトで内部に組み込まれた。
そのため1.3の時のようにerror: Invalid base class "Base"というmypyエラーを回避するために
sqlalchemy-stubsなどのスタブをわざわざインストールする必要もなくなった。