mongoengineでmongodbにデータを作成する

2022/10/12に公開

初めに

普段APIの開発や運用をしていますが、APIがWebアプリでどのように利用されているかを全くイメージできませんでした。
そのため、APIをいくつか作成し、簡単なWebページをhtmlで実装して連携させてみたいと思います。

概要

下記のようなwebページを作りたいです。

  • ユーザー登録ページ
  • ログインページ
  • マイページ

そのために、数回に分けて下記のAPIと、それらを使用したwebページを作成していきます。

  • ユーザーAPI
    • 作成機能 <-今ここ
    • 情報取得機能
    • 情報削除機能
  • cookieAPI
    • 作成機能
  • 現在のユーザーAPI
    • 取得機能

今回はユーザー作成APIのための、ユーザー情報をDBに作成する機能を作成していきます。

この記事のゴール

ユーザー情報をDBに作成する機能を実装したいです。
ユーザー情報をMongoDBで管理することを考えています。
DBの操作はmongoengineを使用します。
ユーザーのパスワードはハッシュ化したいです。

完了条件は下記の通りです。

  • ユーザー情報をDBに作成できる
  • DBに作成されるユーザードキュメントにおけるパスワードはハッシュ化されている

使用するツール

pipenv で python 3.10の仮想環境を作成します。

pipenv --python 3.10

実行環境

OS : macOS Monterey 12.6
使用言語 : python 3.10

ディレクトリ構成

.
├── Pipfile
├── Pipfile.lock
├── app
│   ├── __init__.py
│   ├── core
│   │   ├── __init__.py
│   │   ├── config.py
│   │   └── security.py
│   ├── crud
│   │   ├── __init__.py
│   │   └── crud_user.py
│   ├── models
│   │   ├── __init__.py
│   │   └── user.py
│   ├── schemas
│   │   ├── __init__.py
│   │   └── user.py
│   └── tests
│       ├── __init__.py
│       ├── conftest.py
│       └── crud
│           ├── __init__.py
│           └── test_user.py
├── container_setup.sh
├── docker-compose.yaml
└── requirements.txt

テストの作成

ユーザー情報をDBに作成する機能を実装するために、テストを作成します。

app/tests/crud/test_user.py
from app import crud
from app.schemas.user import UserCreate


def test_create_user(random_email, random_lower_string) -> None:
    email = random_email()              # ランダムなemailアドレスを作成する関数
    password = random_lower_string()    # ランダムな文字列を作成する関数
    user_in = UserCreate(email=email, password=password)
    user = crud.user.create(obj_in=user_in)
    assert user.email == email
    assert hasattr(user, "hashed_password")

mongoDBのドキュメントモデルを作成する

ユーザードキュメントが持つことができるフィールドと、格納するデータの種類を定義します。

app/models/user.py
from mongoengine import Document
from mongoengine.fields import StringField, BooleanField, EmailField


class User(Document):
    uuid = StringField(unique=True, required=True)
    email = EmailField(unique=True, required=True)
    hashed_password = StringField(required=True)
    is_active = BooleanField(required=True)
    is_superuser = BooleanField(required=True)

    meta = {
        'db_alias': 'mongodb',          # ドキュメントのエイリアスを定義
        'collection': 'my_collection',  # ドキュメントを作成するコレクションを定義
    }

ユーザー作成時の入力値の型を設定する

BaseModelを使用して、ユーザー作成時の入力値の型を設定します。

app/schemas/user.py
from typing import Optional
from pydantic import BaseModel, EmailStr


# ユーザーを操作する際の共通の項目と型
class UserBase(BaseModel):
    email: Optional[EmailStr] = None
    is_active: Optional[bool] = True
    is_superuser: bool = False


# ユーザーを作成する際の入力項目の型
class UserCreate(UserBase):
    email: EmailStr
    password: str

ユーザー情報をDBに作成する機能を実装する

uuidは情報をDBに登録する際に自動生成します。

app/crud/crud_user.py
from uuid import uuid4

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


class CRUDUser:
    def create(self, obj_in: UserCreate) -> User:
        db_obj = User(
            uuid=uuid4().hex,
            email=obj_in.email,
            hashed_password=obj_in.password,
            is_active=obj_in.is_active,
            is_superuser=obj_in.is_superuser,
        )
        db_obj.save()

        return db_obj


user = CRUDUser()
app/crud/__init__.py
from .crud_user import user  # noqa

DBに接続する際に必要な値を設定ファイルに記載する

DBに接続する際に必要な値を設定ファイルに記載します。

app/core/config.py
from pydantic import BaseSettings


class Settings(BaseSettings):
    # datastore settings
    DB_USER: str
    DB_PASSWORD: str
    DB_HOST: str
    DB_PORT: int
    DB_NAME: str = 'my_db'


settings = Settings()

テストに必要な処理を作成する

テストに必要な処理をconftest.pyに記載します。

app/tests/conftest.py
import random
import string
import pytest

from mongoengine import connect, disconnect

from app.core.config import settings

# ランダムな文字列を生成する関数
def _random_lower_string():
    return "".join(random.choices(string.ascii_lowercase, k=32))


@pytest.fixture(scope='function')
def random_lower_string():
    return _random_lower_string()


@pytest.fixture(scope='function')
def random_email():
    return f"{_random_lower_string()}@{_random_lower_string()}.com"


# dbへの接続と切断を行うpytest fixture
@pytest.fixture(scope='session', autouse=True)
def init_db():
    connect(
        host=settings.DB_HOST,
        port=settings.DB_PORT,
        username=settings.DB_USER,
        password=settings.DB_PASSWORD,
        db=settings.DB_NAME,
        uuidRepresentation="standard",
        alias='mongodb'
    )
    yield
    disconnect(alias='mongodb')

動作確認用のコンテナを作成する

作成した機能の動作確認をするために、mongoDBとそのデータを見るためのツールのコンテナを作成します。
${}の値は.envファイルから読み込まれます。

docker-compose.yaml
version: "3.8"

services:
  mongo:
    image: mongo:5.0
    restart: always
    ports:
      - "${DB_PORT}:${DB_PORT}"
    environment:
      MONGO_INITDB_ROOT_USERNAME: ${DB_USER}
      MONGO_INITDB_ROOT_PASSWORD: ${DB_PASSWORD}
    volumes:
      - mongo-database:/database

  mongo_express:
    image: mongo-express:1.0.0-alpha.4
    restart: always
    ports:
      - "8081:8081"
    environment:
      ME_CONFIG_MONGODB_ADMINUSERNAME: ${DB_USER}
      ME_CONFIG_MONGODB_ADMINPASSWORD: ${DB_PASSWORD}
      ME_CONFIG_MONGODB_SERVER: mongo
    depends_on:
      - mongo # mongoコンテナが起動してから起動させる
volumes:
  mongo-database:

コンテナの作成と.envファイルの作成を行うshellスクリプトを作成します。

container_setup.sh
#!/bin/sh

echo DB_USER=root >.env
echo DB_PASSWORD=password >>.env
echo DB_HOST=localhost >>.env
echo DB_PORT=27017 >>.env

docker-compose up -d

schellスクリプトを実行します。

sh container_setup.sh

テストの実行

仮想環境を作成します。

pipenv --python 3.10

テスト実行に必要なパッケージを記載します。

requirements.txt
mongoengine
pydantic[email]
typing

# test package
pytest

テスト実行に必要なパッケージをインストールします。

pipenv install -r requirements.txt

作成したテストを実行します。

% pipenv run pytest
Loading .env environment variables...

~~~

app/tests/crud/test_user.py . 

テストが成功しました。
dbに作成されたデータを確認してみます。

ブラウザでhttp://localhost:8081にアクセスすると
mongo-express(mongodbのデータを表示するツールに)のUIが表示されます。
このmy_dbmy_collectionを確認するとデータが登録されていることがわかります。

パスワードのハッシュ化

パスワードのハッシュ化を実装するためにテストを変更します。

app/tests/crud/test_user.py
from app import crud
from app.schemas.user import UserCreate


def test_create_user(random_email, random_lower_string) -> None:
    email = random_email                # ランダムなemailアドレスを作成する関数
    password = random_lower_string      # ランダムな文字列を作成する関数
    user_in = UserCreate(email=email, password=password)
    user = crud.user.create(obj_in=user_in)
    assert user.email == email
    assert hasattr(user, "hashed_password")
+    assert user_in.password != user.hashed_password

入力したパスワードが平文でDBに保存されていないことをテストします。
もちろんテストは失敗します。

% pipenv run pytest
Loading .env environment variables...

~~~

app/tests/crud/test_user.py F 

ハッシュ化する機能を実装するために、文字列のハッシュ化を行う関数を作成します。

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


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


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

パスワードをハッシュ化する機能を、ユーザー情報作成機能に実装します。

from uuid import uuid4

from app.models.user import User
from app.schemas.user import UserCreate
+from app.core.security import get_password_hash


class CRUDUser:
    def create(self, obj_in: UserCreate) -> User:
        db_obj = User(
            uuid=uuid4().hex,
            email=obj_in.email,
+            hashed_password=get_password_hash(obj_in.password),
            is_active=obj_in.is_active,
            is_superuser=obj_in.is_superuser,
        )
        db_obj.save()

        return db_obj


user = CRUDUser()

動作に必要なパッケージを追加し、インストールします。

requirements.txt
mongoengine
pydantic[email]
typing
+passlib[bcrypt]

# test package
pytest
pipenv install -r requirements.txt
pipenv run pytest

テストが成功したので実装は完了です。

備考

作成した仮想環境やコンテナ、コンテナイメージは下記のコマンドで削除できます。

pipenv --rm
docker-compose down --rmi all --volumes --remove-orphans

Discussion