33
34

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

爆速PythonフレームワークFastAPIをMySQLに繋いでRESTfulなAPIを作成してみた。

Last updated at Posted at 2020-09-24

FastAPI とは?

68747470733a2f2f666173746170692e7469616e676f6c6f2e636f6d2f696d672f6c6f676f2d6d617267696e2f6c6f676f2d7465616c2e706e67.png

FastAPIとはpython3.6以上を対象とした、APIを作成するためのモダンで処理速度が爆速なフレームワークです。

主な特徴としては

  • 速さ: 非常にハイパフォーマンス。NodeJSやGoに匹敵する処理速度を誇る(StarletteとPydanticのおかげもあり)。Pythonの数あるフレームワークのうち最も処理速度が速いフレームワークの一つ。

  • コードの簡素化: コードの書く速度を約2~3倍から上昇させる。(*)

  • バグの少なさ: 約40%ほどの人為的コードバグを減らすことが可能。(*)

  • 直感的に書ける: エディターのサポートも充実、補完も効きます。デバッグにかかる時間を減らすことが可能。

  • 簡単: 簡単に書け、理解しやすいように設計されている。ドキュメント読むのに時間がたくさんかかる心配もない。

  • 短い: コードの重複を避けることができる。渡す引数を変えるだけで様々な機能を提供する関数を備えている。

  • 堅実: 本番環境でも開発環境と差異のないコードを使える。

  • Swaggerの提供: 作成したAPIはデフォルトで備えているSwaggerをもとに自動でドキュメント化され、各処理を実行できる。

(*)FastAPI制作チーム調べ、だそうです。

#とりあえずサーバーを立ち上げてみる

「コードは言葉より物を言う」と言うことで、早速使っていきたいと思います。

まずは適当にフォルダを作り

mkdir fastapi-practice

必要なパッケージをインストールします。

pip install fastapi sqlalchemy uvicorn mysqlclient

グローバルインストールが嫌な方はpoetryなどを使ってインストールしてください(このあとでどのみちpoetry使います)。

FastAPIを動かすために必要な以下のファイルを作成します

touch main.py

そして以下の様にコードを記述していきます。

main.py
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def index():
    return {'Hello': 'World'}

なんとこれだけでサーバーが立ち上がってしまいます。

uvicorn main:app

と打つだけでサーバーが立ち上がったはずです。
http://localhost:8000/
をブラウザで表示してみると{"Hello":"World"}と表示されているはずです。

爆速ですね、FastAPI。
しかもSwaggerによりAPIの仕様も自動で作成されています!!(驚き!)
http://localhost:8000/docs
を表示してみてください。お洒落なUIで仕様書が作られているはずです。

タブを開いてTry it outのボタンを押すと実際にリクエストを送り、レスポンスを確認することだってできてしまいます!(感動!!)

スクリーンショット 2020-09-24 14.05.22.png

ちなみに
http://localhost:8000/redoc
も自動で作られておりさらに詳細なドキュメントを簡単に作ることもできてしまいます!(凄すぎ!!!)

実際に使ってみる (docker環境構築編)

それではリアルケースを想定したFastAPI + MySQLでRESTfulなAPIを作成してみましょう。

FastAPIはdockerで簡単に環境構築できるのでmysqlとFastAPIをそれぞれ今時っぽくコンテナ内で動かし、通信させる様にしてみます。

まずはフォルダ内にdocker-compose.ymlとdocker-sync.yml, Dockerfileを作成します。

touch docker-compose.yml docker-sync.yml Dockerfile

dockerの詳しい使い方の説明はここでは省きますが、Dockerfileにコンテナを作成するための情報、docker-compose.ymlに作成されたコンテナ上で走らせるコマンド、docker-sync.ymlでローカルの開発環境とdockerコンテナ内のファイルをリアルタイムで同期するためにコードをつらつらと書いていきます。

docker-syncの使い方は他の方が書いてくださった以下の様な記事が参考になるかと思うので読んでみてください。
docker-syncは使わなくてもできますが、同期速度を爆速!にするために私は使ってます。

それではDockerfileからコードを書いていきます。

Dockerfile
FROM python:3.8-alpine
RUN apk --update-cache add python3-dev mariadb-dev gcc make build-base libffi-dev libressl-dev
WORKDIR /app
RUN pip install poetry

パッケージ管理にはpoetryを使います。
パッケージ管理にはpipenvやpyflowなどもあるのでここは好みですかね...?

https://qiita.com/sk217/items/43c994640f4843a18dbe
こちらの記事に各パッケージマネージャーが分かりやすくまとめられています。
気になる方は是非一読してみてください。

続いてdocker-sync.yml

docker-sync.yml
version: "2"
options:
  verbose: true
syncs:
  fastapi-practice-sync:
    src: "."
    notify_terminal: true
    sync_strategy: "native_osx"
    sync_userid: "1000"
    sync_excludes: [".git", ".gitignore", ".venv"]

そしてdocker-compose.ymlです

docker-compose.yml
version: "3"
services:
  db:
    image: mysql:latest
    command: --default-authentication-plugin=mysql_native_password
    restart: always
    environment:
      MYSQL_DATABASE: fastapi_practice_development
      MYSQL_USER: root
      MYSQL_PASSWORD: "password"
      MYSQL_ROOT_PASSWORD: "password"
    ports:
      - "3306:4306"
    volumes:
      - mysql_data:/var/lib/mysql
  fastapi:
    build:
      context: .
      dockerfile: "./Dockerfile"
    command: sh -c "poetry install && poetry run uvicorn main:app --reload --host 0.0.0.0 --port 8000"
    ports:
      - "8000:8000"
    depends_on:
      - db
    volumes:
      - fastapi-sync:/app:nocopy
      - poetry_data:/root/.cache/pypoetry/

volumes:
  mysql_data:
  poetry_data:
  fastapi-sync:
    external: true

docker-compose.ymlでミソなのが永続化するデータとその場かぎりのデータをうまく使い分けることです。
永続化しないデータはdocker-compose downするたびにリセットされてしまいます。

今回のケースだとmysql内のデータ、poetryでインストールしたパッケージは永続化させ、コンテナを立ち上げる度にmysql内のデータが空になったりパッケージをダウンロードしなくていい様にします。
また書いていくコードはdocker-syncを使って同期させたいのでfastapi-practice-sync:/app:nocopyを記述して勝手に同期されるのを防ぎます。

MySQLもdockerから最新のイメージをpullして構築していきます。

ここまででdockerのセットアップは終了です。

#実際に使ってみる(FastAPI設定編)

まずはFastAPIに必要なパッケージをインストールするpoetryのセットアップです。

poetry init

をターミナルで叩きます。
そうすると対話形式でセットアップが始まりますのでyesかnoを連打しましょう。(基本デフォルトの設定で問題ないのでEnter連打でも問題ない...と思います)

そうするとpyproject.tomlというファイルが作成されたかと思います。

ここにパッケージの依存情報が追記されていくのでpoetryを使ってFastAPIを立ち上げるのに必要なパッケージをインストールしていきましょう。

poetry add fastapi sqlalchemy uvicorn mysqlclient

こちらを入力しパッケージのインストールが終わるのを待ちます。
終了したら、pyproject.tomlを開いてみるとインストールされたパッケージの情報が記載されていることがわかります。

pyproject.toml

[tool.poetry]
name = "fastapi-practice"
version = "0.1.0"
description = ""
authors = ["Your Name <you@example.com>"]

[tool.poetry.dependencies]
python = "^3.8"
fastapi = "^0.61.1"
sqlalchemy = "^1.3.19"
uvicorn = "^0.11.8"

[tool.poetry.dev-dependencies]

[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"

そうしたらあとはdocker-compose buildを打ち、イメージをビルドしてdocker-sync-stack startを入力するだけです!

*docker-sync-stack startdocker-sync startdocker-compose upを同時に実行するコマンドです。ログもよしなに出してくれるので便利です。

新たにパッケージをインストールする際は、まずはローカルでpoetry addでパッケージをインストールしてdockerコンテナを再起動させればコンテナ内にも同期されるはずです!

マイグレーションをする

お次はDB(MySQL)と連携させていきます。

今回はCRUDの勉強といえばTodoリストの作成!なのでTodoテーブルを定義しマイグレーションをかけていくことにします。

Todos Table

|column |datatype
|---|---|---|
|id |integer
|title |string
|content |string
|done |boolean

この様な構成のテーブルをマイグレーションしていきます。

まずはデータベースを定義するファイルを作り

touch db.py

以下の内容を書き込んでいきます。

db.py

from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship, sessionmaker, scoped_session

user_name = "root"
password = "password"
host = "db"
database_name = "fastapi_practice_development"

DATABASE = f'mysql://{user_name}:{password}@{host}/{database_name}'

engine = create_engine(
    DATABASE,
    encoding="utf-8",
    echo=True
)

Base = declarative_base()

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


class Todo(Base):
    __tablename__ = 'todos'
    id = Column(Integer, primary_key=True, autoincrement=True)
    title = Column(String(30), nullable=False)
    content = Column(String(300), nullable=False)
    done = Column(Boolean, default=False)


def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()


def main():
    Base.metadata.drop_all(bind=engine)
    Base.metadata.create_all(bind=engine)


if __name__ == "__main__":
    main()

FastAPIはsqlalchemyというPythonの中で最もよく利用されるORM(Object-Relation Mapping)の一つを使ってデータベースとPythonのオブジェクトを関連付けるのが主流みたいです。

これを書いたらdockerのコンテナ内に入り、マイグレーションをかけていきます。

docker-sync-stack start

でコンテナを立ち上げ同期モードにし、

docker container ls

で立ち上がっているコンテナリストをみます。
そしたら、

docker exec -it {コンテナ名} sh

を叩き、コンテナの中に入ります。
そして、以下のコマンドでマイグレーションをかけていきます。

poetry run python db.py

そうすると無事マイグレーションが走りテーブル作成に成功したのではないでしょうか!

そしたら次はCRUD処理をFastAPIで書いていきます。

FastAPIでCRUD処理を書こう

拡張性を意識してFastAPIに内蔵されているinclude_routerという機能を使い、ファイルを分割していきます。

mkdir routers

と打ち、

touch routers/todo.py

というファイルを作ります。
こちらにCRUD処理を書いていきます。

routers/todo.py
from fastapi import Depends, APIRouter
from sqlalchemy.orm import Session
from starlette.requests import Request
from pydantic import BaseModel
from db import Todo, engine, get_db

router = APIRouter()


class TodoCreate(BaseModel):
    title: str
    content: str
    done: bool


class TodoUpdate(BaseModel):
    title: str
    content: str
    done: bool


@router.get("/")
def read_todos(db: Session = Depends(get_db)):
    todos = db.query(Todo).all()
    return todos


@router.get("/{todo_id}")
def read_todo_by_todo_id(todo_id: int, db: Session = Depends(get_db)):
    todo = db.query(Todo).filter(Todo.id == todo_id).first()
    return todo


@router.post("/")
def create_todo(todo: TodoCreate,  db: Session = Depends(get_db)):
    db_todo = Todo(title=todo.title,
                   content=todo.content, done=todo.done)
    db.add(db_todo)
    db.commit()


@router.put("/{todo_id}")
def update_todo(todo_id: int, todo: TodoUpdate, db: Session = Depends(get_db)):
    db_todo = db.query(Todo).filter(Todo.id == todo_id).first()
    db_todo.title = todo.title
    db_todo.content = todo.content
    db_todo.done = todo.done
    db.commit()


@router.delete("/{todo_id}")
def delete_todo(todo_id: int, db: Session = Depends(get_db)):
    db_todo = db.query(Todo).filter(Todo.id == todo_id).first()
    db.delete(db_todo)
    db.commit()

この様にしてざっとCRUD操作を書いていきました。
@routerの後にリクエスト名を書き、動作対象のURLを書くだけです。

そしたらこちらを読み込める様にmain.pyも編集していきます。

main.py
from fastapi import FastAPI
from routers import todos

app = FastAPI()


app.include_router(
    todos.router,
    prefix="/todos",
    tags=["todos"],
    responses={404: {"description": "Not found"}},
)

prefixはurlのパスを作ってくれます。tagsはdocsを見やすい様グルーピング化してくれます。

そうして
http://localhost:8000/docs
に接続すると以下の様になっているはずです!

スクリーンショット 2020-09-24 14.53.20.png

タブを開いてポチポチボタンを押してCRUD処理を試してみてください!

このままだとフロントエンドから呼ぶ際CORSエラーが起きるので別アプリから呼ぶ際は以下のCORSの処理を追記してみてください。

main.py
# 追記
from starlette.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

#まとめ

FastAPIいかがだったでしょうか?
こんなにも少ないコード量でAPIが作成できてしまうのがとても魅力的ですね。
Pythonでマイクロサービスを作成する際とても相性が良さそうです。

Qiita初投稿だったため何か分かりづらい点あったら質問くださいませ!
これからはなるべくQiitaにもアウトプットしていきたい...です(頑張る

33
34
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
33
34

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?