Reflex：DBマイグレーションの手順

# １．記事概要

この記事は、PythonだけでWebアプリを作ることができるライブラリ「Reflex」に関する記事です。

https://github.com/reflex-dev/reflex

今回は、DBマイグレーションの方法を整理しました。公式ドキュメントにある手順を少し詳しく説明しています。

https://reflex.dev/docs/database/overview/

Reflexのマイグレーションは、PythonのDBマイグレーションライブラリの「Almenbic」を間接的に利用しています。

https://alembic.sqlalchemy.org/en/latest/

また、ReflexのORMについては「SQLAlchemy」を利用しています。それ故、テーブル定義の詳細な設定をする際には「SQLAlchemy」の設定方法に即して行います。

https://www.sqlalchemy.org/

# ２．前提条件

手順の前提となる環境について説明します。

## （１）開発環境とフォルダ構成

- 開発環境は、VSCodeのDevContainerを利用して構築
- ReflexをはじめPythonライブラリはPoetryを利用してパッケージ管理
- 「project」フォルダを、DevContainerのワークスペースとする
- 「app」フォルダを、Reflexのプロジェクトフォルダとする

※DevContainerの構築方法はこの記事では省略

```sh:project-folder
project
  ├── .devcontainer
  ├── app # Reflexのアプリケーションソースを格納
  ├── poetry.lock
  ├── pyproject.toml
  └── README.md
```

## （２）Reflexのプロジェクトフォルダ（app）を初期化

以下の手順で初期化します。
アプリケーションの名前は「screen」とし、テンプレートはサイドメニューのあるものを採用します。

```sh:init-reflex-app

# Reflexのプロジェクトフォルダに移動
cd app

# Reflexの必要ファイルを作成
poetry run reflex init --name screen --template sidebar

```

上記の操作で以下のようにReflexのプロジェクトフォルダが初期化されます。
アプリ名として指定した「screen」フォルダ内に、アプリケーションのソースファイルが作成されます。
アプリ名と同じファイル名の「screen.py」がアプリケーションの実行起点となります。


```sh:app-folder
 app
  ├── .web # nodeやNext.jsなどのWebのフロントに関するモジュールやビルドソースが格納される
  ├── assets # 画像ファイルなどが格納される
  ├── screen # コーディングするファイルを格納するフォルダ
  │   ├── components
  │   ├── pages
  │   ├── templates
  │   ├── screen.py # アプリケーションの実行起点ソース
  │   └── styles.py
  ├── .gitignore
  ├── README.md
  ├── requirements.txt
  └── rxconfig.py # Reflexの設定ファイル

```

上記の初期化の手順ついては、公式ドキュメントの以下のページに記載があります。

https://reflex.dev/docs/getting-started/installation/

## （３）DBはDockerを利用して構築済みとする

docker-composeにてDBコンテナを定義し、コンテナ作成時にDBやアクセスユーザーは作成されます。
以下、定義のサンプルです。

```yml:docker-compose
services:
  db:
    image: mysql:8.0
    container_name: db
    restart: unless-stopped
    volumes:
      - db-data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: d_sample
      MYSQL_USER: db_user 
      MYSQL_PASSWORD: password
    networks:
      - app-net
    ports:
      - 3306:3306

volumes:
  db-data:

networks:
  app-net:
    driver: bridge

```


# ３．マイグレーション関連ファイルの作成

最初に、Reflexのプロジェクトフォルダ（app）で、以下のコマンドを実行します。

```sh
# Reflexのプロジェクトフォルダ（app）にて以下を実行
reflex db init 
```

上記のコマンドを実行すると、以下のTree図でコメントをつけたフォルダやファイルが作成されます。
作成されたフォルダやファイルはAlmenbicのリソースであり、これらの細かい説明は、Alembicに関するドキュメントを参照してください。

https://zenn.dev/shimakaze_soft/articles/4c0784d9a87751

```sh
 app
  ├── .web
  ├── alembic # このフォルダが追加
  │   ├── versions
  │   ├── env.py
  │   ├── README
  │   └── script.py.mako
  ├── assets
  ├── screen
  ├── .gitignore
  ├── alembic.ini # このファイルが追加
  ├── README.md
  ├── requirements.txt
  └── rxconfig.py

```

# 4. DBの接続情報の設定

Almenbicと接続方法は同じです。almebic.iniファイルに以下のように設定します。

```ini
sqlalchemy.url = mysql://db_user:password@db:3306/d_sample
```

# 5. モデルクラスの作成

RailsやLaravelなどのWebフレームワークと同じように、モデルクラスを利用してDBテーブルを作成します。

ただ、Reflexにはモデルを作成するコマンドはなく、作成場所も決まっていません。ですので、任意の場所にモデルクラスを作成してください。今回は、おそらく一般的であろう以下の場所に作成します。

```sh
 app
  ├── .web
  ├── alembic
  ├── assets
  ├── screen
  │   ├── components
  │   ├── models # modelsというフォルダを作成し、この中に格納
  │   │   └── user.py
  │   ├── pages
  │   ├── templates
  │   ├── screen.py
  │   └── styles.py
  ├── .gitignore
  ├── alembic.ini
  ├── README.md
  ├── requirements.txt
  └── rxconfig.py
```

モデルファイルを作成したら、モデルクラスの定義をします。
Reflexのモデルクラスを継承し、列名をクラスのプロパティとして定義してください。以下は、公式ドキュメントにある簡易的な定義方法です。

```py:user.py
import reflex as rx

# table=Trueでテーブル作成対象となる
class User(rx.Model, table=True):
    username: str
    email: str
    password: str
```

# 6. モデルクラスをReflexアプリ内でインポートする

:::note alert
ここで注意が必要です。これを知らないとマイグレーションファイルができずにハマってしまいます。
:::

マイグレーションする前に、必ずモデルファイルをアプリケーション内でインポートしておく必要があります。

何故このような手続きが必要かというと、Railsのようにモデルファイルが特定の場所にないため、インポートという手続きを通してモデルファイルの場所を知らせる必要があるのだと思います。

すでにページファイルにおいてモデルクラスをインポートしていれば問題ないです。もし、どのページで使うかわからない段階ならば、アプリケーションの起動ファイル内でインポートすればよいと思います。当記事の例では、「app/screen/screen.py」が該当します。

```py:screen.py
# Import all the pages.
from screen.pages import *
import reflex as rx

# ★ ここでインポート
from screen.models.user import User 

class State(rx.State):
    """Define empty state to allow access to rx.State.router."""

# Create the app.
app = rx.App()


```

:::note alert
アプリからimportされなくなると、drop tableされてしまうので、一度つくったモデルのimport文は必ず残しておく。
:::


:::note warn
正直、この仕様はやや微妙な気もします。Railsみたいにモデルを格納する場所をフレームワークで決めて、そこにコマンドを入力して作成できればよいと思います。ただ、Reflexはまだメジャーバージョン(2024.07.13時点ではv0.5.6）ではないので、そのうちこうした対応もされるのかもしれません。すごく不便ではないですが、知らないと結構ハマります。
:::

# 7. マイグレーションファイルの作成

次にマイグレーションファイルを作成します。
Reflexのプロジェクトフォルダ(appフォルダ）にて、以下のコマンドを実行します。
「--message」はファイル名に付与されるラベルを指定します。和名よりは半角英数字にしておくのがよいと思います。

```sh
reflex db makemigrations --message 'create-user'

# 成功すると以下のようにメッセージが表示される
 Generating /<プロジェクトルートパス>/app/alembic/versions/bc3c7b5d443e_create_user.py ...  done
```

上記の手順で以下のように、「alembic/versions」フォルダ以下に、マイグレーションファイルが作成されます。

```sh
 app
  ├── .web
  ├── alembic
  │   ├── versions # ここに作成される。
  │   │   └── bc3c7b5d443e_create_user.py # 作成されたマイグレーションファイル
  │   ├── env.py
  │   ├── README
  │   └── script.py.mako
  ├── assets
  ├── screen
  │   ├── components
  │   ├── models
  │   │   └── user.py # モデルファイル
  │   ├── pages
  │   ├── templates
  │   ├── screen.py # モデルファイルをインポート
  │   └── styles.py
  ├── .gitignore
  ├── alembic.ini
  ├── README.md
  ├── requirements.txt
  └── rxconfig.py
```

# 8.マイグレーションの実行

以下のコマンドでマイグレーションが実行できます。

```sh
reflex db migrate
```

# 9. ロールバック、アップグレード

テーブル定義の状態を戻したり、最新や特定の状態にしたい場合は、reflexのコマンドではなくalembicのコマンドで行います。
reflexのマイグレートを行ったフォルダ（alembic.iniが存在する）にて以下のコマンドを実行するとロールバック、アップグレードができます。この例では、「app」フォルダが該当します。

コマンドの詳細は、公式リファレンスや以下の記事をご確認ください。

https://alembic.sqlalchemy.org/en/latest/api/commands.html#alembic.command.show

https://zenn.dev/shimakaze_soft/articles/4c0784d9a87751

```sh

# マイグレーションの履歴：revisionを表示（ここで表示されたrevisionをつかって特定の状態に変更する）
alembic history

# 初期状態にロールバック
alembic downgrade base

# 一つ前ににロールバック
alembic downgrade -1

# 特定のrevisionにロールバック
alembic downgrade 3f1cb7f09b7a

# 最新までアップグレード
alembic upgrade head

# 1つ進める
alembic upgrade +1

# 特定のrevisionまで進める
alembic upgrade 3f1cb7f09b7a

```


# 10. 補足：詳細なテーブル定義

テーブル定義が簡易的でよければ上記のような簡単なモデル定義で十分だと思います。
ただ、データ型、サイズ、コメントをつけたりなどをしたい場合、SQLAlchemyの設定情報を与える必要があります。

たとえば、以下のようにすることで、データ型、サイズ、コメントを付けることができます。その他の設定についても、SQLAlchemyのパラメーターを参考に設定すればよいと思います。
こちらも公式に記載はあります。

https://reflex.dev/docs/database/tables/#advanced-column-types

```py:crew.py

import reflex as rx
from typing import Any
import sqlmodel as model
import sqlalchemy as sql
from sqlalchemy.types import BIGINT, BLOB, VARCHAR
from sqlalchemy.sql.sqltypes import TypeEngine

"""
列の情報を簡便に記載するための関数
"""
def attr(
    label: str, 
    data_type: TypeEngine, 
    is_primary_key: bool = False
) -> Any:

    # sa_colmunがSQLAlchemyのパラメーター。ここで細かいDBの定義情報を設定
    return model.Field(
        sa_column=
            sql.Column(
                    data_type, 
                    comment=label,
                    primary_key=is_primary_key, 
                    autoincrement=is_primary_key
                )
        )

"""
モデルクラス
"""
class Crew(rx.Model, table=True):
    __tablename__ :str = "crews" # クラス名と別のテーブル名（SQLAlchemyと同じ仕様）
    id: int = attr('担当者ID', BIGINT(), True)
    crew_department_id: int = attr('所属部署ID', BIGINT())
    name: str = attr('名前', VARCHAR(30))
    email: str = attr('メールアドレス', VARCHAR(255))
    password: str = attr('passowrd', BLOB())


```

上記のコードで、以下のようにテーブルが作成されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15972/36dce791-c75b-e821-d06f-adce84ce6e92.png)

:::note alert
テーブル名にアンダースコアがある場合は、必ず「__tablename__」で指定しておく必要があります。そうしないと外部制約をつけた場合にテーブル名がないというエラーが表示されます。クラス名が、パスカルケースであっても、スネークケースには変換されません。
:::


```py:crew_department
class CrewDepartment(rx.Model, table=True):
    __tablename__ :str = "crew_department" # アンダースコアがある場合は必ずテーブル名をつけておく。そうしないと外部制約など連携でエラーになる。
    
    id: int = attr('部署ID', BIGINT(), True)
　　name: str = attr('部署名', VARCHAR(30))


```

テーブル定義を外部制約を考慮して、指定の順番で行う場合は、Reflexだけではできません。
alembic/env.pyを以下の記事のように編集すれば指定の順番でテーブル定義が作成されます。

https://qiita.com/SFITB/items/ab93682bb42cdf05de91



# 10. さいごに：困ったときの検索・質問先

検索や質問方法について、別記事に整理しました。参考になればと思います。

https://qiita.com/SFITB/items/ae710653bbe626c2e953

ちなみに、Reflexのdiscordサーバは以下のリンクから参加できます。

https://discord.gg/DeWEstSxZy