SQLAlchemy 2.0 のMovedIn20Warningを多少取り除く

## 概要

数年前に作ったコードを久しぶりに動かしたら、警告が出たので少しずつ直してみた。
移行に関する修正方法は [Migrating to SQLAlchemy 2.0](https://docs.sqlalchemy.org/en/14/changelog/migration_20.html) に載っているが、単純置換できなかったり、修正方法に少し悩んだコードを載せる。

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

### 環境

* SQLAlchemy(Linux): 1.4.17

### コード1: import の警告

#### 修正前

```python
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 の行を修正

```python
from sqlalchemy.orm import declarative_base

# ...

Base = declarative_base()
```


### コード2: engine.execute()

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

#### 修正前

```python
    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 を使えとあるので、言う通りにする

```python
    if driver_name == 'sqlite':
        with engine.begin() as conn:
            conn.execute(
                sqlalchemy.text("ATTACH DATABASE ':memory:' AS :schema"),
                {'schema': 'aaaaa'}
            )
```

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

#### 修正前

```python
    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.select` を `execute` に喰わせる
* all() の代わりに mappings() を使う
* 何気に生SQLの出力方法に非互換があるので、Selectのインスタンスに対して .compile() を実行する必要がある

```python
    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を出力する部分は、検索しても情報が出てこず、しばらくやり方が分からなかった。