概要
前回と前々回を通して、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
と、FileBase
、ResultBase
が存在します。
これらがそれぞれ先述のコレクションの中身になっています。
今回は、ファイルを分けるところまでやらなかったため、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
に対しても、
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
今回は、きちんと router
とcruds
に機能を分散せず、
多くを router
に直接書いています。
全てのエンドポイントに対して解説するのもあまり意味がないので、
以下のエンドポイントに対して少し詳細を見てみます。
@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を用いています)
を追加したあと、アップデートをかけています。
次に、result
をfile
に追加する時です。
@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
を選びます。
選んだ後は、user
にfile
を追加した時と同じように、
file
にresult
を追加し、DBをアップデートします。
データの挿入ができたら、SwaggerUIを使って実際にデータが挿入できているかを
GETリクエストをかけて確認してみましょう。
できていたら簡易的な実装はこれで完了です。お疲れ様でした。
まとめ
今回は
FastAPI + Motor (Mongodb ドライバ) + MongoDB
の構成で、コレクションがネストされた構成のデータの読み書きを行ってみました。
案外このような構成はすぐには見つからなかったので、
MongoDBとFastAPIを組み合わせて見ようと思っている方がいらっしゃったら参考になれば幸いです。
これからもFastAPIなどのバックエンドを用いていろいろ開発したりできたらなと思います。
Prismaとかも聞いたことしかないので触ってみようかな。。
今回はこの辺で。