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を利用しましたが、今回の投稿にはバックエンド側を重点的にご説明したいと思います。
![image15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2277113/549d8386-55e3-808c-a245-71ff87ff8720.png)

##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作成

:::note info
「/opt/fastapi/app」
任意のFastAPIディレクトリです。
:::

```ruby: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確認

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

![無題1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2277113/8655b7ed-ce2d-689b-fcc3-b3ba913229a3.png)

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

###FastAPIファイル構造
先ず、MYSQLが連動されたFastAPIを構築するため以下のファイルを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2277113/f7ee5ec1-55ab-3874-3893-ec55069c2721.png)



| ファイル名   | 用途 |
|:------------|:-----------|
| databases.py| DB設定を作成するファイル|
| handle_db.py| DBクエリを作成するファイル|
| main.py     | FastAPIメインファイル|
| models.py   | Tableモデル作成するファイル|

###MYSQL連動及び各メソッド作成（GET、POST、PUT、DELETE）
①/opt/fastapi/app/databases.py作成
　・MYSQL連動するためにDBの情報を記載します。

```ruby: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）に使われるクエリを記載します。

```ruby: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が呼ばれた場合、実行されるクエリを記載します。

```ruby: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作成
 ・データベースのテーブルモデルを記載します。

```ruby: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) に容易に適用することができます。

①データベース作成
![image3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2277113/38a26c42-2248-0c83-d653-a9c5b5d7852b.png)
②テーブル作成
![image4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2277113/ea23017b-8254-37b2-f3aa-17ae6f4281a7.png)

###GET、POST、PUT、DELETE動作確認
①swagger UI確認

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

![image1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2277113/cca7692f-dcfd-c49a-8fa7-1f3eb29f3fcd.png)

②GETメソッド(Select_All)
![image5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2277113/e8f59184-ad9d-7eb4-514a-21d3544f63a0.png)
![image10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2277113/4da6592e-10a9-2cb9-68a4-cfe89406cc84.png)
③POSTメソッド（Create）
![image6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2277113/17c38a6b-ff5a-249d-de1a-16a04e391907.png)
![image11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2277113/1b406dd1-7b2d-e233-0804-293cd993fb7e.png)
④GETメソッド（Select）
![image7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2277113/fc6980a8-52df-a97f-f0f8-bc4e89b8fbcf.png)
![image12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2277113/b8351cd6-7db4-a22e-6b5a-b801ab689c40.png)
⑤PUTメソッド（Update）
![image8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2277113/8e9e8bd9-2120-67dd-4c37-dca20d3d6b20.png)
![image13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2277113/6f65918a-f865-8656-53bb-4d8aa2b9ca67.png)
⑥DELETEメソッド（Delete）
![image9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2277113/3751dddf-6534-63fe-7e15-6b34d8e7e7ad.png)
![image14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2277113/d8679321-1e61-8ba5-bbb9-29654fec169b.png)
##FastAPI構築時に困ったところ
FastAPIを開発しながら何個か困ってた内容と分かった内容を紹介したいと思います。:sob:
###API命名規則
最初、API命名する時にどんなルールで作るか困ってましたが、API命名にも規則が有るというのが分かりましたので代表的な２種類を紹介したいと思います。

①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: