LoginSignup
48
51

More than 1 year has passed since last update.

FastAPI * MySQL * DockerでCRUDを行うAPIをサクッと作ってみた(初学者向け)

Last updated at Posted at 2022-02-26

目次

1.はじめに
2.DB構築
3.アプリ構築
4.動作確認
5.まとめ

1. はじめに

以前の記事でPythonのフレームワーク(FastAPI)を用いて超シンプルなREST APIの作り方を記載した。そこではDockerコンテナの作成まで記したが、機能としてはHTTPリクエストを受け付けるだけの内容となっている。

本記事では、DBへ接続しSQL実行を行う処理とローカル環境での検証用にDBコンテナの構築をし、ローカルPC内で動作確認ができる状態までについて掲載する。

Docker Containerを複数立ち上げて疎通をする場合、他の方の記事だとdocker-composeを利用する内容が多いが、本記事では利用しない。知らないことを調べたら別の知らない内容が出てきたという再帰的サイクルはどこかでbreakしないと学ぶことを苦に感じてしまうからだ。

RDBMSはMySQLを採用。
個人的にはOracleやpostgresを使ってみたい所だが、AWSのRDB(Amazon RDS)のなかで無料で使えるものはMySQLぐらいしか無かったため、今後AWS上に構築する事を想定してローカル環境に閉じた本記事でもMySQLを使用している。

FastAPI自体にはRDB接続用のライブラリなどは無さそうなため、SQLAlchemyを採用。公式にもSQLAlchemyを利用したexampleがあるためこれを参考にした。

公式より引用

FastAPI doesn't require you to use a SQL (relational) database.
But you can use any relational database that you want.
Here we'll see an example using SQLAlchemy.
You can easily adapt it to any database supported by SQLAlchemy, like:

  • PostgreSQL
  • MySQL
  • SQLite
  • Oracle
  • Microsoft SQL Server, etc.

本記事ではSQLAlchemyやMySQL自体の解説はしておらず、「とりあえずDBと繋げたい!」というゴールに向けた手順書にも似た記載なため、初学者向けかもしれない。

2. DB構築

まずは構成の説明。

.
└── db/
    ├── init.sql
    └── Dockerfile

2-1. initialze sql

DBコンテナを立ち上げた際に、テーブル作成や初期データ投入を行うためにinit.sqlを作成しておく。

よくある内容を記載。

init.sql
USE sample_db;

DROP TABLE IF EXISTS item;
CREATE TABLE item
(
  item_id SERIAL PRIMARY KEY,
  name VARCHAR(50),
  price INT,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

INSERT INTO item (name, price) VALUES ('apple', 100);
INSERT INTO item (name, price) VALUES ('orange', 200);

順に解説する。「知ってるよ!」という方は是非読み飛ばして頂きたい。

MySQL用の内容のため他RDBでは記述が異なることに注意。

  • sample_dbという名のデータベースを利用する。
    USE sample_db;
    
  • 既にitemというテーブルが存在すれば削除する。
    DROP TABLE IF EXISTS item;
    
  • itemテーブルを作成する。
    CREATE TABLE item
    (
        item_id SERIAL PRIMARY KEY,
        name VARCHAR(50),
        price INT,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
    );
    
    • SERIALBIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUEのこと。insertする時にVALUEを指定しなくとも自動で連番を振ってくれる。嬉しい。
    • DEFAULT CURRENT_TIMESTAMPはinsertするときにVALUEを指定しなくともCURRENT_TIMESTAMPを格納してくれる。便利。
    • ON UPDATE CURRENT_TIMESTAMPはupdateするときにVALUEを指定しなくともCURRENT_TIMESTAMPを格納してくれる。便利で嬉しい。

注意
現在日時の取得を、DB側で行うかアプリ側で行うかは要件や設計次第。
必ずしもDB側で現在日時を取得することをオススメしているわけではないのでご注意。
クラウド上の無料のDBを使用する場合はZoneの指定などできない可能性があり、アプリ側で現在日時を持ち回る方が良い場合もある。複数のアプリサーバーを使用するケースで全てのサーバーで全く同じ現在時刻を持ち回れない場合など、タイムサーバーとしてDBの現在日付を持ち回る方が良い場合もある。

  • データを格納する
    INSERT INTO item (name, price) VALUES ('apple', 100);
    
    • item_idcreated_atupdated_atは自動で値が格納されるために値を指定していない。

2-2. MySQL Container

続いてDockerfileについて。

Dockerfile
FROM mysql:latest

ENV MYSQL_DATABASE sample_db
ENV MYSQL_USER sample_user
ENV MYSQL_PASSWORD sample_password
ENV TZ "Asia/Tokyo"
ENV MYSQL_ROOT_PASSWORD p@assw0rd

COPY ./init.sql /docker-entrypoint-initdb.d/init.sql

細かな環境変数についての説明は公式のEnvironment Variablesを参照してください。

ここで定義している内容をまとめると下記。rootアカウントについては割愛。

項目
DB名 sample_db
ユーザ名 sample_user
パスワード sample_password
タイムゾーン Asia/Tokyo

/docker-entrypoint-initdb.dにinit.sqlを格納することで、起動時に実行してくれる。

では早速imageをbuild。

cd db # Dockerfileのあるディレクトリまで移動
docker build -t sample-mysql:1.0.0 .

確認。

docker images sample-mysql:1.0.0    
REPOSITORY     TAG       IMAGE ID       CREATED         SIZE
sample-mysql   1.0.0     95380b8fbf50   6 minutes ago   519MB

いらっしゃる。次にコンテナも立ち上げてみる。

docker run -d --name sample-mysql-container -p 3306:3306 sample-mysql:1.0.0

確認

docker ps -f "name=sample-mysql-container" 
CONTAINER ID   IMAGE                COMMAND                  CREATED          STATUS          PORTS                                                  NAMES
3c8f3c7429ba   sample-mysql:1.0.0   "docker-entrypoint.s…"   32 seconds ago   Up 31 seconds   0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp   sample-mysql-container

いた。次にコンテナ内に入り、init.sql内で記載したinsert通りにデータが存在するか確認する。

  1. コンテナ内に入る。
    docker exec -it $(docker ps -q -f "name=sample-mysql-container") /bin/bash
    
  2. dbに接続。
    mysql sample_db -h localhost -u sample_user -psample_password
    
  3. sample_dbが存在するか確認。
    mysql> show databases;
    +--------------------+
    | Database           |
    +--------------------+
    | information_schema |
    | sample_db          |
    +--------------------+
    2 rows in set (0.00 sec)
    
  4. itemテーブルが存在するか確認。
    mysql> show tables from sample_db;
    +---------------------+
    | Tables_in_sample_db |
    +---------------------+
    | item                |
    +---------------------+
    1 row in set (0.00 sec)
    
  5. itemテーブルの定義を確認。
    mysql> SHOW COLUMNS FROM item FROM sample_db;
    +------------+-----------------+------+-----+-------------------+-----------------------------------------------+
    | Field      | Type            | Null | Key | Default           | Extra                                         |
    +------------+-----------------+------+-----+-------------------+-----------------------------------------------+
    | item_id    | bigint unsigned | NO   | PRI | NULL              | auto_increment                                |
    | name       | varchar(50)     | YES  |     | NULL              |                                               |
    | price      | int             | YES  |     | NULL              |                                               |
    | created_at | timestamp       | YES  |     | CURRENT_TIMESTAMP | DEFAULT_GENERATED                             |
    | updated_at | timestamp       | YES  |     | CURRENT_TIMESTAMP | DEFAULT_GENERATED on update CURRENT_TIMESTAMP |
    +------------+-----------------+------+-----+-------------------+-----------------------------------------------+
    5 rows in set (0.01 sec)
    
  6. レコードを確認
    mysql> select * from item;
    +---------+--------+-------+---------------------+---------------------+
    | item_id | name   | price | created_at          | updated_at          |
    +---------+--------+-------+---------------------+---------------------+
    |       1 | apple  |   100 | 2022-02-26 18:29:08 | 2022-02-26 18:29:08 |
    |       2 | orange |   200 | 2022-02-26 18:29:08 | 2022-02-26 18:29:08 |
    +---------+--------+-------+---------------------+---------------------+
    2 rows in set (0.00 sec)
    

無事構築が完了したので、exitでmysql/containerのコンソールを抜けておこう。

後にアプリの構築が完了した後、sample-mysql-containerに接続するため立ち上げっぱなしでOK。

3. アプリ構築

まずは構成の説明。

.
├── db/
│   ├── init.sql
│   └── Dockerfile
└── api/
    ├── requirements.txt
    ├── Dockerfile
    ├── .env
    └── app/
        └── main.py

apiディレクトリを追加し、ここにアプリケーションを構築する、

3-1. requirements.txt

パッケージ一括管理用にrequirements.txtを(使用する内容は少数だが)作成しておく。

requirements.txt
sqlalchemy==1.4.31
mysqlclient==2.1.0

fastapiuvicornは要らないの?」と思われるかもしれないが、今回使用するbaseimage(uvicorn-gunicorn-fastapi)の中には既にインストールされているため記載不要。

3-2. App

早速コーディングしてみる。
順に説明を記載するが、一旦全量を見たい方はソースコード全量をどうぞ。

db接続

db接続周りの記述。

  • db engineの作成。

    main.py
    import os
    from sqlalchemy import create_engine
    
    SQLALCHEMY_DATABASE_URI = 'mysql://{}:{}@{}:{}/{}'.format(
            os.environ.get("DB_USER"), 
            os.environ.get("DB_PASSWORD"),
            os.environ.get("DB_HOST"), 
            os.environ.get("DB_PORT"), 
            os.environ.get("DB_NAME")
        )
    
    engine = create_engine(SQLALCHEMY_DATABASE_URI)
    
    • 環境変数から接続情報を取得している。
      • 開発環境と本番環境でDBの接続情報が異なる場合がほとんど。
      • 開発で動いたのに本番で動かない!」を避けるために全ての環境でソースコードが同じだという保証が欲しい。
  • sessionの作成。

    main.py
    from sqlalchemy.orm import sessionmaker
    Session = sessionmaker(autocommit=False, autoflush=False, bind=engine)
    
    def session():
        db = Session()
        try:
            yield db
        finally:
            db.close()
    
    • engineは上記で定義したもの。
    • sessionはAPI実行後に都度閉じたいため、finallyにcloseするmethodを定義。
  • modelの作成

    main.py
    from sqlalchemy import Column, TIMESTAMP, Integer, String
    from sqlalchemy.ext.declarative import declarative_base
    from sqlalchemy.schema import FetchedValue
    
    Base = declarative_base(bind=engine)
    
    class Item(Base):
        __tablename__ = "item"
        __table_args__ = {"autoload": True}
        item_id = Column(Integer, primary_key = True, nullable=False)
        name = Column(String(50), nullable=False)
        price = Column(Integer, nullable=False)
        created_at = Column(TIMESTAMP, FetchedValue())
        updated_at = Column(TIMESTAMP, FetchedValue())
    
    • Base : モデルのベースクラスを定義。
    • class Item(Base)はORMで扱えるモデルクラスを定義。Itemテーブルに対してSQL文を書かずにCRUD処理を実現するために利用。
    • sqlalchemyではテーブルの作成も可能なようだが、既に作成済みなテーブルと連携するため__table_args__ = {"autoload": True}を設定している。
    • FetchedValue() : default valueが設定されている項目に対して、DB側の値を利用する場合に指定。この設定を行わないと insert時などに Nullが設定されてしまう。

Controller(logic)

HTTPリクエストを受け付け、CRUD処理を行いレスポンスを返す処理を記述する。

細かい設定については以前の記事の参照をオススメ。

  • GET : SELECTする

    main.py
    from fastapi import Depends, Query, status
    from fastapi.responses import JSONResponse
    from fastapi.encoders import jsonable_encoder
    
    @app.get("/item")
    def get_item(id: int = None, name: str = Query(None, max_length=50), db: Session = Depends(session)):
        if id is not None:
            result_set = db.query(Item).filter(Item.item_id == id).all()
        elif name is not None:
            result_set = db.query(Item).filter(Item.name == name).all()
        else:
            result_set = db.query(Item).all()    
        response_body = jsonable_encoder({"list": result_set})
        return JSONResponse(status_code=status.HTTP_200_OK, content=response_body)
    
    • nameは受付可能な最大文字数を50に設定。
    • db: Session = Depends(session) : api実行時にdb sessionを生成する。
    • db.query(Item) : itemテーブルへのQuery実行。
      • .filter(Item.COL == vaule) : COL列の値がvalueと一致するものを対象とする。
      • .all() : 全件取得。
    • jsonable_encoder({"list": result_set}) : json形式へ変更。
      sample
      {
          "list" : [
              {
                  //検索結果の1
              },
              {
                  //検索結果の1
              }
          ]
      }
      
  • POST : INSERTする。

    main.py
    from pydantic import BaseModel
    from fastapi import Depends, Query, status
    from fastapi.responses import JSONResponse
    from fastapi.encoders import jsonable_encoder
    
    class ItemRequest(BaseModel):
        name: str = Query(..., max_length=50)
        price: float
    
    @app.post("/item")
    def create_item(request: ItemRequest, db: Session = Depends(session)):
        item = Item(
                    name = request.name,
                    price = request.price
                )
        db.add(item)
        db.commit()
        response_body = jsonable_encoder({"item_id" : item.item_id})
        return JSONResponse(status_code=status.HTTP_200_OK, content=response_body)
    
    • class ItemRequest(BaseModel) : postの際のrequest bodyを定義。
    • db.add(item) : insert処理定義。
    • db.commit() : commitする。ここで実際にinsertされる。また、itemにinsert結果が反映される。自動生成されたitem_idなどを取得することができる。
  • PUT : UPDATEする。

    main.py
    from pydantic import BaseModel
    from fastapi import Depends, Query, status
    from fastapi.responses import JSONResponse
    from fastapi.encoders import jsonable_encoder
    
    class ItemRequest(BaseModel):
        name: str = Query(..., max_length=50)
        price: float
    
    @app.put("/item/{id}")
    def update_item(id: int, request: ItemRequest, db: Session = Depends(session)):
        item = db.query(Item).filter(Item.item_id == id).first()
        if item is None:
                return JSONResponse(status_code=status.HTTP_404_NOT_FOUND)
        item.name = request.name
        item.price = request.price
        db.commit()
        return JSONResponse(status_code=status.HTTP_200_OK)
    
    • .first() : 最初の1件をselectする。
    • selectした内容をitemに格納する。
    • update対象が存在しなければ404を返すように設定。
    • itemの値を更新後、db.commit()を行いDB側に反映することでupdateする。
  • DELETE : DELETEする。

    main.py
    from fastapi import Depends, Query, status
    from fastapi.responses import JSONResponse
    
    @app.delete("/item/{id}")
    def delete_item(id: int, db: Session = Depends(session)):
        db.query(Item).filter(Item.item_id == id).delete()
        db.commit()
        return JSONResponse(status_code=status.HTTP_200_OK)
    
    • .delete() : 文字通りdeleteする。

以上で、CRUD処理のAPIの実装が完了した。
下にソースコードの全量を記す。

ソースコード全量

main.py
import os
from fastapi import FastAPI, Depends, Query, status
from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
from sqlalchemy import Column, TIMESTAMP, Integer, String, create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.schema import FetchedValue


app = FastAPI()

SQLALCHEMY_DATABASE_URI = 'mysql://{}:{}@{}:{}/{}'.format(
        os.environ.get("DB_USER"), 
        os.environ.get("DB_PASSWORD"),
        os.environ.get("DB_HOST"), 
        os.environ.get("DB_PORT"), 
        os.environ.get("DB_NAME")
    )

engine = create_engine(SQLALCHEMY_DATABASE_URI)
Session = sessionmaker(autocommit=False, autoflush=False, bind=engine)

def session():
    db = Session()
    try:
        yield db
    finally:
        db.close()

Base = declarative_base(bind=engine)

# Entity Item
class Item(Base):
    __tablename__ = "item"
    __table_args__ = {"autoload": True}
    item_id = Column(Integer, primary_key = True, nullable=False)
    name = Column(String(50), nullable=False)
    price = Column(Integer, nullable=False)
    created_at = Column(TIMESTAMP, FetchedValue())
    updated_at = Column(TIMESTAMP, FetchedValue())

# Request Body
class ItemRequest(BaseModel):
    name: str = Query(..., max_length=50)
    price: float

# GetItemByName
@app.get("/item")
def get_item(id: int = None, name: str = Query(None, max_length=50), db: Session = Depends(session)):
    if id is not None:
        result_set = db.query(Item).filter(Item.item_id == id).all()
    elif name is not None:
        result_set = db.query(Item).filter(Item.name == name).all()
    else:
        result_set = db.query(Item).all()    
    response_body = jsonable_encoder({"list": result_set})
    return JSONResponse(status_code=status.HTTP_200_OK, content=response_body)

# CreateItem
@app.post("/item")
def create_item(request: ItemRequest, db: Session = Depends(session)):
    item = Item(
                name = request.name,
                price = request.price
            )
    db.add(item)
    db.commit()
    response_body = jsonable_encoder({"item_id" : item.item_id})
    return JSONResponse(status_code=status.HTTP_200_OK, content=response_body)

# UpdateItem
@app.put("/item/{id}")
def update_item(id: int, request: ItemRequest, db: Session = Depends(session)):
    item = db.query(Item).filter(Item.item_id == id).first()
    if item is None:
            return JSONResponse(status_code=status.HTTP_404_NOT_FOUND)
    item.name = request.name
    item.price = request.price
    db.commit()
    return JSONResponse(status_code=status.HTTP_200_OK)

# DeleteItem
@app.delete("/item/{id}")
def delete_item(id: int, db: Session = Depends(session)):
    db.query(Item).filter(Item.item_id == id).delete()
    db.commit()
    return JSONResponse(status_code=status.HTTP_200_OK)

3-4. 環境変数

次にコンテナに渡す環境変数を定義する。

.env
DB_HOST=host.docker.internal
DB_PORT=3306
DB_NAME=sample_db
DB_USER=sample_user
DB_PASSWORD=sample_password

DBのホスト名はlocalhostじゃないの?」と思うかも知れないが、上記の値はアプリコンテナ内で参照されるためlocalhostだと外に出れない。つまるところ、コンテナの中でのlocalhostは同コンテナ自身である。

host.docker.internalはdocker for Macでサポートされているコンテナからホスト上のサービスに対して接続するドメイン。Windowsでの利用、いつまでサポートされているか不明なため要確認。

公式より引用

ホストの IP アドレスは変動します(あるいは、ネットワークへの接続がありません)。18.03 よりも前は、特定の DNS 名 host.docker.internal での接続を推奨していました。これはホスト上で内部の IP アドレスで名前解決します。これは開発用途であり、Docker Desktop forMac 外の本番環境では動作しません。

最新のversionだとオススメされていないのかしら?大人しくdocker composeを使用すれば明示的にホスト名を定義・指定できるが、主題ではないため割愛。

3-5. Api Container

次にDockerfile。

Dockerfile
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9-slim

COPY ./requirements.txt /app/requirements.txt

RUN apt-get update && \
    apt-get -y install gcc libmariadb-dev && \
    pip install --no-cache-dir --upgrade -r /app/requirements.txt

COPY ./app /app

gcclibmariadb-devは何?」という細かい話は割愛するが、これが無いとmysqlclientをインストール際にエラーとなってしまう。

実際に直面したエラーはこちら。

3.044 ERROR: Could not find a version that satisfies the requirement mysqlclient

docker container化せずローカルで遊んでみたいという方で、上記のようなエラーが出る場合もgcc libmariadb-devの導入を試して欲しい。

では実際に動かしてみる。

cd ../api # Dockerfileのあるディレクトリまで移動
docker build -t sample-api:1.0.0 . 

確認する。

docker images sample-api:1.0.0               
REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
sample-api   1.0.0     7ccc69b2d334   11 minutes ago   426MB

いた。次にコンテナを立ち上げる。

コンテナ立ち上げの際に、環境変数定義ファイルを渡す。

docker run -d --env-file=.env --name sample-api-container -p 80:80 sample-api:1.0.0

しっかりと確認。

docker ps -f "name=sample-api-container" 
CONTAINER ID   IMAGE              COMMAND       CREATED        STATUS                  PORTS                               NAMES
bcb5939e0146   sample-api:1.0.0   "/start.sh"   1 second ago   Up Less than a second   0.0.0.0:80->80/tcp, :::80->80/tcp   sample-api-container

いらっしゃる。ログをtailしてみる。

docker logs --tail=-1 -f $(docker ps -q -f "name=sample-api-container") 
#省略
[2022-02-26 11:30:36 +0000] [12] [INFO] Application startup complete.

起動している!

最後に、動作確認をする。

4. 動作確認

curlで動作確認を行う。

まずは全検索。

curl -v -X GET 'http://localhost:80/item'
# 省略
< HTTP/1.1 200 OK
< date: Sat, 26 Feb 2022 12:03:15 GMT
< server: uvicorn
< content-length: 233
< content-type: application/json
< 
* Connection #0 to host localhost left intact
{"list":[{"created_at":"2022-02-26T21:02:50","item_id":1,"updated_at":"2022-02-26T21:02:50","price":100,"name":"apple"},{"created_at":"2022-02-26T21:02:50","item_id":2,"updated_at":"2022-02-26T21:02:50","price":200,"name":"orange"}]}* Closing connection 0

とれた。jsonが見づらい場合はjqコマンドを併用すると便利。

curl -s -X GET 'http://localhost:80/item' | jq
{
  "list": [
    {
      "created_at": "2022-02-26T21:02:50",
      "item_id": 1,
      "updated_at": "2022-02-26T21:02:50",
      "price": 100,
      "name": "apple"
    },
    {
      "created_at": "2022-02-26T21:02:50",
      "item_id": 2,
      "updated_at": "2022-02-26T21:02:50",
      "price": 200,
      "name": "orange"
    }
  ]
}

綺麗、便利。

jqコマンドないよという方は | jq がなくてもOK。ただ見やすい方がいいので導入しておくことをオススメする。

さて、init.sqlで実行されたレコードが2件存在することが確認できた。さらにinsert文では指定していないitem_idcreated_atupdate_atが自動で生成されていることも確認できた。

次に id指定で検索してみる。

curl -s -X GET 'http://localhost:80/item?id=1' | jq
{
  "list": [
    {
      "created_at": "2022-02-26T21:02:50",
      "item_id": 1,
      "updated_at": "2022-02-26T21:02:50",
      "price": 100,
      "name": "apple"
    }
  ]
}

絞り込めた。

次に存在しないidを指定してみる。

curl -s -X GET 'http://localhost:80/item?id=999' | jq
{
  "list": []
}

特にエラーがなく、空リストが返ってきた。REST設計としては404 Not Foundにしても良いかしら?とも考えたが、ここでは統一して200を返している。

名前での検索も同様に確認してみる。

curl -s -X GET 'http://localhost:80/item?name=orange' | jq
{
  "list": [
    {
      "created_at": "2022-02-26T21:02:50",
      "item_id": 2,
      "updated_at": "2022-02-26T21:02:50",
      "price": 200,
      "name": "orange"
    }
  ]
}

こちらも問題ない。

次に文字数を増やして50文字を超えてみる。

curl -s -X GET 'http://localhost:80/item?name=123456789022345678903234567890423456789052345678901' | jq
{
  "detail": [
    {
      "loc": [
        "query",
        "name"
      ],
      "msg": "ensure this value has at most 50 characters",
      "type": "value_error.any_str.max_length",
      "ctx": {
        "limit_value": 50
      }
    }
  ]
}

エラーが返ってきた。しっかりvalidation checkされているようだ。

しかし、クライアントに対してデフォルトのエラーを返してしまうと「ははーん50文字以内なのだな?」と悪い人にバレるため(バレても良い場合もあるが)、共通処理としてValidationErrorの際のresponse定義をすることをオススメする。実装方法は別記事で紹介予定。

次に新規登録をする。

curl -X POST 'http://localhost:80/item' -H 'Content-Type: application/json' -d '{"name":"banana","price":123}'
{"item_id":3}

idは3が採番されたことを確認。

再び取得してみる。

curl -s -X GET 'http://localhost:80/item?name=banana' | jq
{
  "list": [
    {
      "created_at": "2022-02-26T21:04:39",
      "item_id": 3,
      "updated_at": "2022-02-26T21:04:39",
      "price": 123,
      "name": "banana"
    }
  ]
}

ばっちり取得できた。

次に更新してみる。

curl -s -X PUT "http://localhost:80/item/1" -H 'Content-Type: application/json' -d '{"name":"super apple","price":500}' 
null

response bodyを定義していないためnullが返ってきた。あえて消したい場合は要修正。

再び取得してみる。

curl -s -X GET 'http://localhost:80/item?id=1' | jq
{
  "list": [
    {
      "created_at": "2022-02-26T21:02:50",
      "item_id": 1,
      "updated_at": "2022-02-26T21:09:21",
      "price": 500,
      "name": "super apple"
    }
  ]
}

無事に指定した値と合わせてupdated_atも更新されたことを確認。

次に存在しないidに対して更新してみる。

curl -v -X PUT "http://localhost:80/item/999" -H 'Content-Type: application/json' -d '{"name":"super hoge","price":12345}' 
# 省略
< HTTP/1.1 404 Not Found
< date: Sat, 26 Feb 2022 12:12:52 GMT
< server: uvicorn
< content-length: 4
< content-type: application/json
< 
* Connection #0 to host localhost left intact
null* Closing connection 0

定義した通り、404が返ってきた。

次に削除してみる。

curl -s -X DELETE 'http://localhost:80/item/2'
null

次に全件取得してみる。

curl -s -X GET 'http://localhost:80/item' | jq
{
  "list": [
    {
      "created_at": "2022-02-26T21:02:50",
      "item_id": 1,
      "updated_at": "2022-02-26T21:09:21",
      "price": 500,
      "name": "super apple"
    },
    {
      "created_at": "2022-02-26T21:04:39",
      "item_id": 3,
      "updated_at": "2022-02-26T21:04:39",
      "price": 123,
      "name": "banana"
    }
  ]
}

消えている。

コンテナを止めて終了!

docker kill sample-mysql-container                                      
docker kill sample-api-container 

5. まとめ

下記を実施した。

  • ローカル上にMySQLのDockerContainerを構築
    • 初期化sqlを定義
  • ローカル上にAPIのDockerContainerを構築
    • SQLAlchemyを用いてCRUD処理を実装
      • DB接続情報は環境変数から取得
    • FastAPIを用いてHTTPリクエストを受け付けるように実装

この次のステップは何をすればいいの?という方に。

  • 環境変数を書き換えて、クラウド上のDBなどに接続してみる。
  • API RequestのValidation Errorの際の共通Responseを定義してみる。
  • API実行時にExceptionが発生した場合のhandlingを実装してみる。
  • logginのformat変更やfile出力をしてみる。

この辺りの記事を記載予定。

以上。

48
51
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
48
51