グロースエクスパートナーズ Advent Calendar 2021へ投稿        FastAPI+MYSQLの「環境構築及びAPI機能」について

はじめに

ジーアールソリューションズの金と申します。
本記事はグロースエクスパートナーズアドベントカレンダー 23日目の記事です。

現在、プライベートクラウドサービスでOSSを利用したIaaS/PaaSの開発を担当しています。
今年、APIサーバーを利用する案件にてFastAPIを採択して開発を行う機会がありました。
利用するに当たり調査を始めたところ、FastAPIの情報が比較的少ないことが分かりましたので調査内容をまとめておきたいとおもいます。
今回記載する内容は、FastAPIを構築し、API機能を利用してDatabase(CRUD)の制御方法について紹介したいと思います。

FastAPIとは

Python3.6バージョンから提供されるモダンで、高速(高パフォーマンス)なAPI を構築するためのWeb フレームワークです。一般的な特徴は以下の通りです。

・高速パフォーマンス :NodeJS及びGoと同等な性能、現存している高速なPython Web フレームワークの一つです。
・高速なコーディング:開発速度向上されます。
・少ないバグ:開発者によるHuman Error減少します。
・直感的:デバッグが簡単です。
・簡単:FastAPIは使いやすいし、ドキュメントも読みやすく簡単に記載されてます。
・堅牢性: 自動対話ドキュメントを使用して、本番環境で使用できるコードを取得します。
・Standards-based: API のオープンスタンダードに基づいており、完全に互換性があります。(Swaggerドキュメントの自動生成)

※参考
https://fastapi.tiangolo.com/

FastAPI構成

FastAPI構成としてフロントエンド側にはReactを、バックエンド側にはFastAPIとMysqlを利用しましたが、今回の投稿にはバックエンド側を重点的にご説明したいと思います。

FastAPI構築

FastAPI構築における「環境準備、構築手順」をご説明します。

環境準備

①Centos 7
②Python 3.6+
③mysql 8.0

構築手順

①pythonバージョンを確認

$ python -V
Python 3.9.5

②pipバージョン確認

$ pip -V
pip 21.3.1 from /usr/local/lib/python3.9/site-packages/pip (python 3.9)

③fastapiインストール

$ pip install fastapi
Collecting fastapi
  Downloading fastapi-0.70.1-py3-none-any.whl (51 kB)
     |████████████████████████████████| 51 kB 1.4 MB/s             
Collecting starlette==0.16.0
  Downloading starlette-0.16.0-py3-none-any.whl (61 kB)
     |████████████████████████████████| 61 kB 534 kB/s             
Collecting pydantic!=1.7,!=1.7.1,!=1.7.2,!=1.7.3,!=1.8,!=1.8.1,<2.0.0,>=1.6.2
  Downloading pydantic-1.8.2-cp39-cp39-manylinux2014_x86_64.whl (11.3 MB)
     |████████████████████████████████| 11.3 MB 24.2 MB/s            
Requirement already satisfied: anyio<4,>=3.0.0 in /usr/local/lib/python3.9/site-packages (from starlette==0.16.0->fastapi) (3.3.4)
Collecting typing-extensions>=3.7.4.3
  Downloading typing_extensions-4.0.1-py3-none-any.whl (22 kB)
Requirement already satisfied: idna>=2.8 in /usr/local/lib/python3.9/site-packages (from anyio<4,>=3.0.0->starlette==0.16.0->fastapi) (3.3)
Requirement already satisfied: sniffio>=1.1 in /usr/local/lib/python3.9/site-packages (from anyio<4,>=3.0.0->starlette==0.16.0->fastapi) (1.2.0)
Installing collected packages: typing-extensions, starlette, pydantic, fastapi
Successfully installed fastapi-0.70.1 pydantic-1.8.2 starlette-0.16.0 typing-extensions-4.0.1

④uvicornインストール

$ pip install uvicorn
Collecting uvicorn
  Downloading uvicorn-0.16.0-py3-none-any.whl (54 kB)
     |████████████████████████████████| 54 kB 5.3 MB/s             
Requirement already satisfied: h11>=0.8 in /usr/local/lib/python3.9/site-packages (from uvicorn) (0.12.0)
Collecting asgiref>=3.4.0
  Downloading asgiref-3.4.1-py3-none-any.whl (25 kB)
Requirement already satisfied: click>=7.0 in /usr/local/lib/python3.9/site-packages (from uvicorn) (7.1.2)
Installing collected packages: asgiref, uvicorn
Successfully installed asgiref-3.4.1 uvicorn-0.16.0

⑤sqlalchemyインストール(Mysql連動するため)

$ pip install sqlalchemy
Collecting sqlalchemy
  Using cached SQLAlchemy-1.4.28-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.6 MB)
Requirement already satisfied: greenlet!=0.4.17 in /usr/local/lib/python3.9/site-packages (from sqlalchemy) (1.1.2)
Installing collected packages: sqlalchemy
Successfully installed sqlalchemy-1.4.28

⑥pymysqlインストール(Mysql連動するため)

$ pip install pymysql
Collecting pymysql
  Downloading PyMySQL-1.0.2-py3-none-any.whl (43 kB)
     |████████████████████████████████| 43 kB 4.5 MB/s             
Installing collected packages: pymysql
Successfully installed pymysql-1.0.2

⑤/opt/fastapi/app/main.py作成

「/opt/fastapi/app」 任意のFastAPIディレクトリです。 main.py

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware


app = FastAPI()

origins = ["*"]

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

@app.get(path="/")
async def FastAPI():
    return { "message" : "Hello World" }

⑥FastAPIサービス起動

$ uvicorn main:app --reload --host=0.0.0.0 --port=8000
INFO:     Will watch for changes in these directories: ['/opt/fastapi/app']
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [10873] using statreload
INFO:     Started server process [10875]
INFO:     Waiting for application startup.
INFO:     Application startup complete.

⑦swagger UI確認

「URL」 http://[IPアドレス]/#docs/

無題1.png

FastAPI機能(GET、POST、PUT、DELETE)

FastAPIの「GET、POST、PUT、DELETE」メソッドについて動作確認するために
メソッドの実装、MYSQL連動についてご説明します。

FastAPIファイル構造

先ず、MYSQLが連動されたFastAPIを構築するため以下のファイルを作成します。

ファイル名用途
databases.pyDB設定を作成するファイル
handle_db.pyDBクエリを作成するファイル
main.pyFastAPIメインファイル
models.pyTableモデル作成するファイル

MYSQL連動及び各メソッド作成(GET、POST、PUT、DELETE)

①/opt/fastapi/app/databases.py作成
 ・MYSQL連動するためにDBの情報を記載します。databases.py

from sqlalchemy import create_engine
import sys
from sqlalchemy.orm import (sessionmaker, relationship, scoped_session)

sys.dont_write_bytecode = True

#setting db connection
url = "mysql+pymysql://dbadmin:password@<MYSQLのIPアドレス>:3306/fastapi?charset=utf8"
engine = create_engine(url, echo=False, pool_recycle=10)

#create session
def create_new_session():
    return  scoped_session(sessionmaker(autocommit=False, autoflush=True, expire_on_commit=False, bind=engine))

②/opt/fastapi/app/handle_db.py作成
 ・各メソッド(GET、POST、PUT、DELETE)に使われるクエリを記載します。handle_db.py

# -*- encoding: utf-8 -*-
import sys
import models
import databases

sys.dont_write_bytecode = True
def select_all_user():
    session = databases.create_new_session()
    user_list = session.query(models.user).\
            filter(models.user.status == 'created').\
            all()
    if user_list == None:
        user_list = []
    return user_list

def create_user(user_name, user_mail):
    session = databases.create_new_session()
    user = models.user()
    user.name = user_name
    user.mail_address = user_mail
    user.status = 'created'
    session.add(user)
    session.commit()
    return 0

def select_user(user_id):
    session = databases.create_new_session()
    user = session.query(models.user).\
                filter(models.user.id == user_id).\
                first()           
    if user == None:
        user = ""
    return user

def update_user(user_id, user_name, user_mail, user_status):
    session = databases.create_new_session()
    user = session.query(models.user).\
                filter(models.user.id == user_id).\
                first()
    if user == None:
        return 1
    user.name = user_name
    user.mail_address = user_mail
    user.status = user_status
    session.commit()
    return 0

def delete_user(user_id):
    session = databases.create_new_session()
    user = session.query(models.user).\
                filter(models.user.id == user_id).\
                first()
    if user == None:
        return 1
    user.status = "deleted"
    session.commit()
    return 0

③/opt/fastapi/app/main.py更新
 ・FastAPIがリスポンスするURLとAPIが呼ばれた場合、実行されるクエリを記載します。main.py

# -*- encoding: utf-8 -*-
from fastapi import FastAPI, Depends, Path, HTTPException
import models
from fastapi.middleware.cors import CORSMiddleware
import handle_db
import datetime

app = FastAPI()

origins = ["*"]

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

@app.get(path="/")
async def FastAPI():
    return { "message" : "Hello World" }

## select user list
@app.get(path="/api/users")
async def get_list_user():
    result = handle_db.select_all_user()
    return {
        "status": "OK",
        "data": result
    }

## create user
@app.post(path="/api/users")
async def post_user(user_name: str, user_mail: str):
    result = handle_db.create_user(user_name, user_mail)
    if result == 1:
        raise HTTPException(status_code=404, detail="Query Error!!")
    return {
        "status": "OK",
        "data": result
    }

## select user
@app.get(path="/api/users/{user_id}")
async def get_user(user_id: str):
    result = handle_db.select_user(user_id)
    if result == 1:
        raise HTTPException(status_code=404, detail="Query Error!!")
    return {
        "status": "OK",
        "data": result
    }

## update user 
@app.put(path="/api/users/{user_id}")
async def put_user(user_id: str, user_name: str, user_mail: str, user_status: str):
    result = handle_db.update_user(user_id, user_name, user_mail, user_status)
    if result == 1:
        raise HTTPException(status_code=404, detail="Query Error!!")
    return {
        "status": "OK",
        "data": result
    }

## delete user 
@app.delete(path="/api/users/{user_id}")
async def delete_user(user_id: str):
    result = handle_db.delete_user(user_id)
    if result == 1:
        raise HTTPException(status_code=404, detail="Query Error!!")
    return {
        "status": "OK",
        "data": result
    }

④/opt/fastapi/app/models.py作成
・データベースのテーブルモデルを記載します。models.py

# -*- encoding: utf-8 -*-
import datetime
import uuid
import sys
from sqlalchemy import (Column, String, Text, ForeignKey,CHAR, VARCHAR, INT,  \
                create_engine, MetaData, DECIMAL, DATETIME, exc, event, Index, \
                and_)
from sqlalchemy.ext.declarative import declarative_base

sys.dont_write_bytecode = True

base = declarative_base()

class user(base):
    __tablename__ = 'user'
    id = Column(CHAR(36), primary_key=True)
    name = Column(VARCHAR(255))
    mail_address = Column(VARCHAR(255))
    create_date_time = Column(DATETIME)
    update_date_time = Column(DATETIME)
    status = Column(VARCHAR(255))

    def __init__(self):
        self.id = str(uuid.uuid4())
        now_data_time = str(datetime.datetime.now().strftime("%Y%m%d%H%M%S"))
        self.create_date_time =  now_data_time
        self.update_date_time =  now_data_time

MYSQL設定(データベース及びテーブル作成)

FastAPIからデータを制御するためにMYSQLでデータベースとテーブルを作成します。
この投稿ではuserテーブルを作成してデータ制御(SELECT、CREATE、UPDATE、DELETE)します。
※FastAPI は、SQLAlchemy に対応するすべてのDB (Mysql、Postgresql、SQLite) に容易に適用することができます。

①データベース作成

②テーブル作成

GET、POST、PUT、DELETE動作確認

①swagger UI確認

「URL」 http://[IPアドレス]/#docs/

image1.png

②GETメソッド(Select_All)


③POSTメソッド(Create)


④GETメソッド(Select)


⑤PUTメソッド(Update)


⑥DELETEメソッド(Delete)

FastAPI構築時に困ったところ

FastAPIを開発しながら何個か困ってた内容と分かった内容を紹介したいと思います。:sob:

API命名規則

最初、API命名する時にどんなルールで作るか困ってましたが、API命名にも規則が有るというのが分かりましたので代表的な2種類を紹介したいと思います。

①Camel Case
例)http://[IPアドレス]:[port>]/api/userInfo/mailAddress
②Snake Case
例)http://[IPアドレス]:[port>]/api/user_info/mail_address

Search機能

APIのGETメソッドで取得したデータに対したSearch機能が必要でしたが、FastAPIはSearch機能を提供してないようです。
でも、FastAPIはPython基盤を使っているのでSearch機能のロジックを実装すれば色んな対応案を作ることが出来ると思います。

CDN(Contents Delivery Network)

今の現場ではオンプレミスでサーバーを構築していまして、インターネットが繋がってない環境なので(セキュリティー強化のため)
Offlineでサーバー構築を行ってますが、FastAPIのSwagger UIはCDNから提供されますのでOfflineサーバーでは使えない状態になります。
でも、pipからfastapi-offlineをインストールするとofflineサーバーでもSwagger UIが使えるようになります。

※参考
・CDN 
 https://fastapi.tiangolo.com/advanced/extending-openapi/
・fastapi-offline 
 https://pypi.org/project/fastapi-offline/

Dockerイメージ作成

構築したFastAPIをDockerコンテナーにデプロイしたい場合は、
FastAPIをDockerのイメージにビルドしてデプロイすることも可能です。

※参考
 https://fastapi.tiangolo.com/ja/deployment/docker/

さいごに

今回は、FastAPIを開発する方法についてご紹介しました。
FastAPIという言葉通りに早い性能及び開発速度、さらに便利なGUI提供まで支援してくれるので開発者には魅力があるミドルウェアだと思いました。
また、今までは各種サービスに内包されているAPIを使うことが多かったのですが、
この機会にFastAPIを開発したことで、APIについて多くの知識が積み重なったと思います。
初の投稿なので足りない部分があると思いますが、
初めてFastAPIを接する方の役になれば幸いです。:blush: