docker composeで手軽にReact＋FastAPI＋MySQLお試しローカル環境を作る

# docker composeで手軽にReact＋FastAPI＋MySQLお試しローカル環境を作る

## 動機
- Reactを使った静的ホスティングなるものをやってみたい。
- 動的なコンテンツはFastAPIで作って適宜呼び出したい。
- FastAPIとデータベースはつなげておいて、FastAPIサーバーは将来的にサーバーレスみたいにしたい。

## この記事の目的
Reactで生成したページから、FastAPIで作ったAPIを利用して、MySQLに入っているデータをブラウザ上で確認する。
  
## 最初のディレクトリ構成
- docker-compose.yml
- client
  - Dockerfile
- server
  - Dockerfile
  - requirements.txt
  - code
    - db.py
    - main.py
    - model.py
- mysql
  - conf.d
    - my.cnf
  - initdb.d
    - schema.sql
    - testdata.sql
  - log
    - mysql

## Docker系ファイル
### docker-compose.yml
```docker-compose:docker-compose.yml
version: "3"

services:
  db: 
    container_name: back_db
    image: mysql:5.7
    restart: always
    tty: true
    environment:
      MYSQL_DATABASE: sample_db
      MYSQL_USER: user
      MYSQL_PASSWORD: password
      MYSQL_ROOT_PASSWORD: password
    ports: 
      - 3306:3306
    command: --port 3306
    volumes: 
      - ./mysql/initdb.d:/docker-entrypoint-initdb.d
      - ./mysql/conf.d:/etc/myaql/conf.d
      - ./mysql/log/mysql:/var/log/mysql

  server:
    links:
      - db
    build: ./server/
    tty: true
    container_name: back_fastapi
    working_dir: /usr/src/server
    ports:
      - 8080:8080
    volumes:
      - ./server/code/:/usr/src/server

  client:
    build: ./client/
    container_name: front_react
    ports:
      - 3000:3000
    volumes:
      - ./server/:/var/www/server/
      - ./client/:/var/www/client/
    tty: true
    stdin_open: true
    command: sh -c "cd test_app && npm start"
```
- ポイント
  - clientのcommandで、コンテナたちを立ち上げるたびにReactのサーバーを起動している。
  - serverの方も本当はcommandで起動できるのかもしれないが、今回はのちに出てくるFastAPIの方のDockerfileで起動の処理を行った。
  - MySQLの環境変数の設定の項目は、おそらく本来は.envか何かで外に出して、gitignoreさせるのがよろしいとは思う。

### server/Dockerfile
```Docker:Dockerfile
FROM python:3.8
WORKDIR /usr/src/server
ADD ./requirements.txt .
RUN pip install --trusted-host pypi.python.org -r requirements.txt
CMD ["uvicorn", "main:app", "--reload", "--host", "0.0.0.0", "--port", "8080"]
```
- ポイント
  - 地味にWORKDIRを記しておくことが大切そう。
  - requirements.txtをコンテナ内に追加して、環境構築をする。
  - CMDコマンドはおそらくdocker-composeのcommandと一緒（確証なし）

### client/Dockerfile
```Docker:Dockerfile
FROM node:latest
WORKDIR /var/www/client/
RUN npm install -g create-react-app
RUN npm install --save react-router-dom
RUN npm install --save prop-types
```
- ポイント
  - こちらも環境構築のためにDockerfileを構築した

## serverの中身
### requirements.txt
```:requirement.txt
mysqlclient
sqlalchemy
uvicorn
fastapi
```
- ポイント
  - 上二つはSQLとつなげるため
  - 下二つはFastAPIサーバーを立てるため

### code/main.py
```python: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の実装------------

@app.get("/")
def read_root():
    return {"Hello": "World!!!!"}

# テーブルにいる全ユーザ情報を取得 GET
@app.get("/users")
def read_users():
    users = session.query(UserTable).all()
    return users
```
- ポイント
  - ルートでFastAPIをたたくと、{"Hello": "World!!!!"}を返す。
  - /usersでたたくと、DBに入っているusersのキークエリを返す。


### code/db.py
```python: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()
```

### code/model.py
```python: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()
```

## mysqlの中身
### conf.d/my.cnf
```mysql:my.cnf
[mysqld]
character-set-server=utf8
skip-character-set-client-handshake=utf8
default-storage-engine=INNODB
explicit-defaults-for-timestamp=1
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
```
- ポイント
  - 主に文字コードの設定

### initdb.d/schema.sql
```mysql:schema.sql
CREATE TABLE user (
    id INT NOT NULL AUTO_INCREMENT,
    name VARCHAR(30) NOT NULL,
    age INT,
    PRIMARY KEY (id)
);
```
### initdb.d/testdata.sql
```mysql:testdata.sql
INSERT INTO user (name, age) VALUES ("saburo", 15);
INSERT INTO user (name, age) VALUES ("jiro", 18);
INSERT INTO user (name, age) VALUES ("taro", 20);
```

## 起動コマンド
```
docker compose run --rm client sh -c "create-react-app test_app"
docker compose up -d --remove-orphans
```
1行目は最初だけ。reactの初期フォルダーを作成するためのコマンド。
二回目以降は2行目だけでOK

最初は  
localhost:3000でReactの初期画面  
localhost:8080で{"Hello": "World!!!!"}  
localhost:8080/usersでtestdata.sqlで設定したuserの中身が見えるはず。


## ReactからAPIを呼び出す
### モジュールのインストール
```
docker compose exec client bash         
root@xxxxxxxxxxxx:/var/www/client# cd app_test
root@xxxxxxxxxxxx:/var/www/client/app_test# npm install --save axios
```
### App.jsの更新
```javascript:client/test_app/App.js
import React from "react";
import axios from "axios";

function App() {
	const [data, setData] = React.useState();
	const url = "http://127.0.0.1:8080";

	const GetData = () => {
		axios.get(url).then((res) => {
			setData(res.data);
		});
	};

  const url_users = "http://127.0.0.1:8080/users";

	const GetData_users = () => {
		axios.get(url_users).then((res) => {
			setData(res.data);
		});
	};

	return (
		<div>
			{/* <div>8080</div>
			{data ? <div>{data.Hello}</div> : <button onClick={GetData}>データを取得　Hello</button>} */}
      <div>8080/users</div>
			{data ? <div>{data[1].name}</div> : <button onClick={GetData_users}>データを取得　Users</button>}
		</div>

	);
}

export default App;
```
- ポイント
  - jiroが出てくればOK。目的達成
  - React詳しい方はもっとうまいことできるはず

## 終了コマンド
```
docker compose stop
```
## 参考記事
https://qiita.com/KWS_0901/items/684ac71e728575b6eab0
https://qiita.com/ikeikeda/items/eeed5abb2230bf031ba5
https://qiita.com/A-Kira/items/d2f9c8cef9346cb32229