FastAPIを使ったCRUD操作(MySQL、SQLAlchemy、XAMPP)

# 初めに
今回はFastAPIを使った簡単なRest APIのCRUDを作成してみました。
下記の要件定義をもとに作成してます。

**要件定義**
・フレームワークにFast APIを利用すること
・DBにMySQLを利用すること
・ORMにSQLAlchemyを利用すること

なお、作成いただくAPIのパスは /user とし、スキーマとしては以下を備えてください。ただし、各スキーマの制約等は不要とし、その他未指定の場合はご自身で前提条件を設定していただいて構いません。

・ユーザーID（主キー、更新不可）
・ユーザー名
・メールアドレス

**使った環境**
* Python 3.10.4 (Anaconda 3で環境構築)
* MySQL 5.7.29
* XAMPP 3.3.0
``` requirements.txt
fastapi==0.85.0
# (PythonでREST APIを開発するためのWebフレームワーク)
PyMySQL==1.0.2
# (PythonからMySQLの操作が行えるパッケージ)
SQLAlchemy==1.4.41
# (後ほど説明)
uvicorn==0.18.3
# (Python用のASGI Webサーバー実装)
```
になります。

### SQLAlchemy について
SQLAlchemyとは、Pythonの中では最もよく利用されているORMの一つです。ORM以外にも以下の機能を持ちます。

* **データベースへの接続、SQLの実行**
SQLAlchemyは様々なデータベースに対して接続してSQLを実行することができます。
* **メタデータ**
SQLAlchemyにはメタデータと呼ばれるテーブルとPythonのモデルクラスをマッピングする機能があります。
* **SQL Expression Language**
JavaのORM、HibernateにはHQLというクエリ言語がありますが、SQLAlchemyにも同様にクエリを表す記述が用意されています。
* **ORM**
クエリの実行結果をモデルに格納します。

# MySQLの設定
今回は```XAMPP```でも```local```でも、全く同じ構造でデータベースとテーブルを作ってます。
```local```のコマンドプロンプトで説明します。

```fastapi```が今回作成したデータベースになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2629918/36c21a36-0eda-585d-ad66-87489dc98e51.png)

テーブル名は```fast```になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2629918/0ab80262-5ce8-982a-3e12-b06c70eac5af.png)

テーブルのスキーマ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2629918/ae3778c1-2815-816f-2f4b-fff5423af207.png)

仮でこのようなデータを一つ入れてます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2629918/8bde6722-e600-645d-ced3-2d9877113365.png)

※XAMPP側
XAMPPの方は、phpMyAdminが簡単にアクセス可能なので初めの環境では使わせていただきました。

テーブルのスキーマ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2629918/c1040edf-1b42-6768-28ea-1ccfe820d1f8.png)
中のデータ
こちらにも仮で先にデータを入れてます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2629918/e05dbd6a-2494-a67e-267a-e00086a09e77.png)

# ファイル構造
```
  Rest_API_CRUD
  |-- fast_api
  |   |-- db.py
  |   |-- models.py
  |   |-- routes.py
  |   |-- schemas.py
  |   |-- test.py
  |
  README.md
  requirements.txt
```
# コードの中身
コードをわかりやすく説明していきます。

### db.py

コードの全体
```python:db.py
from sqlalchemy import create_engine, MetaData

# XAMPP上での実行コマンド
# engine = create_engine("mysql+pymysql://root@localhost:3306/fastapi")

#ローカルでの実行コマンド
engine = create_engine("mysql+pymysql://root:{password}@localhost:3306/fastapi")

meta = MetaData()
conn = engine.connect()
```
SQLAlchemyを使ってMy SQLに接続していきます。

#### ```create_engine```の構成の説明
```create_engine```で、DBエンジンのインスタンスを作成します。
```python
engine = create_engine("{dialect}+{driver}://{username}:{password}@{host}:{port}/{database}")
```
|要素|説明|
|:-:|:-:|
|dialect|DBの種類<br>今回はMySQL|
|driver|DBに接続するためのドライバー<br>今回はPyMySQL|
|username|DBに接続することができるユーザ名<br>初期設定のままなので、```root```|
|password|DBに接続するためのパスワード<br>```local```のみ記載<br>※XAMPPの場合、パスワードはなしです。|
|host|ホスト名(localhostとかIPアドレスとか)<br>今回は```localhost```|
|port|ポート番号<br>今回はデフォルトなので```3306```を使用|
|database|接続するデータベース名<br>MySQLで確認した通り```fastapi```|

#### その他
```MetaData()```
テーブルのスキーマをPythonのコード上で定義できる。
```engine.connect()```
DBに接続する。

### models.py
全体のコード
```python:models.py
from sqlalchemy import Table, Column
from sqlalchemy.sql.sqltypes import Integer, String
from db import meta

users = Table(
    'fast',meta,
    Column('id',Integer,primary_key=True),
    Column('name',String(255)),
    Column('mail',String(255)),
)
```
```models.py```では、```db.py```で作った変数```meta```を使用して、テーブルのスキーマをPythonのコード上で定義していきます。
```python
users = Table(
    '{MyAQLで作成したテーブルの名前}',meta,
    # Columnでカラムの定義
    # 今回の主キーはidになるため、primary_key=Trueが設定されてる。
    Column('id',Integer,primary_key=True),
    Column('name',String(255)),
    Column('mail',String(255)),
)
```

### schemas.py
全体のコード
```python:schemas.py
from pydantic import BaseModel

class User(BaseModel):
    name: str
    mail: str
```
ここではPydanticを使って型の変換をしてます。
今回は、```id```が自動で割り振られるので、記入する```name```と```mail```を文字列として変換します。

### routes.py
全体のコード
```python:routes.py
from fastapi import APIRouter
from db import conn
from models import users
from schemas import User

user = APIRouter(prefix='/user', tags=['user'])

# 全件表示
@user.get("/")
async def read_data():
    return conn.execute(users.select()).fetchall()

# 指定のIDのみ検索
@user.get("/{id}")
async def read_data(id: int):
    return conn.execute(users.select().where(users.c.id == id)).fetchall()

# 新しいデータの作成
@user.post("/")
async def write_data(user: User):
    conn.execute(users.insert().values(
        name=user.name,
        mail=user.mail
    ))
    return conn.execute(users.select()).fetchall()

# 既存データのアップデート
@user.put("/{id}")
async def update_data(id:int, user: User):
    conn.execute(users.update().values(
        name=user.name,
        mail=user.mail
    ).where(users.c.id == id))
    return conn.execute(users.select()).fetchall()

# データの削除
@user.delete("/{id}")
async def delete_data(id: int):
    conn.execute(users.delete().where(users.c.id == id))
    return conn.execute(users.select()).fetchall()
```
##### ```Router```のインスタンス化
```python
user = APIRouter(prefix='/user', tags=['user'])
```
```prefix```で、これから作成するエンドポイントの頭のURLを指定。
```tags```で、```http;//127.0.0.1/docs```の分類で使用される。
##### routingの定義
```python
@{APIRouterの変数}.{operation}({Path})
```
でroutingを定義してます。
##### ```async def```について
今回は全体的に```async def```と言われる、非同期処理で行っていきます。
##### ```conn.execute```について
```db.py```で作成した変数```conn```を使って処理をしています。
```.execute```を使ってSQLを実行できます。

#### 全件表示の処理
```python
@user.get("/")
async def read_data():
    return conn.execute(users.select()).fetchall()
```
全件表示の返り値には、```users.select()```で```user```の中身を取り出して、```.fetchall()```で返り値を全部表示しています。
※一件だけにしたい場合は、```.fetchone()```で行える。

#### 指定のIDのみ検索
```python
@user.get("/{id}")
async def read_data(id: int):
    return conn.execute(users.select().where(users.c.id == id)).fetchall()
```
routingのPathに```id```を追加して、```id```に該当するデータを持ってくるようにします。
```id```で探すために```.where(users.c.id == id)```としており、```users```のカラムの```id```から、Pathで指定した```id```と同じものを探すというものになります。

#### 新しいデータの作成
```python
@user.post("/")
async def write_data(user: User):
    conn.execute(users.insert().values(
        name=user.name,
        mail=user.mail
    ))
    return conn.execute(users.select()).fetchall()
```
データの作成には、```.execute```内でinsert文を記入します。
作成に必要なデータは、```name```と```mail```です。(```id```は自動で作られます。)
最後に返り値でinsertした後のデータを呼び出してます。

#### 既存データのアップデート
```python
@user.put("/{id}")
async def update_data(id:int, user: User):
    conn.execute(users.update().values(
        name=user.name,
        mail=user.mail
    ).where(users.c.id == id))
    return conn.execute(users.select()).fetchall()
```
データのアップデートには、```.execute```内でupdate文を記入します。
基本的には、**新しいデータの作成**と同じですが、```id```の指定がPathにあります。
そのため、同じ```id```のデータを書き換えるという処理になります。
こちらも、最後に返り値で全体を出力してます。

#### データの削除
```python
@user.delete("/{id}")
async def delete_data(id: int):
    conn.execute(users.delete().where(users.c.id == id))
    return conn.execute(users.select()).fetchall()
```
データの削除では、```.execute```内でdelete文を記入します。
今回はPathで指定した```id```と同じものを削除するというものになります。
こちらも、最後に返り値で全体を出力してます。

### test.py
全体のコード
```python:test.py
from fastapi import FastAPI
from routes import user

app = FastAPI()

app.include_router(user)
```
```app```でUvicornで呼び出せるように指定しています。
```app.include_router(user)```で、サブモジュール```routes```の```user```をinclude関数で呼び出しています。

# 実行画面
それでは、実行していきます。
### 概要
FastAPIでは、OpenAPIを自動生成してくれます。
URL:http://127.0.0.1:8000/docs#/
にアクセスすると入れます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2629918/b6e9687a-8ee7-59e1-3c10-acfe1454e164.png)
画面はこのようにまとまっており、ここで実行から確認までできます。

### 全件表示
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2629918/4f533626-4516-71da-b595-1a99618b8db5.png)

### 新しいデータの作成
```name```と```mail```を書き込むことで反映されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2629918/7ef62a1e-4646-cfa2-f601-de6e1b9c35a7.png)
実行すると、新しくデータが作成されます。
(自分の方で何回か作成したため、```id```が10になってますが、実際は連番で登録されます。削除した```id```は欠番になります。)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2629918/4b1b6efc-5390-5b55-4311-ede68fa966cd.png)

### 指定のIDのみ検索
```id```を指定することで、該当データのみ抽出できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2629918/6ac8f2a0-8446-1326-3b90-6bcb3f5094bc.png)

### 既存データのアップデート
```id```を指定することで、該当データを書き換えることができます。
```name```と```mail```を書き込むことで反映されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2629918/b041cb71-8a6b-7a33-415e-c12128ef67e3.png)
しっかり反映されてます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2629918/40e36016-6061-3379-9a01-5b2a36805203.png)

### データの削除
```id```を指定することで、該当データを削除することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2629918/c50d0fdf-53f2-8759-e984-08b5a391b1d9.png)
削除されました。
次作成されるデータは11番からになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2629918/97d6b205-61aa-4502-d7dc-e5870351c0db.png)

# 最後に
調べながらやりましたが、個人的にはこれをReactまでやりたいなと思ったので、余裕があれば自分でやってみたいと思います。
とても楽しかったです。
GitHubにも上げてるので見てみてください。
https://github.com/kyoppy-1229/Rest_API_CRUD

# 参考動画
https://youtu.be/4Zy90rd0bkU