FastAPIをMySQLと接続してDockerで管理してみる

この記事では、爆速でAPIを実装したい。また、FastAPIの文献少なすぎて、
MySQLとの連携ってどうやってやるの？と疑問に思っている方に向けて、
三分で環境を構築する方法を紹介しています。
![Screenshot from 2019-04-09 03-16-23.png](https://qiita-image-store.s3.amazonaws.com/0/130670/039a9703-bea8-7684-6a7b-80d99d810aee.png)

# 前提
- MySQLを触ったことがある。selectやwhereなどがわかる
- Pythonを少しは触ったことがある。
- Dockerをインストールして使える状態

※ まだ、Dockerをインストールしていない方は、以下のサイトでわかりやすく紹介しているので、
参考にしながら導入してみましょう！
https://qiita.com/scrummasudar/items/750aa52f4e0e747eed68

# FastAPIとは???
Pythonを使ったAPI構築のためのフレームワークです。ちなみに、Python3.6以上で動作します。
Pythonを使ったAPIはFlaskやdjangoなどが有名で、どれも簡単にAPIを構築することができますが、
FastAPIでは、**より手軽に、そして動作も速く、非同期通信も簡単に実装**することができます。

公式では、**NodeJSやGoと同等の高いパフォーマンス**とうたっています。

公式にはサンプルも多いので、困ったときはこちらを確認してみるといいでしょう。
https://fastapi.tiangolo.com/

# 今回やること
- docker-composeでMySQLとFastAPIを起動する
- 起動したMySQLに接続し、設定ファイルどおりに構築されているか確認する
- FastAPIで実装したAPIをSwaggerやcurlで叩いてMySQL上のデータを操作してみる

### できたものがこちらになります(お急ぎの方)
今回紹介するコードの完成版はこちらになります
https://github.com/hogeline/sample_fastapi

お急ぎの方はこちらからすぐ環境を構築することができます
~~はい、三分で実装できましたね笑~~

お急ぎ出ない方は、以下から本題です。

# 事前準備

### ディレクトリ構造

```bash
.
├── code
│   ├── db.py
│   ├── main.py
│   └── model.py
├── docker
│   ├── api
│   │   ├── Dockerfile
│   │   └── requirements.txt
│   └── mysql
│       ├── Dockerfile
│       ├── conf.d
│       │   └── my.cnf
│       └── initdb.d
│           ├── schema.sql
│           └── testdata.sql
└── docker-compose.yml
```
今回のディレクトリ構造はこのようになっています

codeにはPythonのコードが入ります
dockerにはDockerfileやMySQLの設定、
FastAPIを実行するために必要なパッケージなどの情報が入っています。

###1. docker-compose.ymlの作成

```yml:docker-compose.yml
version: "3.3"

services:
  # MySQL
  db:
    container_name: "db"
    # path配下のDockerfile読み込み
    build: ./docker/mysql
    # コンテナが落ちたら再起動する
    restart: always
    tty: true
    environment:
      MYSQL_DATABASE: sample_db
      MYSQL_USER: user
      MYSQL_PASSWORD: password # ユーザのパスワード
      MYSQL_ROOT_PASSWORD: password # ルートパスワード
    ports:
      - "3306:3306"
    volumes:
      - ./docker/mysql/initdb.d:/docker-entrypoint-initdb.d # 定義どおりにテーブル作成
      - ./docker/mysql/conf.d:/etc/mysql/conf.d # MySQLの基本設定(文字化け対策)
      - ./log/mysql:/var/log/mysql # ログの保存
    networks:
      - local-net

  # FastAPI
  api:
    # db起動後に立ち上げる
    links:
      - db
    container_name: "api"
    # path配下のDockerfile読み込み
    build: ./docker/api
    ports:
      - "8000:8000"
    volumes:
      - ./code:/usr/src/server
    networks:
      - local-net

# コンテナ間で通信を行うためのネットワークブリッジ
networks:
  local-net:
    driver: bridge
```
docker-composeでは、MySQLとFastAPIを扱うdbとapiのサービスを作ります
それぞれのコンテナイメージは./docker/mysqlと./docker/apiのDockerfileを参照して作成します

apiからdbのmysqlへ接続するためにコンテナ間の通信が必要だったので、networks: local-netを定義しています

###2. Dockerfileを作成する
```dockerfile:./docker/mysql/Dockerfile
FROM mysql:5.7

# MySQLの操作ログのファイルを作成
RUN touch /var/log/mysql/mysqld.log
```



```dockerfile:./docker/api/Dockerfile
FROM python:3.7

WORKDIR /usr/src/server
ADD requirements.txt .
# requirements.txtにリストされたパッケージをインストールする
RUN pip install --trusted-host pypi.python.org -r requirements.txt

# コンテナ起動後、FastAPIを実行し8000ポートで待機
CMD ["uvicorn", "main:app", "--reload", "--host", "0.0.0.0", "--port", "8000"]
```
今回利用するPythonのバージョンは3.7です
FastAPIを立ち上げるuvicornのオプションに--reloadをつけると
./code/main.pyを編集と同時に変更内容が反映されます

###3. pipでインストールするパッケージ一覧

```txt:./docker/api/requirements.txt
mysqlclient
sqlalchemy
uvicorn
fastapi
```

mysqlclientとsqlalchemyはMySQLを操作するために必要で、
FastAPIを起動するために必要なパッケージはuvicornとfastapiのみです


###4. MySQL設定ファイルの作成

```cnf:./docker/mysql/conf.d/my.cnf
[mysqld]
character-set-server=utf8 # mysqlサーバー側が使用する文字コード
skip-character-set-client-handshake # 文字化け対策
default-storage-engine=INNODB # InnoDBを使用
explicit-defaults-for-timestamp=1 # テーブルにTimeStamp型のカラム用
general-log=1 # 実行したクエリの全ての履歴を記録
general-log-file=/var/log/mysql/mysqld.log # ログの出力先

[mysqldump]
default-character-set=utf8

[mysql]
default-character-set=utf8

[client]
default-character-set=utf8 # mysqlのクライアント側が使用する文字コード

```

sqlalchemyを利用するにあたって、ストレージエンジンはInnoDBじゃないといけません、MySQLではデフォルトでInnoDBを利用しますが、一応指定しましょう

###5. スキーマの定義

```sql:./docker/mysql/initdb.d/schema.sql
CREATE TABLE user (
    id INT NOT NULL AUTO_INCREMENT,
    name VARCHAR(30) NOT NULL,
    age INT,
    PRIMARY KEY (id)
);
```
スキーマで定義したuserテーブルを作成します

###6. テストデータの作成

```sql:./docker/mysql/init.db/testdata.sql
INSERT INTO user (name, age) VALUES ("太郎", 15);
INSERT INTO user (name, age) VALUES ("次郎", 18);
INSERT INTO user (name, age) VALUES ("花子", 20);
```

テーブル作成後、上記のSQLを実行しテストデータを流します

###7. PythonでDBと接続するための設定

```python:./code/db.py
# -*- coding: utf-8 -*-
# DBへの接続設定
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, scoped_session


# 接続したいDBの基本情報を設定
user_name = "user"
password = "password"
host = "db"  # docker-composeで定義したMySQLのサービス名
database_name = "sample_db"

DATABASE = 'mysql://%s:%s@%s/%s?charset=utf8' % (
    user_name,
    password,
    host,
    database_name,
)

# DBとの接続
ENGINE = create_engine(
    DATABASE,
    encoding="utf-8",
    echo=True
)

# Sessionの作成
session = scoped_session(
    # ORM実行時の設定。自動コミットするか、自動反映するか
    sessionmaker(
        autocommit=False,
        autoflush=False,
        bind=ENGINE
    )
)

# modelで使用する
Base = declarative_base()
# DB接続用のセッションクラス、インスタンスが作成されると接続する
Base.query = session.query_property()
```

既存のDBを利用したい場合は、このファイルの基本情報を設定している項目を編集することで、
そちらへアクセスすることが可能です

###8. FastAPIで扱うモデルを定義する

```python:./code/model.py
# -*- coding: utf-8 -*-
# モデルの定義
from sqlalchemy import Column, Integer, String
from pydantic import BaseModel
from db import Base
from db import ENGINE


# userテーブルのモデルUserTableを定義
class UserTable(Base):
    __tablename__ = 'user'
    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(String(30), nullable=False)
    age = Column(Integer)


# POSTやPUTのとき受け取るRequest Bodyのモデルを定義
class User(BaseModel):
    id: int
    name: str
    age: int


def main():
    # テーブルが存在しなければ、テーブルを作成
    Base.metadata.create_all(bind=ENGINE)


if __name__ == "__main__":
    main()
```

ここでは、FastAPIで操作するMySQLのテーブルのモデルの定義と
POSTやPUTで送られてきたリクエストbodyを扱うためにモデルを定義しています



# FastAPIの実装
FastAPIの実装を行います
今回は、GETとPOST、PUTを実装しました

```python:./code/main.py
from fastapi import FastAPI
from typing import List  # ネストされたBodyを定義するために必要
from starlette.middleware.cors import CORSMiddleware  # CORSを回避するために必要
from db import session  # DBと接続するためのセッション
from model import UserTable, User  # 今回使うモデルをインポート

app = FastAPI()

# CORSを回避するために設定
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# ----------APIの実装------------
# テーブルにいる全ユーザ情報を取得 GET
@app.get("/users")
def read_users():
    users = session.query(UserTable).all()
    return users

# idにマッチするユーザ情報を取得 GET
@app.get("/users/{user_id}")
def read_user(user_id: int):
    user = session.query(UserTable).\
        filter(UserTable.id == user_id).first()
    return user

# ユーザ情報を登録 POST
@app.post("/user")
# クエリでnameとstrを受け取る
# /user?name="三郎"&age=10
async def create_user(name: str, age: int):
    user = UserTable()
    user.name = name
    user.age = age
    session.add(user)
    session.commit()

# 複数のユーザ情報を更新 PUT
@app.put("/users")
# modelで定義したUserモデルのリクエストbodyをリストに入れた形で受け取る
# users=[{"id": 1, "name": "一郎", "age": 16},{"id": 2, "name": "二郎", "age": 20}]
async def update_users(users: List[User]):
    for new_user in users:
        user = session.query(UserTable).\
            filter(UserTable.id == new_user.id).first()
        user.name = new_user.name
        user.age = new_user.age
        session.commit()
```
asyncをdefの前につけることで非同期で処理してくれます

defで定義した関数の引数にクエリやリクエストbodyが入ってきます。
例えば、

```python
@app.get("/users/{user_id}")
def read_user(user_id: int):
```
であれば、"/users/1"のアクセスがあればuser_idは1の値が引数に入ってきます
ここらへんの使い方は、以下のサイトで詳しく紹介しているので参考にしてください
https://qiita.com/mtitg/items/47770e9a562dd150631d

クエリやレスポンスbodyから取得した値を使ってDBを操作します
操作はsqlalchemyを使って行うので、直感的に簡単にできると思います

詳しい使い方は、こちらのサイトで紹介されているので参考にしましょう
https://qiita.com/bokotomo/items/a762b1bc0f192a55eae8

# 実行手順
さっそく、FastAPIとMySQLを構築してAPIを叩いて見ましょう

###1. docker-composeコマンドを実行してみる
コンテナの作成＆起動

```bash
$ docker-compose up -d --build
```
コンテナのステータスを確認

```bash
$ docker-compose ps
Name              Command               State                 Ports              
---------------------------------------------------------------------------------
api    uvicorn main:app --reload  ...   Up      0.0.0.0:8000->8000/tcp           
db     docker-entrypoint.sh mysqld      Up      0.0.0.0:3306->3306/tcp, 33060/tcp
```

apiとdbがUPしていることを確認しましょう

###2. MySQLへ接続して、設定どおりテーブルが作成されているか確認する

Dockerで作成したMySQLへ接続してみます．
このとき、ホストはlocalhostではなく127.0.0.1であることに注意です

```bash
$ mysql -u user -h 127.0.0.1 -D sample_db -p
Enter password:

mysql> show tables;
+---------------------+
| Tables_in_sample_db |
+---------------------+
| user                |
+---------------------+
1 row in set (0.01 sec)

mysql> select * from user;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | 太郎   |   15 |
|  2 | 次郎   |   18 |
|  3 | 花子   |   20 |
+----+--------+------+
3 rows in set (0.00 sec)
```
userテーブルが作成していることが確認できました


###3. FastAPIのドキュメントを確認する
FastAPIでは、作成したAPIの仕様を自動でドキュメント化してくれます
ドキュメントはSwaggerというもので作られていて、ドキュメント内でAPIを叩いたりクエリを確認したりできます

http://localhost:8000/docs

上記のリンクへアクセスすると実装したAPIが表示されていると思います
<img width="416" alt="スクリーンショット 2019-04-09 16.23.57.png" src="https://qiita-image-store.s3.amazonaws.com/0/130670/5c8de1ba-48dd-c800-2725-f8e417d59074.png">

###4. Swagger上でAPIを叩いてみる
#### GETで全ユーザ情報を取得
GET /usersを実行してみましょう
Tryを押して
<img width="136" alt="スクリーンショット 2019-04-09 16.32.59.png" src="https://qiita-image-store.s3.amazonaws.com/0/130670/4c7a2183-447e-5a1c-ef47-5902cbf3c9e4.png">

Executeを押すと
<img width="625" alt="スクリーンショット 2019-04-09 16.33.22.png" src="https://qiita-image-store.s3.amazonaws.com/0/130670/cc77faf4-a35d-5229-8915-f6202ada52f7.png">

全データが取得できていることが確認できます
<img width="493" alt="スクリーンショット 2019-04-09 16.28.25.png" src="https://qiita-image-store.s3.amazonaws.com/0/130670/24304a21-b011-2040-9a73-b2bfc26c377f.png">

#### POSTでユーザ登録
Swaggerでは簡単にPOSTを試すことができます
curlだとヘッダーつけなきゃいけなくて大変ですが...

<img width="685" alt="スクリーンショット 2019-04-09 18.26.55.png" src="https://qiita-image-store.s3.amazonaws.com/0/130670/aa13b1b8-a302-16ee-795a-53dc0e62f5f0.png">

今回はnameに"四郎"、ageに10を入れてPOSTしてみます
クエリとしては/user?name="四郎"&age=10でPOSTします

Excuteを押したあと、200で返ってきたことを確認し
ユーザが登録されているか先程実行したGET /usersを再度実行してみましょう
<img width="328" alt="スクリーンショット 2019-04-09 18.27.34.png" src="https://qiita-image-store.s3.amazonaws.com/0/130670/f3d22f67-e6fe-8a34-bf6d-a1010dc1a87d.png">

増えていることが確認できました

#### PUTでユーザの情報を更新
次郎と花子の年齢を変えてみましょう
<img width="400" alt="スクリーンショット 2019-04-09 18.46.00.png" src="https://qiita-image-store.s3.amazonaws.com/0/130670/f74bd4d1-df81-fd11-61ab-e39d47a8063e.png">

今回、PUTではUserモデルをネストしたリクエストbodyを受け付けているので

```
[
  {
    "id": 2,
    "name": "次郎",
    "age": 13
  },
  {
    "id": 3,
    "name": "花子",
    "age": 14
  }
]
```

このようなbody作ってPUTしてみます

200で返ってきてることを確認し、
GET /usersで反映されているか確認してみましょう

<img width="463" alt="スクリーンショット 2019-04-09 18.56.56.png" src="https://qiita-image-store.s3.amazonaws.com/0/130670/0db9a0ba-aa91-f2dc-56a3-13be6dc0ea71.png">


###5. curlでAPIを叩いてみる

```bash
$ curl localhost:8000/users/1
{"id":1,"age":15,"name":"太郎"}
```
curlでuserテーブルのID1の情報が取得できました

# 感想
FastAPIは簡単にAPIを実装できて非同期処理も簡単にできるので便利だな〜と感じました

少しでも、このサンプルプログラムが役にたったな〜と思った方は
いいね！を押していただければと思います！

# リファレンス
DockerComposeを用いたMySQLの環境構築はこちらのサイトを参考にしました
> https://qiita.com/Manatee/items/58d0f98a15656ed65136

sqlalchemyはこちらのサイトを参考にしました
> https://qiita.com/bokotomo/items/a762b1bc0f192a55eae8

FastAPIの基本的な使い方を日本語でまとめてくれているサイト
> https://qiita.com/mtitg/items/47770e9a562dd150631d

公式ドキュメント
> https://fastapi.tiangolo.com/features/