3
6

More than 1 year has passed since last update.

SQLAlchemy 2.0 のMovedIn20Warningを多少取り除く

Last updated at Posted at 2022-09-09

概要

数年前に作ったコードを久しぶりに動かしたら、警告が出たので少しずつ直してみた。
移行に関する修正方法は Migrating to SQLAlchemy 2.0 に載っているが、単純置換できなかったり、修正方法に少し悩んだコードを載せる。

  • (1.4のコードを捨てて)本気で2.0に移行するのでなく、2.0 の衝撃に備える程度ぐらいのモチベーションとなる

環境

  • SQLAlchemy(Linux): 1.4.17

コード1: import の警告

修正前

from sqlalchemy.ext.declarative import declarative_base  # here

# ...

Base = declarative_base()
MovedIn20Warning: The ``declarative_base()`` function is now available as sqlalchemy.orm.declarative_base(). (deprecated since: 1.4) (Background on SQLAlchemy 2.0 at: http://sqlalche.me/e/b8d9)
  Base = declarative_base()

修正後

  • import の declarative_base の行を修正
from sqlalchemy.orm import declarative_base

# ...

Base = declarative_base()

コード2: engine.execute()

コード自体は PostgreSQL 向けにスキーマのあるテーブルに対して SQLite でも同じコードを動かすための前処理。

修正前

    if driver_name == 'sqlite':
        conn.execute(      # here
            sqlalchemy.text("ATTACH DATABASE ':memory:' AS :schema"),
            schema='aaaaa' # here
        )
RemovedIn20Warning: The Engine.execute() method is considered legacy as of the 1.x series of SQLAlchemy and will be removed in 2.0. All statement execution in SQLAlchemy 2.0 is performed by the Connection.execute() method of Connection, or in the ORM by the Session.execute() method of Session. (Background on SQLAlchemy 2.0 at: http://sqlalche.me/e/b8d9)
  engine.execute(
RemovedIn20Warning: The connection.execute() method in SQLAlchemy 2.0 will accept parameters as a single dictionary or a single sequence of dictionaries only. Parameters passed as keyword arguments, tuples or positionally oriened dictionaries and/or tuples will no longer be accepted. (Background on SQLAlchemy 2.0 at: http://sqlalche.me/e/b8d9)
  engine.execute(

修正後

  • Connection.execute() を行うように修正
  • パラメーターは dict を使えとあるので、言う通りにする
    if driver_name == 'sqlite':
        with engine.begin() as conn:
            conn.execute(
                sqlalchemy.text("ATTACH DATABASE ':memory:' AS :schema"),
                {'schema': 'aaaaa'}
            )

コード2: all()の後のrowの参照で警告

修正前

    query_obj = session.query(
        FruitsMenu
    ).filter(
        sqlalchemy.or_(
            FruitsMenu.name == 'Apple',
            FruitsMenu.name == 'Orange'
        )
    ).with_entities(
        FruitsMenu.name,
        FruitsMenu.price
    )
    items = query_obj.all()

    records = []
    row: sqlalchemy.engine.row.Row
    print(f"items={items}, type={type(items).__module__}.{type(items).__name__}")
    for row in items:
        print(f"row={row}, type={type(row).__module__}.{type(row).__name__}")
        records.append([row['name'], row['price']])  #  警告

    stmt = query_obj.statement.compile(         # 非互換あり
        compile_kwargs={"literal_binds": True}  #
    )                                           #
    print(str(stmt))                            #

row の型は sqlalchemy.engine.row.Row となる。

items=[('Apple', 10), ('Orange', 110)], type=builtins.list
row=('Apple', 10), type=sqlalchemy.engine.row.Row
database/sqlalchemy-common/src/sqlalchemy_orm_select_with_entities.py:101: RemovedIn20Warning: Retrieving row members using strings or other non-integers is deprecated; use row._mapping for a dictionary interface to the row (Background on SQLAlchemy 2.0 at: http://sqlalche.me/e/b8d9)
  records.append([row['name'], row['price']])
row=('Orange', 110), type=sqlalchemy.engine.row.Row
SELECT guest.fruits_menu.name, guest.fruits_menu.price
FROM guest.fruits_menu
WHERE guest.fruits_menu.name = 'Apple' OR guest.fruits_menu.name = 'Orange'

警告内容

RemovedIn20Warning: Retrieving row members using strings or other non-integers is deprecated; use row._mapping for a dictionary interface to the row (Background on SQLAlchemy 2.0 at: http://sqlalche.me/e/b8d9)

修正後

  • sqlalchemy.sql.expression.selectexecute に喰わせる
  • all() の代わりに mappings() を使う
  • 何気に生SQLの出力方法に非互換があるので、Selectのインスタンスに対して .compile() を実行する必要がある
    query_obj = sqlalchemy.sql.expression.select(
        FruitsMenu.name,
        FruitsMenu.price
    ).filter(
        sqlalchemy.or_(
            FruitsMenu.name == 'Apple',
            FruitsMenu.name == 'Orange'
        )
    ).with_only_columns(FruitsMenu.name, FruitsMenu.price)
    items = session.execute(query_obj).mappings() # here

    records = []
    row: typing.Mapping[typing.Any, typing.Any]
    print(f"items={items}, type={type(items).__module__}.{type(items).__name__}")
    for row in items:
        print(f"row={row}, type={type(row).__module__}.{type(row).__name__}")
        records.append([row['name'], row['price']])

    print("{}".format(query_obj.compile(compile_kwargs={'literal_binds': True})))

次のように変わっている。

  • items の型が list → sqlalchemy.engine.result.MappingResult
  • row の型が sqlalchemy.engine.row.Row → sqlalchemy.engine.row.RowMapping
items=<sqlalchemy.engine.result.MappingResult object at 0x7fd0326e4430>, type=sqlalchemy.engine.result.MappingResult
row={'name': 'Apple', 'price': 10}, type=sqlalchemy.engine.row.RowMapping
row={'name': 'Orange', 'price': 110}, type=sqlalchemy.engine.row.RowMapping
SELECT guest.fruits_menu.name, guest.fruits_menu.price
FROM guest.fruits_menu
WHERE guest.fruits_menu.name = 'Apple' OR guest.fruits_menu.name = 'Orange'

まとめ

人のコードをレビューする立場の人は気にしておいた方が良いとは思うが、本家記事でも情報不足がある。少しずつ情報収集していくしかない。

特に、生SQLを出力する部分は、検索しても情報が出てこず、しばらくやり方が分からなかった。

3
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
6