Skip to content

Commit

Permalink
add chapter4 & chapter5 intro
Browse files Browse the repository at this point in the history
  • Loading branch information
noknmgc committed Aug 22, 2023
1 parent 4c62b32 commit fc8cb21
Show file tree
Hide file tree
Showing 2 changed files with 368 additions and 0 deletions.
356 changes: 356 additions & 0 deletions FastAPI/chapters/chapter4.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@
- [DB 接続クラス](#db-接続クラス)
- [DB モデルの定義](#db-モデルの定義)
- [DB マイグレーション](#db-マイグレーション)
- [CRUDsの実装](#crudsの実装)
- [テスト用ユーザーデータの登録(スキップ可)](#テスト用ユーザーデータの登録スキップ可)
- [usersエンドポイントの実装](#usersエンドポイントの実装)
- [POST /users](#post-users)
- [GET /users](#get-users)
- [PUT /users/{signin\_id}](#put-userssignin_id)
- [DELETE /users/{signin\_id}](#delete-userssignin_id)
- [テスト](#テスト)
- [Next: Chapter5 セキュリティの実装](#next-chapter5-セキュリティの実装)
- [Prev: Chapter3 エンドポイントの作成](#prev-chapter3-エンドポイントの作成)

## ライブラリのインストール

Expand Down Expand Up @@ -195,3 +205,349 @@ python -m app.migrate
```sql
SELECT * FROM "user";
```

## CRUDsの実装
ここでは、DBのCRUD(IO処理)を記述していきます。CRUDとは、永続的なデータを取り扱うソフトウェアに要求される4つの基本機能である、データの作成(Create)、読み出し(Read)、更新(Update)、削除(Delete)の頭文字を繋げた言葉です。`app/crud`ディレクトリにファイルを作成します。
ここでは、CRUD操作のベースとなるクラスを定義します。以下のファイルを作成してください。
※難しい場合は、個別に(UserのCreate、Read、Update、Deleteの4つ)CRUDの関数を実装してください。

`app/crud/base.py`

```python
from typing import Any, Dict, Generic, List, Optional, Type, TypeVar

from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
from sqlalchemy.orm import Session

from app.db.base import Base

ModelType = TypeVar("ModelType", bound=Base)
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)


class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
def __init__(self, model: Type[ModelType]):
"""
CRUD object with default methods to Create, Read, Update, Delete (CRUD).
**Parameters**
* `model`: A SQLAlchemy model class
* `schema`: A Pydantic model (schema) class
"""
self.model = model

def create(self, db: Session, obj_in: CreateSchemaType) -> ModelType:
obj_in_data = jsonable_encoder(obj_in)
db_obj = self.model(**obj_in_data) # type: ignore
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj

def read(self, db: Session, id: Any) -> Optional[ModelType]:
return db.query(self.model).filter(self.model.id == id).first()

def read_multi(
self, db: Session, skip: int = 0, limit: int = 100
) -> List[ModelType]:
return db.query(self.model).offset(skip).limit(limit).all()

def update(
self,
db: Session,
db_obj: ModelType,
obj_in: UpdateSchemaType | Dict[str, Any],
) -> ModelType:
obj_data = jsonable_encoder(db_obj)
if isinstance(obj_in, dict):
update_data = obj_in
else:
update_data = obj_in.dict(exclude_unset=True)
for field in obj_data:
if field in update_data:
setattr(db_obj, field, update_data[field])
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj

def delete(self, db: Session, id: int) -> ModelType:
obj = db.query(self.model).get(id)
db.delete(obj)
db.commit()
return obj
```

それぞれのモデルに対して、CRUD操作を行うクラスを定義するのですが、そのクラスは、この`CRUDBase`クラスを継承し、必要に応じて、CRUD操作をオーバーライドしたり、新しくメソッドを定義したりします。

それでは、`User`モデルに対するCRUD操作を行うクラスを定義しましょう。DBでは、Userのパスワードは、そのまま保存せず、ハッシュ化して保存します。そのため、CreateやUpdateの際、リクエストの`password`をハッシュ化し、`hashed_password`に保存する必要があります。また、Readの際も、DBのidではなく、`signin_id`で読み込めるようにします。

`User`モデルのCRUD操作を行うクラスは、以下のようになります。ファイルを作成してください。

`app/crud/crud_user.py`

```python
from typing import Dict, Any, Optional

from sqlalchemy.orm import Session

from app.models.user import User
from app.schemas.user import UserCreate, UserUpdate

from app.crud.base import CRUDBase


class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]):
def read_by_signin_id(self, db: Session, signin_id: str) -> Optional[User]:
return db.query(User).filter(User.signin_id == signin_id).first()

def create(self, db: Session, user_create: UserCreate) -> User:
# userはpasswordをhashed passwordにするので、CRUDBaseのcreateはオーバーライド
user_create_dict = self.__hash_password(user_create)
db_obj = self.model(**user_create_dict)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj

def update(self, db: Session, user_update: UserUpdate, db_obj: User) -> User:
# userはpasswordをhashed passwordにするので、CRUDBaseのupdateはオーバーライド
user_update_dict = self.__hash_password(user_update)

db_obj = super().update(db, db_obj, user_update_dict)
return db_obj

def __hash_password(self, user_schema: UserCreate | UserUpdate) -> Dict[str, Any]:
user_dict: Dict[str, Any] = {}
for field, value in user_schema:
if value is None:
continue
if field == "password":
user_dict["hashed_password"] = value + "password"
else:
user_dict[field] = value
return user_dict


user = CRUDUser(User)

```

このCRUD操作をパスオペレーション関数で使う際に、CRUD操作であることを即座にわかるようにしたいので、`crud.user.read(...)`のようにしたいです。そのため、`app/crud/__init__.py`を以下のように編集します。

`app/crud/__init__.py`
```python
from .crud_user import user
```

ここでは、パスワードのハッシュ化が仮の物になっているので、次はパスワードのハッシュ化を実装します。パスワードのハッシュ化には、`passlib`を使います。また、ハッシュ化には`bcrypt`も使用します。以下のコマンドでインストールしてください。

```bash
pip install passlib
```

```bash
pip install bcrypt
```

パスワードのハッシュ化やこの後実装するJson Web Tokenの発行などは、`app/core/security.py`に記述します。パスワードのハッシュ化と同時にパスワードの検証も実装します。以下のファイルを作成してください。

`app/core/security.py`
```python
from passlib.context import CryptContext

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")


def get_password_hash(password: str) -> str:
return pwd_context.hash(password)


def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
```

ここで作成した`get_password_hash``CRUDUser`で使います。`app/crud/crud_user.py`に定義した`CRUDUser`クラスの`__hash_password`メソッドを以下のように編集してください。

`app/crud/crud_user.py`
```python
from app.core.security import get_password_hash


class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]):
...

def __hash_password(self, user_schema: UserCreate | UserUpdate) -> Dict[str, Any]:
user_dict: Dict[str, Any] = {}
for field, value in user_schema:
if value is None:
continue
if field == "password":
user_dict["hashed_password"] = get_password_hash(value)
else:
user_dict[field] = value
return user_dict
```

## テスト用ユーザーデータの登録(スキップ可)

パスオペレーション関数の実装の前に、テスト用にユーザーを登録するスクリプトを作成しましょう。先ほど実装した`CRUDUser`を使います。以下のファイルを作成してください。

`app/create_initial_data.py`
```python
from app.db.session import SessionLocal
from app.schemas.user import UserCreate
from app import crud


initial_data = [
UserCreate(signin_id="tarou", password="tarou", name="太郎"),
UserCreate(signin_id="john", password="john"),
UserCreate(signin_id="admin", password="password", role="Admin"),
]


def init() -> None:
db = SessionLocal()
for data in initial_data:
crud.user.create(db, data)


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

それでは、実行してみましょう。FastAPIのサーバーは立てる必要はありません。DBのみで良いです。

```bash
python -m app.create_initial_data
```

実行後、反映されているか以下のSQL文を実行して確認してみましょう。

```sql
SELECT * FROM "user";
```

## usersエンドポイントの実装

ここからは、Chapter3で作成したエンドポイントをDBと連携したものに書き換えていきましょう。

まず、`app/schemas/user.py`に定義したレスポンススキーマ`UserResponse`を以下のように変更します。

`app/schemas/user.py`
```python
class UserResponse(BaseModel):
signin_id: str
name: str
role: str

class Config:
orm_mode = True
```

こうすることで、DBのモデルである`User`をスキーマの`UserResponse`に変換できるようになります。それでは、それぞれのエンドポイントを書き換えていきましょう。ここからは、`app/api/endpoints/users.py`に定義した擬似的なDB`fake_user_db`は使わないので、削除してください。また、同じファイルで定義している`User`も使わないので、これも削除してください。

`app/api/endpoints/users.py`

```python
# 以下は削除。
from pydantic import BaseModel


class User(BaseModel):
signin_id: str
password: str
name: str
role: str


fake_user_db = [User(signin_id="tarou", password="tarou", name="太郎", role="User")]

```

ここからは、DBとの接続も行うので、`models``crud`が必要になります。また、それぞれのパスオペレーション関数の引数に`db: Session = Depends(get_db)`が必要となります。importを以下のように編集してください。

```python
from typing import List

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session

from app.api.deps import get_db
from app import crud, schemas, models
```

`app/api/endpoints/users.py`に定義したパスオペレーション関数をそれぞれ編集してください。

### POST /users

```python
@router.post("", response_model=schemas.UserResponse)
def create_user(
user_create: schemas.UserCreate,
db: Session = Depends(get_db),
):
user = crud.user.read_by_signin_id(db, user_create.signin_id)
if user:
raise HTTPException(
status_code=400,
detail="The id already exists in the system.",
)
user = crud.user.create(db, user_create)
return user
```

### GET /users

```python
@router.get("", response_model=List[schemas.UserResponse])
def read_all_users(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
):
users = crud.user.read_multi(db, skip=skip, limit=limit)
return users
```


### PUT /users/{signin_id}

```python
@router.put("/{signin_id}", response_model=schemas.UserResponse)
def update_user(
signin_id: str,
user_update: schemas.UserUpdate,
db: Session = Depends(get_db),
):
db_obj = crud.user.read_by_signin_id(db, signin_id)
if db_obj is None:
raise HTTPException(status_code=404, detail="User not found")
user = crud.user.update(db, user_update, db_obj)
return user
```

### DELETE /users/{signin_id}

```python
@router.delete("/{signin_id}", response_model=None)
def delete_user(
signin_id: str,
db: Session = Depends(get_db),
):
user = crud.user.read_by_signin_id(db, signin_id)
if user is None:
raise HTTPException(status_code=404, detail="User not found")
crud.user.delete(db, user.id)
```

### テスト
全てのパスオペレーション関数を書き換えたら、サーバーを起動し、[Swagger UI](http://127.0.0.1:8000/docs)で色々試してみましょう。

## [Next: Chapter5 セキュリティの実装](../chapters/chapter5.md)

## [Prev: Chapter3 エンドポイントの作成](../chapters/chapter3.md)
12 changes: 12 additions & 0 deletions FastAPI/chapters/chapter5.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Chapter5 セキュリティの実装

ここでは、Json Web Tokenを利用したセキュリティを実装します。

## 目次

- [Chapter5 セキュリティの実装](#chapter5-セキュリティの実装)
- [目次](#目次)
- [Prev: Chapter4 DBとの連携](#prev-chapter4-dbとの連携)


## [Prev: Chapter4 DBとの連携](../chapters/chapter4.md)

0 comments on commit fc8cb21

Please sign in to comment.