初心者が、docker上で、Fastapi経由でpostgresにデータの追加、更新、削除を行ってみた。

# 目次
1. [はじめに](#index)
2. [やること](#todo)
3. [前提条件](#prerequisite)
4. [必要なフレームワーク](#requirements)
5. [各種概念説明](#concept)
6. [構成](#diagram)
7. [手順](#procedure)
8. [確認](#check)
9. [まとめ](#summary)
10. [参考](#reference)


<a id="index"></a>

# 1. はじめに

## 内容・目的
初心者が、docker上でpostgresqlに対してfastapi経由でデータの登録等ができるようにやったことをまとめた。
サクッと、dockerでpostgresとfastapiを動かしてみたいと初心者が思ったときに、
完全に合致する内容で書いてある記事があまりなかったので、過去の自分を助けるという意味合いも込めて書いた。


## 想定している対象
pythonをつかってwebアプリを作成してみたいけど、何から手をつければよいかわからない人向け

## 想定読了時間
読むだけなら10分程度
コードを動かしてみるなら30分程度

<a id="todo"></a>

# 2.やること
ローカル環境上でFastAPI経由でデータベースにデータを登録、更新、削除する。

## 要件・仕様

### 取得
idを指定し、データベースからユーザー情報を取得する
ユーザーはidを入力

* 取得が成功した場合　返り値：200_ok,登録情報 
* 取得が失敗した場合  返り値：400_error,"user_mail is not found"

### 登録時

データベースにユーザーを登録する。
ユーザーは、メールアドレス、PW、名前を入力。

* 登録が成功した場合　返り値：200_ok,登録情報 
* 登録が失敗した場合  返り値：500_error,"user_mail is wrong"


### 削除時
メールアドレスおよびPWが入力し、正しい場合、ユーザーデータを削除する。

* 削除が成功した場合、返り値：200_ok,削除した情報
* 削除が失敗した場合、返り値：404_error ,"not match"

を返す

### 更新時
メールアドレスおよびPWを入力し、正しい場合、ユーザーデータのPWを更新できる

* 更新が成功した場合  返り値：200_ok,更新した情報
* 更新が失敗した場合　返り値：404_error,"user_mail is wrong"　

を返す

<a id="prerequisite"></a>

# 3.前提条件
dockerを利用する環境構築完了していること。

## 実行環境について
docker desktop for windowsで実行。
* docker desktop for windowsのインストールは[こちらから](https://docs.docker.jp/docker-for-windows/install.html)

<a id="requirements"></a>

# 4.言語や必要なフレームワーク
今回利用するもの一覧は下記の通り。

* Python
* docker
* FastAPI
* Postgresql
* SQLAlchemy

## 選定理由
### Python
Qiita でもタグランキングで上位に位置し、初心者向きのため。

### Docker 
コンテナツールの中でデファクトスタンダードのため。

### FastAPi
ドキュメントがしっかりしているため。

### Postgresql
過去に触ったことがあったため。
~~ MySQLでもやってみます、いつか~~

### SQLAlchemy
SQLツールキットのデファクトスタンダードであるため。

<a id="concept"></a>

# 5.各種キーワード説明
## docker
> コンテナとは、ある環境から別の環境へと移動した際にアプリケーションを迅速かつ確実に実行できるように、コードとその依存関係をすべてパッケージ化したソフトウェアの標準単位である。Dockerkコンテナイメージは、軽量、スタンドアローンな実行可能なソフトウェアパッケージである。

引用：docker(https://www.docker.com/resources/what-container/)

>Dockerは仮想化技術の一つで、コンテナ型に分類される。
仮想化技術は、ホスト型、ハイパーバイザー型、コンテナ型の三種類ある。

引用：(https://www.itmanage.co.jp/column/virtualization-server-integration/)

Dockerが属するコンテナ型は、ホスト型、ハイパーバイザー型よりも構築、破棄が容易なので、コンテナ型を採用。

## FastAPI
pythonでAPIを構築するためのWebフレームワーク。

## Postgresql
オープンソースのリレーショナルデータベース管理システム(RDBS)の一つ。

### リレーショナルデータベース管理システム(RDBS)
データベースは、階層型、ネットワーク型、リレーショナル型の三つに分類される。
リレーショナルデータベースは列と行で構成されているもの。
イメージとしては、エクセルが近い。

## SQLAlchemy
SQLAlchemyはpython SQKのツールキットで、Object Relational Mapperである。
要するに、postgresqlのデータベースを操作する(selectやdelete)のをpythonで実行できるようにしたツール

* Object Relational Mapperについて
Object　Relational Mapper/Object　Relational Mapping(ORM)はデータベースとオブジェクト指向プログラミング言語の間の非互換なデータを変換するプログラミング技法。



<a id="diagram"></a>

# 6.構成
## 構成図
ローカルマシンにdockerコンテナを作成する。
作成するコンテナはdatabase用とwepapp用の二つ
構成は下記のようになる。
![Untitled.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2496121/7e7c8444-18c5-4796-384e-0389acc7bd71.png)

## データベース

|Column|value|
|------|------|
|user_id|int|
|user_mail|str|
|user_name|str|
|user_password|str|

<a id="procedure"></a>

# 7.手順
## ディレクトリ構成
下記のようにディレクトリを構成する

```
--app--
|    |-app
|    |   |-router
|    |        |-user.py
|    |   
|    |-crud.py
|    |-database.py
|    |-Dockerfile
|    |-main.py
|    |-models.py
|    |-requirements.txt
|    |-schemas.py
|
|-docker-compose.yml
```

## コンテナの準備
### Dockerfile

```
FROM python:3


WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
RUN apt-get update && apt install -y tzdata && apt-get clean && rm -rf /var/lib/apt/lists/* 
ENV TZ Asia/Tokyo

COPY . .
```

### docker-compose.yml
```
version: '3'
services:
  webapp_fastapi:
    build: 
      context: ./app ## docker build コマンドを実行した時のカレントワーキングディレクリをビルドコンテキストと呼ぶ。
      dockerfile: Dockerfile
    volumes:
      - .:/usr/src/app
      - /var/run/docker.sock:/var/run/docker.sock
    command: uvicorn main:app --reload --workers 1 --host 0.0.0.0 --port 8000
    ports:
      - 8000:8000
    depends_on:
      - postgres_fastapi

  postgres_fastapi:
    image: postgres:13.1-alpine
    environment:
      TZ: Asia/Tokyo
      POSTGRES_DB: postgres_test
      POSTGRES_USER: postgres_test
      POSTGRES_PASSWORD: postgres_sample
      PGDATA: /var/lib/postgresql/data/pgdata
    volumes:
      - ./app:/var/lib/postgresql/data
    ports:
      - 5432:5432
```

### models.py
postgresqlのテーブルのカラムの設定は下記の通り

```
from sqlalchemy import String, Column, Integer
from sqlalchemy.ext.declarative import declarative_base
from database import Base
from uuid import uuid4

# データモデルの作成
# sqlalchemyによってデータベースのテーブルが生成

class User(Base):
    __tablename__ = 'user'
    user_id = Column(Integer, primary_key=True, autoincrement= True)  # intで連番で採番される
    user_mail = Column(String)
    user_name = Column(String)
    user_password = Column(String)

```

### main.py

```
from typing import Optional

from fastapi import FastAPI
import models
import database
from sqlalchemy.orm import sessionmaker
from router import user


# DBコンテナとappコンテナが更新するために必要なもの

engine = database.engine
models.Base.metadata.create_all(engine)
DBsession = sessionmaker(bind=engine)
session = DBsession()

# パスオペレーションの設定
app = FastAPI()
app.include_router(user.router)
```

### crud.py
```
from sqlalchemy.orm import Session
import models
from sqlalchemy.exc import DBAPIError
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
from fastapi import HTTPException

# 概要
# dbに対して操作するコード

# user_idをキーにuserの情報を取得する
def get_user(session:Session, user_id:int) -> any:
    response =  session.query(models.User).filter(models.User.user_id == user_id).first()
    if response is None:
        raise HTTPException(status_code=404, detail="user_id is not found")

    return response

# userを新規作成すeる
def create_user(session, user_name:str, user_mail:str ,user_password: str) -> any:
    user_obj = models.User(
        user_mail =  user_mail,
        user_name =  user_name,
        user_password = user_password
    )
    try:
        session.add(user_obj)
    except:
        raise HTTPException(status_code=500, detail='error')
    session.commit()
    session.refresh(user_obj)
    return user_obj

    

# userのpasswordを更新する。
def update_password(session:Session, user_id:int, user_mail:str,user_password:str) -> any:
    response = get_user(session,user_id)
    # user_idがある場合のみの処理でよい。
    # user_idがない場合は、get_user関数でHTTPexceptionが返るので、この関数内では不要。
    if response:
        if response.user_mail == user_mail:
            response.user_password = user_password
            session.commit()
            # コミットしたresponseを取得するには、再度sessionを取得する必要がある。
            session.refresh(response)
            return response
        elif response.user_mail != user_mail:
            raise HTTPException(status_code=404, detail='user_mail is wrong')


# userを削除する
def delete_user(session:Session, user_id:int,user_mail:str, user_password:str):
    # user_idがデータベースに存在しているかを確認する
    response = get_user(session, user_id)
    # user_idがデータベースに存在している場合
    if response:
        # idのuserのデータが空ではなく、かつ、malとpasswordともに合致しているか
        if response.user_mail == user_mail and response.user_password == user_password:
            session.delete(response)
            session.commit()

            return response
        else:
            raise HTTPException(status_code=404, detail="not match")
```

### user.py
```
from fastapi import APIRouter,Depends, HTTPException
from database import SessionLocal, engine
import models,crud
import schemas
from sqlalchemy.orm import Session
import typing

# 概要
# APIの受け口

# 依存関係を作成する
# リクエスト毎に独室したデータベースセッション(データベースへの接続)を持つ必要がある。
models.Base.metadata.create_all(bind = engine)

router = APIRouter()

def get_db():
    db = SessionLocal()
    try:
        yield db # returnではなくyieldがを記載することで繰り返し利用することが可能。
    finally:
        db.close

# ユーザーを取得する
@router.get("/user/get")
def read_user(user_id:int, db:Session=Depends(get_db)) -> any:
    return crud.get_user(db, user_id)

    # idをキーに取得したデータを返す

# ユーザーを新規作成
@router.post("/user/create")
def create_user(user_name:str,user_mail:str, user_password:str, db:Session = Depends(get_db)):
    return  crud.create_user(db, user_name,user_mail, user_password)


# passwordを更新する。
@router.put("/user/update")
def update_password(user_id:int, user_mail:str, user_password:str, db:Session = Depends(get_db) ):
    return crud.update_password(db,user_id, user_mail,user_password)
    
# ユーザーを削除する
@router.delete("/user/delete")
def delete_user(user_id:int, user_mail:str, user_password:str, db:Session = Depends(get_db)):
    return crud.delete_user(db, user_id,user_mail, user_password)
```


<a id="check"></a>

# 8. 確認
## 作成
http:127.0.0.1/docsから下記内容でuserを作成する。

項目|内容
-|-
name|nobunaga_oda
mail|nobunaga.oda@mail
password|owari

キャプチャの通り、crud.py上で記載したレスポンスが返ってきている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2496121/50454638-315e-1344-3907-fa81abd314e2.png)


## 取得
### 成功する場合
idを引き数に作成したユーザー情報が出力されるかを確認する。
先ほど作成したidで実行し、ユーザー情報が返されている。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2496121/56827132-2e7b-f9e2-0a10-1f9f5c611690.png)


## 失敗する場合
データベースに登録されていないユーザーidを入力する。
[2_user_data]

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2496121/ed254be2-4fa5-d406-41c7-3704099cd7ed.png)


下記の通り、レスポンスが返ってくる。
response bodyには。HHTPExcetptionのdetailが返ってくる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2496121/b831f6fa-d4db-43cf-b357-791e49bfe206.png)

## 更新
passwordの更新を行う。
### 失敗する場合
* 間違ったmailを入力し、"user_mai wrong"が返ってくるかを確認


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2496121/b68371eb-50c8-e7d6-e428-031df8d77da7.png)


* 間違ったidを入力し、"user_id is not found"が返ってくるかを確認


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2496121/bb6bcae1-76b8-34c3-29c6-701032448095.png)


### 成功する場合
正しい情報を入力し、PWが更新されるかを確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2496121/c987ff1f-5c25-c3b4-ebea-7f53e254380f.png)

## 削除
別途ユーザーを作成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2496121/d99547e3-bb71-030d-46c4-942bc22019c8.png)

削除するユーザー情報の確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2496121/187111b3-341d-d37b-3bb7-6868c6e49b4c.png)

##  失敗する場合
* user_idがない場合

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2496121/ba6b3bfd-ae1b-c7c5-098b-76b7c9465009.png)


*  mailが間違っている場合

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2496121/e1a6717f-b29e-4604-2df8-21ddb86b9dae.png)


* passwordが間違っている場合

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2496121/ec37abb5-9765-972b-a1fc-d85469644ca1.png)

##  成功する場合
* 削除成功のレスポンス

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2496121/da3c950f-9933-ca6a-97b6-ef2e19abe224.png)

* 削除したuser_idでuser情報の取得。削除されているので見つからない。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2496121/5aa0d838-7dee-c392-7fc8-dfa7ea76757e.png)

<a id="suumary"></a>
# まとめ
main.pyでfastapiの実行ファイルをルーティングしている。
今回は、routerディレクトリ配下のuser.pyのみ。
user.pyは、http://127.0.0.1/docsで表示される。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2496121/e751a4fc-8f16-4eff-0967-8fe573727f38.png)


fastapiのuser.pyをうけて、crud.pyを実行する。
今回は、ユーザーのデータベースのみで、crud.pyからデータベースに対してアクセスし、削除、更新、作成を行う。


<a id="reference"></a>
# 参考リンク
* docker(https://www.docker.com/resources/what-container/)
* 仮想化について：(https://www.itmanage.co.jp/column/virtualization-server-integration/)
* Fastapi:(https://fastapi.tiangolo.com/ja/)
* docker-composeの環境構築:(https://python-to.hateblo.jp/entry/2021/09/08/000000)
* https://qiita.com/AQUA651/items/b30eaffbc77a252c3c5b