投稿者

インターシステムズジャパン
記事 Toshihiko Minamoto · 5月 27 14m read

SQL データ移行を使用した Python による IRIS を使った REST Api

次回の Python コンテストでは、Python を使用して IRIS をデータベースとして使用する簡単な REST アプリケーションを作成する方法についての小さなデモを作成しようと思います。 以下のツールを使用します。

  • FastAPI フレームワーク: 高パフォーマンス、学習しやすい、高速コーディング、プロダクション対応
  • SQLAlchemy: Python SQL ツールキットで、アプリケーション開発者が SQL の全性能と柔軟性を活用できるオブジェクトリレーションマッパーです。
  • Alembic: Python 用の SQLAlchemy データベースツールキットと使用する軽量のデータベース移行ツール。
  • Uvicorn: Python の ASGI ウェブサーバー実装。

環境の準備

バージョン 3.7 以降の Python がすでにインストール済みだと思います。 プロジェクトフォルダを作成し、その中に以下のコンテンツで requirements.txt ファイルを作成します。

fastapi==0.101.1
alembic==1.11.1
uvicorn==0.22.0
sqlalchemy==2.0.20
sqlalchemy-iris==0.10.5

Python で仮想環境を使用することをお勧めします。新しい環境を作成して有効化しましょう。

python -m venv env && source env/bin/activate

そして、依存関係をインストールします。

pip install -r requirements.txt

クイックスタート

FastAPI を使って最も単純な REST Api を作成しましょう。 これを行うには、app/main.py を作成します。

from fastapi import FastAPI

app = FastAPI( title='TODO Application', version='1.0.0', )

@app.get("/ping")asyncdefpong():return {"ping": "pong!"}

この時点で、アプリケーションを純分に起動して動作させることができます。 サーバーの起動には、uvicorn を使用します。

$ uvicorn app.main:app         
INFO:     Started server process [94936]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

ping リクエストを発行できます。

$ curl http://localhost:8000/ping
{"ping":"pong!"}

FastAPI には API をテストできる UI が用意されています。

Docker 化環境

IRIS をアプリケーションに追加するには、コンテナーを使用します。 IRIS イメージはそのままで使用できますが、Python アプリケーション用の Docker イメージを作成する必要があります。 また、Dockerfile が必要です。

FROM python:3.11-slim-buster

WORKDIR /usr/src/app RUN --mount=type=bind,src=.,dst=.
pip install --upgrade pip &&
pip install -r requirements.txt
COPY . . ENTRYPOINT [ "/usr/src/app/entrypoint.sh" ]

コンテナー内でアプリケーションを起動するには、簡単な entrypoint.sh が必要です。

#!/bin/sh
# Run SQL Migrations, to make DB Schema up to date
alembic upgrade head

# Start Python application uvicorn app.main:app
--workers 1
--host 0.0.0.0
--port 8000 "$@"

実行フラグを忘れずに追加しましょう。

chmod +x entrypoint.sh

docker-compose.yml で IRIS と組み合わせます。

version:"3"services:  iris:    image:intersystemsdc/iris-community    ports:      -1972    environment:      -IRISUSERNAME=demo      -IRISPASSWORD=demo    healthcheck:      test:/irisHealth.sh      interval:5s  app:    build:.    ports:      -8000:8000    environment:      -DATABASE_URL=iris://demo:demo@iris:1972/USER    volumes:      -./:/usr/src/app    depends_on:      iris:        condition:service_healthy    command:      ---reload

ではビルドしましょう。

docker-compose build

最初のデータモデル

アプリケーションに IRIS データベースへのアクセスを宣言し、app/db.py ファイルを追加しましょう。このファイルによって データベースにアクセスできるように SQLAlchemy が構成されます。これは docker-compose.yml によって渡される URL で定義されます。これには後でアプリで使用するハンドラーがいくつか含まれています。

import os

from sqlalchemy import create_engine from sqlalchemy.ext.declarative import DeclarativeMeta, declarative_base from sqlalchemy.orm import sessionmaker

DATABASE_URL = os.environ.get("DATABASE_URL") ifnot DATABASE_URL: DATABASE_URL = "iris://demo:demo@localhost:1972/USER" engine = create_engine(DATABASE_URL, echo=True, future=True)

Base: DeclarativeMeta = declarative_base()

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

definit_db(): engine.connect()

defget_session(): session = SessionLocal() yield session

では、初の唯一のモデルをアプリケーションに定義しましょう。 ファイル app/models.py を作成して編集します。SQLAlchemy を使用して、Todo と言う、id、title、description の 3 つ列を持つモデルを定義します。

from sqlalchemy import Column, Integer, String, Text
from app.db import Base

classTodo(Base):tablename = 'todo' id = Column(Integer, primary_key=True, index=True) title = Column(String(200), index=True, nullable=False) description = Column(Text, nullable=False)

SQL の移行の準備

変化し続ける世界では、アプリケーションは将来的に改善されると考えるため、テーブル構造は最終的なものではなく、さらにテーブル、列、インデックスなどを追加できるとわかっています。 この場合の最善のシナリオは、SQL Migration ツールを使用することです。これは、アプリケーションのバージョンに応じてデータベースの現在の構造をアップグレードできるツールであり、何かが誤ってしまった場合には、これらのツールを使用することでダウングレードすることもできます。 このプロジェクトでは Python と SQLAlchemy を使用していますが、SQLAlchemy の作者は Alembic というツールを提供しており、ここではそれを使用します。

IRIS と、アプリケーションを含むコンテナーを起動する必要がありますが、この時点では、コマンドを実行できるように Bash する必要があります。

$ docker-compose run --entrypoint bash app
[+] Creating 2/0
 ✔ Network fastapi-iris-demo_default   Created                                                                                                                                                        0.0s 
 ✔ Container fastapi-iris-demo-iris-1  Created                                                                                                                                                        0.0s 
[+] Running 1/1
 ✔ Container fastapi-iris-demo-iris-1  Started                                                                                                                                                        0.1s 
root@7bf903cd2721:/usr/src/app# 

コマンド alembic init app/migrations を実行します。

root@7bf903cd2721:/usr/src/app# alembic init app/migrations
  Creating directory '/usr/src/app/app/migrations' ...  done
  Creating directory '/usr/src/app/app/migrations/versions' ...  done
  Generating /usr/src/app/app/migrations/README ...  done
  Generating /usr/src/app/app/migrations/script.py.mako ...  done
  Generating /usr/src/app/app/migrations/env.py ...  done
  Generating /usr/src/app/alembic.ini ...  done
  Please edit configuration/connection/logging settings in '/usr/src/app/alembic.ini' before proceeding.
root@7bf903cd2721:/usr/src/app#

これにより Alembic 構成が準備されたため、アプリケーションのニーズに適合するように修正する必要があります。 これを行うには app/migrations/env.py ファイルを編集します。 これはファイルの始まりに過ぎないため、更新する必要があります。sqlalchemy.urltarget_metadata を更新することに専念しましょう。 その以下の変更はありません。

import os
import urllib.parse
from logging.config import fileConfig

from sqlalchemy import engine_from_config from sqlalchemy import pool

from alembic import context

# this is the Alembic Config object, which provides# access to the values within the .ini file in use. config = context.config

DATABASE_URL = os.environ.get("DATABASE_URL")

decoded_uri = urllib.parse.unquote(DATABASE_URL) config.set_main_option("sqlalchemy.url", decoded_uri)

# Interpret the config file for Python logging.# This line sets up loggers basically.if config.config_file_name isnotNone: fileConfig(config.config_file_name)

# add your model's MetaData object here# for 'autogenerate' supportfrom app.models import Base target_metadata = Base.metadata # target_metadata = None

すでにモデルが存在するため、コマンド alembic revision --autogenerate で移行を作成する必要があります。

root@7bf903cd2721:/usr/src/app# alembic revision --autogenerate
INFO  [alembic.runtime.migration] Context impl IRISImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added table 'todo'
INFO  [alembic.autogenerate.compare] Detected added index 'ix_todo_id' on '['id']'
INFO  [alembic.autogenerate.compare] Detected added index 'ix_todo_title' on '['title']'
  Generating /usr/src/app/app/migrations/versions/1e4d3b4d51ca_.py ...  done
root@7bf903cd2721:/usr/src/app# 
 
生成された移行を見てみましょう。

では、これをデータベースに適用しましょう。コマンド alembic upgrade head を使用します。ここで、head は最新バージョンにアップグレードするためのキーワードです。

root@7bf903cd2721:/usr/src/app# alembic upgrade head
INFO  [alembic.runtime.migration] Context impl IRISImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade  -> 1e4d3b4d51ca, empty message
 
Downgrade
アプリケーションのアップグレード中に元に戻す必要があることが分かった場合に、例えば 1 つ前のリビジョンにデータベースをダウングレードするには head-1 とします。
root@7bf903cd2721:/usr/src/app# alembic downgrade head-1
INFO  [alembic.runtime.migration] Context impl IRISImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running downgrade 1e4d3b4d51ca -> , empty message

and to completely downgrade back to an empty state, use keyword base

現在の状態をいつでも確認できます。いくつかの移行が欠落している場合にはその情報を得られます。

root@7bf903cd2721:/usr/src/app# alembic check
INFO  [alembic.runtime.migration] Context impl IRISImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
No new upgrade operations detected.

データをアクセス可能にする

REST に戻り、実行させる必要があります。現在のコンテナーを終了し、アプリサービスを通常どおり実行すると、uvicorn には --reload フラグが設定されているため、Python ファイル内の変更をチェックし、変更がある場合には再起動されます。

$ docker-compose up app
[+] Running 2/0
 ✔ Container fastapi-iris-demo-iris-1  Running                                                                                                                                                        0.0s 
 ✔ Container fastapi-iris-demo-app-1   Created                                                                                                                                                        0.0s 
Attaching to fastapi-iris-demo-app-1, fastapi-iris-demo-iris-1
fastapi-iris-demo-app-1   | INFO  [alembic.runtime.migration] Context impl IRISImpl.
fastapi-iris-demo-app-1   | INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
fastapi-iris-demo-app-1   | INFO:     Will watch for changes in these directories: ['/usr/src/app']
fastapi-iris-demo-app-1   | INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
fastapi-iris-demo-app-1   | INFO:     Started reloader process [8] using StatReload
fastapi-iris-demo-app-1   | INFO:     Started server process [10]
fastapi-iris-demo-app-1   | INFO:     Waiting for application startup.
fastapi-iris-demo-app-1   | INFO:     Application startup complete.

FastAPI は Pydantic プロジェクトを使用してデータスキーマを宣言しているため、それも必要です。app/schemas.py を作成しましょう。列は models.py と同じですが、単純な Python フォームを使用します。

from pydantic import BaseModel

classTodoCreate(BaseModel): title: str description: str

classTodo(TodoCreate): id: int

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Config</span>:</span>
    from_attributes = <span class="hljs-keyword">True</span>

SQLAlchemy ORM を使ってデータベースを操作する app/crud.py で CRUD 操作を宣言します。

from sqlalchemy.orm import Session
from . import models, schemas

defget_todos(db: Session, skip: int = 0, limit: int = 100):return db.query(models.Todo).offset(skip).limit(limit).all()

defcreate_todo(db: Session, todo: schemas.TodoCreate): db_todo = models.Todo(**todo.dict()) db.add(db_todo) db.commit() db.refresh(db_todo) return db_todo

そして最後に、app/main.py を更新し、ToDo を読み取って作成するルートを追加できます。

from fastapi import FastAPI, Depends
from .db import init_db, get_session
from . import crud, schemas

app = FastAPI( title='TODO Application', version='1.0.0', )

@app.on_event("startup")defon_startup(): init_db()

@app.get("/ping")asyncdefpong():return {"ping": "pong!"}

@app.get("/todo", response_model=list[schemas.Todo])asyncdefread_todos(skip: int = 0, limit: int = 100, session=Depends(get_session)): todos = crud.get_todos(session, skip=skip, limit=limit) return todos

@app.post("/todo", response_model=schemas.Todo)asyncdefcreate_todo(todo: schemas.TodoCreate, session=Depends(get_session)):return crud.create_todo(db=session, todo=todo)

これに応じてドキュメント ページが更新され、実際に操作できるようになりました。

 
試してみましょう。
新しい ToDo を追加します。

その内容を確認しましょう。

IRIS で確認してみましょう。

─$ docker-compose exec iris irissqlcli iris+emb:///
Server:  IRIS for UNIX (Ubuntu Server LTS for ARM64 Containers) 2023.2 (Build 227U) Mon Jul 31 2023 17:43:25 EDT
Version: 0.5.4
[SQL]irisowner@/usr/irissys/:USER> .tables
+-------------------------+
| TABLE_NAME              |
+-------------------------+
| SQLUser.alembic_version |
| SQLUser.todo            |
+-------------------------+
Time: 0.043s
[SQL]irisowner@/usr/irissys/:USER> select * from todo
+----+-------+---------------------+
| id | title | description         |
+----+-------+---------------------+
| 1  | demo  | it's really working |
+----+-------+---------------------+
1 row in set
Time: 0.004s
[SQL]irisowner@/usr/irissys/:USER> select * from alembic_version
+--------------+
| version_num  |
+--------------+
| 1e4d3b4d51ca |
+--------------+
1 row in set
Time: 0.045s
[SQL]irisowner@/usr/irissys/:USER>

 

REST の作成において Python と FastAPI を簡単に使用していただけたなら幸いです。 このプロジェクトのソースコードは、GitHub の https://github.com/caretdev/fastapi-iris-demo にあります。