tag:crieit.net,2005:https://crieit.net/tags/SQLAlchemy/feed 「SQLAlchemy」の記事 - Crieit Crieitでタグ「SQLAlchemy」に投稿された最近の記事 2021-06-26T20:15:08+09:00 https://crieit.net/tags/SQLAlchemy/feed tag:crieit.net,2005:PublicArticle/17446 2021-06-26T20:15:08+09:00 2021-06-26T20:15:08+09:00 https://crieit.net/posts/pytest-sqlalchemy-alembic 📝 pytest で alembic のマイグレーションを行う方法 <h1 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h1> <p><a target="_blank" rel="nofollow noopener" href="https://fastapi.tiangolo.com/ja/">FastAPI</a> と <a target="_blank" rel="nofollow noopener" href="https://github.com/sqlalchemy/sqlalchemy">SQLAlchemy</a> を利用して Web API 開発を行っていた際、SQLAlchemy のマイグレーションツールである <a target="_blank" rel="nofollow noopener" href="https://github.com/sqlalchemy/alembic">alembic</a> を利用していました。</p> <p>ただ E2E テストを書こうとした際に、pytest 実行中に alembic でデータベースマイグレーションを行う方法が分からず模索していました。結果的にマイグレーションのやり方は分かったものの一応今後も利用するかもしれないため、その内容を記事として残しておくことにしました。</p> <p><strong>本記事内で利用しているソースコードを含む FastAPI プロジェクトを <a target="_blank" rel="nofollow noopener" href="https://github.com/nikaera/fastapi-sqlalchemy-alembic-pytest-sample">GitHub リポジトリ</a>上にアップしておいたので、詳細を確認されたい方がいればご参照くださいませ。</strong></p> <h1 id="alembic でマイグレーションを行う"><a href="#alembic+%E3%81%A7%E3%83%9E%E3%82%A4%E3%82%B0%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E8%A1%8C%E3%81%86">alembic でマイグレーションを行う</a></h1> <p><code>conftest.py</code> にグローバルで利用するマイグレーション用の <code>fixture</code> を定義すれば OK です。</p> <pre><code class="python"># conftest.py import os import alembic.config import pytest from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker from sqlalchemy_utils import database_exists, create_database, drop_database # テスト用の初期データを定義した module を import する (必要があれば) # from .seed import users, contents # 指定したパラメータを用いて alembic によるデータベースマイグレーションを行う # 引数のデフォルト設定では全てのマイグレーションを実行するようになっている def migrate(migrations_path, alembic_ini_path='alembic.ini', connection=None, revision="head"): config = alembic.config.Config(alembic_ini_path) config.set_main_option('script_location', migrations_path) if connection is not None: config.attributes['connection'] = connection alembic.command.upgrade(config, revision) # テスト実行用にセットアップされたデータベースのセッション情報を扱う関数 # scope に session を指定することでテスト全体で一回だけ実行されるようにする @pytest.fixture(scope="session", autouse=True) def SessionLocal(): test_sqlalchemy_database_url = os.environ['DATABASE_URL'] engine = create_engine(test_sqlalchemy_database_url) # 既にテスト用データベースが存在していたら破棄する if database_exists(test_sqlalchemy_database_url): drop_database(test_sqlalchemy_database_url) # テスト用データベースを作成する create_database(test_sqlalchemy_database_url) # 環境変数 DATABASE_URL で指定したデータベースに対して、 # マイグレーションを行いテスト実行に必要なテーブルを一括作成する # 第一引数に指定している alembic は `alembic init <環境名>` 実行時に指定した環境名を入力 with engine.begin() as connection: migrate("alembic", 'alembic.ini', connection) Base = declarative_base() Base.metadata.create_all(engine) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) # テスト用の各種データを追加する (必要があれば) # db_session = SessionLocal() # for user in users: # db_session.add(user) # db_session.commit() # for content in contents: # db_session.add(content) # db_session.commit() # db_session.close() # テスト用データ追加後のセットアップ済みの状態で # テスト用に利用する SessionLocal を返却する yield SessionLocal # テストが全て終わったら、テスト用データベースを破棄して、 # SQLAlchemy のセッションも切断する drop_database(test_sqlalchemy_database_url) engine.dispose() </code></pre> <h1 id="FastAPI の pytest への適用例"><a href="#FastAPI+%E3%81%AE+pytest+%E3%81%B8%E3%81%AE%E9%81%A9%E7%94%A8%E4%BE%8B">FastAPI の pytest への適用例</a></h1> <p>上記を関数を利用する方法は各自のテスト環境によって異なると思いますが、一応私が FastAPI のテストコードを書く際に利用したソースコードを元に参考例を載せておきます。</p> <p><code>conftest.py</code> と同じディレクトリに <code>client.py</code> を作成します。</p> <pre><code class="python"># client.py from fastapi import Header, HTTPException, status from fastapi.testclient import TestClient from app.dependencies import get_database from app.main import app # conftest で定義した fixture の SessionLocal を元に、 # データベースセッションを作成するための override_get_db 関数を定義して、 # get_database の代わりに override_get_db を実行するよう差し替える def temp_db(f): def func(SessionLocal, *args, **kwargs): def override_get_db(): db = SessionLocal() try: yield db finally: db.close() app.dependency_overrides[get_database] = override_get_db f(*args, **kwargs) app.dependency_overrides[get_database] = get_database return func client = TestClient(app) </code></pre> <p>あとは <code>pytest</code> のコード内で下記のような記述を行えば、FastAPI の内部でテスト用データベースを利用してくれるようになります。</p> <pre><code class="python"># test_main.py from fastapi import status from .client import client, temp_db # temp_db fixture を定義しておくことで、 # 関数の実行中は FastAPI の内部でテスト用データベースを利用する @temp_db def test_read_me_token_valid(): response = client.get("/users/me", headers={"Authorization": "Bearer 1234567890"}) assert response.status_code == status.HTTP_200_OK #... </code></pre> <h2 id="参考リンク"><a href="#%E5%8F%82%E8%80%83%E3%83%AA%E3%83%B3%E3%82%AF">参考リンク</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/bee2/items/ff9c86d8d345dbcab497#2-%E3%83%86%E3%82%B9%E3%83%88%E3%82%B1%E3%83%BC%E3%82%B9%E6%AF%8E%E3%81%ABdb%E3%82%92%E4%BD%9C%E6%88%90%E3%81%97%E4%BB%96%E3%81%AE%E3%83%86%E3%82%B9%E3%83%88%E3%82%B1%E3%83%BC%E3%82%B9%E3%82%84%E6%9C%AC%E7%95%AA%E7%94%A8%E3%81%AEdb%E3%81%AB%E5%BD%B1%E9%9F%BF%E3%82%92%E4%B8%8E%E3%81%88%E3%81%9A%E3%81%ABapi%E3%81%AEunittest%E3%82%92%E8%A1%8C%E3%81%86">FastAPI でテスト用のクリーンな DB を作成して pytest で API の Unittest を行う - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/_akiyama_/items/9ead227227d669b0564e">pytest:フィクスチャ(fixture)の使い方 - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://fastapi.tiangolo.com/tutorial/dependencies/">Dependencies - First Steps - FastAPI</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://docs.pytest.org/en/6.2.x/">pytest: helps you write better programs — pytest documentation</a></li> </ul> nikaera tag:crieit.net,2005:PublicArticle/14904 2019-04-05T22:33:52+09:00 2019-04-05T22:36:21+09:00 https://crieit.net/posts/SQLAlchemy-Declarative-API SQLAlchemy の Declarative API を使ってハマった事 <p>Python3 + Bottle + Jinja2 + SQLAlchemy で Web サービスを作っているのですが、SQLAlchemy の Declarative API を使い始めた時にちょっとした失敗をやらかしましたので、この辺りで供養しておきます。</p> <p>以下は SQLAlchemy の Declarative API を使って User クラス(users テーブル) を定義し、テーブルを生成するコードです。コードが膨れ上がるのが嫌いなので、User クラスは user.py に、テーブル生成は database.py に書いています。しかし、database.py の <code>create_all()</code> を読んでもテーブルが生成されない、という問題にぶつかりました。</p> <pre><code class="py"># user.py from sqlalchemy import Column from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.types import String Base = declarative_base() class User(Base): __tablename__ = "users" user_id = Column(UUID, primary_key=True) name = Column(String) : </code></pre> <pre><code class="py"># database.py from sqlalchemy.ext.declarative import declarative_base from .user import User Base = declarative_base() Base.metadata.create_all(bind=self.engine) # ここで users テーブルが作られているはずが、作られない </code></pre> <p>何が問題なのかというと、複数のソースに分けて書いた時の対応がまずかったのです。<br /> ネットでよく見かける Delcarative API の サンプルは同じ py ファイルの中に書いてあるので問題はありません。<br /> 上の例の場合、user.py と database.py の双方で declarative_base() を呼び出して基底クラス Base を使用していますが、互いに独立したメタクラスを触っているので、テーブル生成しようにも Users クラスで定義した内容が引き渡されていませんでした。</p> <p>解決方法としては以下のようになります。declarative_base(0 で生成した Base を引き回すため、独立した base.py を生成し、user.py を database.py の双方から参照するように修正しました。</p> <pre><code class="py"># base.py from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() </code></pre> <pre><code class="py"># user.py from .base import Base from sqlalchemy import Column from sqlalchemy.types import String class User(Base): __tablename__ = "users" : </code></pre> <pre><code class="py"># database.py from .base import Base from .user import User Base.metadata.create_all(bind=self.engine) # ここで users テーブルが作られる </code></pre> <p>以上の対応で、無事 users テーブルが生成されるようになりました。よかったよかった。</p> <p>しかしこの Declarative API 、実に ORM らしい使い方になるのでとても快適ですね。<br /> 当初 SQLAlchemy のリファレンスを適当に見ながら書いたものは Reflective Table Object という方法だったようで<br /> クエリの結果がdict(連想配列)で帰ってくるので PHP の MDB を使っていた頃とあまり使い勝手が変わらない感じでしたが、Declarative API だとちゃんとオブジェクトで帰ってくるのでとても良いです。</p> ともたこ