【2021年版】python で ORM で DB操作

## はじめに
技術の進歩は目覚ましいものです。
Web記事を検索すると、検索ボリュームの大きい結果から表示されるため、知らず知らずのうちに古い技術を使ってしまうことが多々あります。

そこで、2021年時点で、windows 環境で python でDB操作+OR Mapper した構成を書いておきたいと思います。

|ツール   |説明  |
|---|---|
|chocolatey | Windows用パッケージ管理システム。
|python3.9.2 | python 本体。
|poetry | python のパッケージ管理システム。<br/ >pip は requirements.txt を手書きしないとダメだったが、npmやyarn の様に pyproject.toml を自動で更新してくれる。
|mysqlalchemy | OR Mapper。簡単なクラス定義で簡単にDB操作ができる。
|alembic | DB 構築/マイグレーションヘルパ。mysqlalchemy のクラス定義に沿ったスキーマをDBに構築。

※以後の説明のコードブロックの null ではじまるファイル名は記事でハイライトされる様に定義したファイル名で実際にはファイルを作っていません。

## chocolatey

[chocolatey](https://chocolatey.org/)
インストーラをダウンロードしてインストールしていたようなパッケージをコマンド一発でインストールできます。

管理者権限で PowerShell を開いて以下コマンド実行でインストールできます。

``` null.ps1
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
```

記事を書く時にググると新しいモノが見つかって微妙な気持ちになりますね。。。

scoop は未チェックでした。
[Windows開発環境の構築をChocolateyからscoopに切り替える](https://tech.guitarrapc.com/entry/2019/12/01/233522)

## python

chocolatey で簡単にインストールできます。
バージョン番号を指定しないと、インストール時点の最新版がインストールされます。

``` null.ps1
choco install python
```

## poetry

pip を使ってインストールします。
pip はこれ以後利用しません。

``` null.ps1
py -m pip install poetry
```

poetry は仮想環境(virtualenv)を利用します。
以下設定をするとプロジェクト内に.venv ファイルを作成しモジュールの保存も行う様になります。(nodejs の node_modulesの様なフォルダ)
容量的にはPCに優しくないのですがこの設定をしないとモジュール参照の解決が面倒くさいのでした方がよいかなと思っています。

``` null.ps1
poetry config virtualenvs.in-project true
```

## alembic, SQLAlchemy

alembic 以前は、SQLAlchemy-Migrate だったそうです。
SQLAlchemy の記事からたどってくと SQLAlchemy-Migrate にたどり着くのでどちらを使うかはググって判断してください。

poetry で、alembic, sqlalchemy をインストールします。

``` null.ps1
poetry init # 対話コンソールになるので適宜回答
poetry add alembic sqlalchemy
```

## alembic

SQLAlchemy の class 定義と、DBを突き合わせて、テーブルを構築するのに必要なスクリプトの生成、実行が行えます。

### 初期化

最初に alembic init を実行し、DB構築の準備をします。

``` null.ps1
# alembic init {フォルダ名}
poetry run alembic init migrations
```

コマンドを実行すると、DB構築用のスクリプトと、migration 用のスクリプトを保存するフォルダが生成されます。

```
migrations/
  versions/
  env.py
  README
  script.py.mako
alembic.ini
```

生成された、alembic.ini のDB接続文字列を環境に合わせて修正します。

``` alembic.ini
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8

sqlalchemy.url = driver://user:pass@localhost/dbname
```

### ORマッパーclass定義

このままだと、作るテーブルがないので適当にクラス定義します。

```
db/
  models.py
```

SQLAlchemy の Baseクラスから派生したクラスを定義します。
全てのテーブルに埋めたい created, modified などのフィールドを含むクラスを `__abstract__` で定義し、そのクラスより派生すると少し楽になれるかもしれません。

``` models.py
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.schema import Column
from sqlalchemy.schema import ForeignKey
from sqlalchemy.sql.functions import random
from sqlalchemy.sql.schema import UniqueConstraint
from sqlalchemy.sql.sqltypes import DATETIME
from sqlalchemy.types import Integer, String

from datetime import datetime

Base=declarative_base()

class BaseModel(Base):
    __abstract__ = True
    id = Column(Integer, primary_key=True, autoincrement=True)
    created = Column(DATETIME, default=datetime.now)
    modified = Column(DATETIME, default=datetime.now)

    def __init__(self):
        self.created = datetime.now
        self.modified = datetime.now

class User(BaseModel):
    """
    service users.
    """
    __tablename__ = "users"
    email = Column(String(255), unique=True)
    password = Column(String(255))

    def __init__(self, email, password):
        self.email = email
        self.password = password

class Match(BaseModel):
    __tablename__ = "matches"
    seed = Column(Integer, nullable=False)

    def __init__(self):
        self.seed = random.randint()

class MatchUser(BaseModel):
    __table_args__= ((UniqueConstraint("match_id", "user_id"), ))
    __tablename__ = "match_users"
    match_id = Column(Integer, ForeignKey("matches.id", onupdate="cascade", ondelete="cascade"))
    user_id = Column(Integer, ForeignKey("users.id"), nullable=False)

    def __init__(self, match_id, user_id):
        self.match_id = match_id
        self.user_id = user_id

class MatchTurn(BaseModel):
    __tablename__ = "match_turns"
    id = Column(Integer, primary_key=True, autoincrement=True)
    match_id = Column(Integer, ForeignKey("matches.id", onupdate="cascade", ondelete="cascade"))
    no = Column(Integer, nullable=False)

    def __init__(self, match_id, no):
        self.match_id = match_id
        self.no = no

```

### model定義を参照する様に、migrations/env.py を編集

`migrations/env.py` を開くと、model の MetaData を定義する様にコメントが記載されているので、定義します。 

``` migrations/env.py
# ★before
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = None
```

　↓先程定義した model のメタデータを定義

```
# ★after
# add your model's MetaData object here
from db.models import Base
target_metadata = Base.metadata
```

### upgrade 作成

alembic revision でDB接続し、現在のDBのスキーマと `models.py`の内容を比較して、スキーマ最新化に必要なスクリプトを作成し、`migrations/versions` に保存します。

``` null.ps1
# alembic revision --autogenerate -m {任意のバージョン番号}
poetry run alembic revision --autogenerate -m v0.01
```

### upgrade 実行

alembic upgrade コマンドで、DBのスキーマを任意のバージョンにします。

``` null.ps1
poetry run alembic upgrade head
```

## おわりに

細切れ記事は色々あるのですが、まとめ記事はないのでまとめました。
これだけ色々なものを入れたにも関わらず、割と短い記事で書き終わって、随分便利な世の中になってるなぁと思いました。
Java で同じアウトプットの記事を書こうと思ったら eclipse のインストールや設定も細かく解説が必要で、@ITあたりで全10回構成の記事にする物量になりそうだなと、なぜかムーアの法則に思いをはぜました。