LoginSignup
1
1

More than 1 year has passed since last update.

FastAPI + MongoDB のバックエンドAPIを構築し、ネストされたコレクションの読み書きをする

Last updated at Posted at 2022-12-05

概要

前回と前々回を通して、FastAPI + SQLAlchemyでRDSテーブルのリレーションいろいろ加味した
バックエンドAPIの構築を実装しました。

今回は、その続編という意味で、MongoDBとも繋げてみたので、
そのときの備忘録をまとめていきたいと思います。

この記事のプログラムは、

こちらにアップロードしています。

今回の構成

今回は、
ルートにあるコレクションをusersとし、
usersが持つドキュメント(各ユーザの情報)の中に、
ネストした形でfilesというコレクションがある、
さらに、それぞれの file に対して、results というコレクションがある、
という構成にします。

つまり、

users -- uid1 -- name
       |        |- files --- uid1 -- results -- uid1
       |                  |--uid2 
       |-uid2 --name
               |- files --- uid1
                         | --uid2

みたいな感じで、firebase のfirestore で情報をどんどんネストさせていくような構成を、
FastAPI + MongoDBで実装したいと思い、やってみた、という構成です。

Schemas

MongoDBのNoSQLの構成でも、
pydantic を用いたschema の構成を行います。

こちらのファイルを見ていただくと、

UserBase と、FileBaseResultBase が存在します。
これらがそれぞれ先述のコレクションの中身になっています。

今回は、ファイルを分けるところまでやらなかったため、user.py に全てのスキーマを
書いていますが、これらをそれぞれ別のファイルに分けることが望ましいかと思います。

api/schemas/user.py

class UserBase(BaseModel):
    id: id_model.PyObjectId = Field(default_factory=id_model.PyObjectId, alias="_id")
    name: str
    password: str
    files: Optional[List[dict]]

    class Config:
        allow_population_by_field_name = True
        arbitrary_types_allowed = True
        json_encoders = {ObjectId: str}
        schema_extra = {
            "example": {
                "name": "user",
                "password": "1234",
            }
        }

以上をみると、コレクションのネストについては、

files: Optional[List[dict]] の1行で定義しています。

同様に、FilesBaseに対しても、

api/schemas/user.py
class FileBase(BaseModel):
    id: id_model.PyObjectId = Field(default_factory=id_model.PyObjectId, alias="_id")
    path: str
    updated_ts: str
    results: Optional[dict]

    class Config:
        allow_population_by_field_name = True
        arbitrary_types_allowed = True
        json_encoders = {ObjectId: str}
        schema_extra = {
            "example": {
                "path": "1.png",
                "updated_ts": "2022-11-09",
            }
        }

というように resultsをネストしており、その対象はdictになっています。

Cruds

今回は、きちんと routercrudsに機能を分散せず、
多くを routerに直接書いています。

全てのエンドポイントに対して解説するのもあまり意味がないので、
以下のエンドポイントに対して少し詳細を見てみます。

api/routers/user.py
@router.put("/{id}/files", response_description="Add a file to user", response_model=user_schema.User)
async def update_user(id: str, user_add_file: user_schema.UserAddFileRequest = Body(...)):
    target_user = await db["users"].find_one({"_id": id})
    if target_user is None:
        raise HTTPException(status_code=404, detail=f"user {id} not found")
    file_to_be_added = jsonable_encoder(user_add_file)
    if target_user.get("files") is None:
        print("file is None")
        target_user["files"] = [file_to_be_added]
        update_result = await db["users"].update_one({"_id": id}, {"$set": target_user})
        changed_user = await db["users"].find_one({"_id": id})
        return changed_user
    else:
        assert(isinstance(target_user.get("files"), list))
        target_user.get("files").append(file_to_be_added)
        update_result = await db["users"].update_one({"_id": id}, {"$set": target_user})
        changed_user = await db["users"].find_one({"_id": id})
        return changed_user

読んだ通りのプログラムなのですが、
まず、ファイルを追加したいターゲットとなるユーザを取得し、
そのユーザ対して、Fileオブジェクトをエンコードしたもの(jsonable_encoderを用いています)
を追加したあと、アップデートをかけています。

次に、resultfileに追加する時です。

api/routers/user.py
@router.put("/{id}/files/{file_id}/results", response_description="Add a result to file", response_model=user_schema.User)
async def update_user(id: str, file_id: str, user_add_result: user_schema.UserAddResultRequest = Body(...)):
    target_user = await db["users"].find_one({"_id": id})
    if target_user is None:
        raise HTTPException(status_code=404, detail=f"user {id} not found")
    print("\n\nhere")
    print(target_user)
    result_to_be_added = jsonable_encoder(user_add_result)

    target_file = None
    if target_user.get("files") is None:
        raise HTTPException(status_code=404, detail=f"no file found for user {id}")
        print("file is None")
        target_user["files"] = [file_to_be_added]
        update_result = await db["users"].update_one({"_id": id}, {"$set": target_user})
        changed_user = await db["users"].find_one({"_id": id})
        return changed_user
    else:
        for i, file in enumerate(target_user.get("files")):
            if file.get("_id") == file_id:
                target_file = file
                break
        if target_file is None:
            raise HTTPException(status_code=404, detail=f"no such a file found for user {id}")
        else:
            if target_file.get("results") is None:
                #target_file["results"] = [result_to_be_added]
                #target_user["files"]["results"] = [result_to_be_added]
                target_user["files"][i]["results"] = [result_to_be_added]
            else:
                assert(isinstance(target_user["files"]["results"], list))
                target_user["files"][i]["results"].append(result_to_be_added)
        
        update_result = await db["users"].update_one({"_id": id}, {"$set": target_user})
        changed_user = await db["users"].find_one({"_id": id})
        return changed_user

無駄な書き方をしているかもしれないですが、
やっていることは単純です。

ターゲットとしているユーザが所持するfileを取得し、
それらのfileの中で、resultを追加するfileを選びます。

選んだ後は、userfileを追加した時と同じように、
fileresultを追加し、DBをアップデートします。

データの挿入ができたら、SwaggerUIを使って実際にデータが挿入できているかを
GETリクエストをかけて確認してみましょう。

できていたら簡易的な実装はこれで完了です。お疲れ様でした。

まとめ

今回は
FastAPI + Motor (Mongodb ドライバ) + MongoDB
の構成で、コレクションがネストされた構成のデータの読み書きを行ってみました。

案外このような構成はすぐには見つからなかったので、
MongoDBとFastAPIを組み合わせて見ようと思っている方がいらっしゃったら参考になれば幸いです。

これからもFastAPIなどのバックエンドを用いていろいろ開発したりできたらなと思います。
Prismaとかも聞いたことしかないので触ってみようかな。。

今回はこの辺で。

@kenmaro

1
1
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
1
1