(WIP) ormxとaxumで作るCRUD APIの実装例.
(WIP) ormxとaxumで作るCRUD APIの実装例
ormxとaxumでCRUD APIを実装することを考えます. リポジトリーパターンでCRUD APIを実装するとき, リポジトリのインターフェースはtraitで表そうとした時にどうなるのか考えています. 現状はこうしていますという具体例を貼っておきます.
エンティティとモデル変換の実装パターン
- Repoトレイト
Document<T>
はidやメタデータと実際のデータを分けたかったので作ったが冗長かも- モデルから関連モデルを除外したものが更新用のモデル
async fn
は-> impl Future<..>
のsugar syntax, RPTITs が1.75で安定化したが, async関数を持つtraitをBox<dyn T>
として構造体に持たせることが出来ない- object-safe にならないので
- コメント欄で詳説されていた → Rust 1.75でtraitでasync fnが書けるようになったらしいよ
- 引き続き
async_trait::async_trait
マクロを使う
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Document<T> {
pub id: uuid::Uuid,
pub data: T,
}
#[async_trait]
pub trait Repo<T: serde::Serialize + for<'de> serde::Deserialize<'de>, UpdateT = T> {
async fn retrieve_list(&self) -> anyhow::Result<Vec<Document<T>>>;
async fn retrieve(&self, id: &uuid::Uuid) -> anyhow::Result<Document<T>>;
async fn insert(&self, doc: UpdateT) -> anyhow::Result<Document<T>>;
async fn update(&self, id: &uuid::Uuid, doc: UpdateT) -> anyhow::Result<Document<T>>;
async fn delete(&self, id: &uuid::Uuid) -> anyhow::Result<()>;
}
pub type BoxedRepo<T, UpdateT = T> = Box<dyn Repo<T, UpdateT> + Send + Sync>;
- Entity, ormxを使用
DATABASE_URL
が設定されていると定義書いている途中にsqlxがコンパイル時にDB見てテーブルが無いだの型が違うだの言ってくれてありがたいsqlx::types::Uuid
とuuid::Uuid
の変換に注意sqlx::types::Uuid::from_u128(uuid.as_u128())
uuid::Uuid::from_u128(uuid.as_u128())
#[ormx(get_one)]
や#[ormx(get_many)]
をフィールドにつけるとそのフィールドで取得する関数が生える https://docs.rs/ormx/latest/ormx/derive.Table.html#accessors-getters
#[derive(Debug, ormx::Table)]
#[ormx(table = "user_accounts", id = id, insertable)]
pub struct UserAccountEntity {
#[ormx(column = "id", default)]
pub id: Uuid,
....
pub resources_id: Uuid,
}
# 更新用のエンティティ
#[derive(Debug, ormx::Patch)]
#[ormx(table_name = "user_accounts", table = UserAccountEntity, id = "id")]
pub struct UpdateUserAccountEntity {}
# 更新用のモデル
#[derive(Debug, serde::Deserialize)]
pub struct UpdateUserAccount {}
impl From<UpdateUserAccount> for InsertUserAccountEntity {}
impl From<UpdateUserAccount> for UpdateUserAccountEntity {}
- Repo実装
- 更新用モデルに
From
を実装しているので楽 - ちなみにコネクションプールは
Arc<>
What happen when a pool is cloned? · launchbadge/sqlx · Discussion #917 futures::future::try_join_all(iter_fut)
でVec<Result<Future<>>>
→Future<Result<Vec<>>>
に変換出来る https://docs.rs/futures/latest/futures/future/fn.try_join_all.html
- 更新用モデルに
pub struct UserAccountRepo {
pub pool: sqlx::PgPool,
pub resources_repo: BoxedRepo<Resources>,
}
type Entity = UserAccountEntity;
type InsertEntity = InsertUserAccountEntity;
type UpdateEntity = UpdateUserAccountEntity;
#[async_trait]
impl Repo<UserAccount, UpdateUserAccount> for UserAccountRepo {
async fn retrieve_list(&self) -> anyhow::Result<Vec<Document<UserAccount>>> {
try_join_all(
Entity::all(&self.pool)
.await?
.into_iter()
.map(|e| self.into_model(e)),
)
.await
}
...
async fn update(
&self,
id: &uuid::Uuid,
doc: UpdateUserAccount,
) -> anyhow::Result<Document<UserAccount>> {
let mut conn = self.pool.acquire().await?;
let mut e = Entity::get(&mut conn, from_uuid(id.clone())).await?;
e.patch(&mut conn, UpdateEntity::from(doc)).await?;
self.into_model(e).await
}
}
- Entityからモデルへの変換
- 変換時に関連モデルをRepo経由で取得する(のでasync)
- これderive macroとかに出来たらうれしいかも
#[async_trait]
trait IntoModel<E, M> {
async fn into_model(&self, value: E) -> Result<M>;
}
#[async_trait]
impl IntoModel<UserAccountEntity, Document<UserAccount>> for UserAccountRepo {
async fn into_model(&self, entity: UserAccountEntity) -> Result<Document<UserAccount>> {
Ok(Document::new(
to_uuid(entity.id),
UserAccount {
...
resources: self
.resources_repo
.retrieve(&to_uuid(entity.resources_id))
.await?,
...
},
))
}
}
- コントローラ
pub async fn create_user_account(
State(ctx): State<Ctx>,
Json(data): Json<CreateUserAccount>,
) -> impl IntoResponse {
let rr = ResourcesRepo(ctx.pool.clone());
let resources = rr.insert(Resources::default()).await.unwrap();
let uar = UserAccountRepo::new(ctx.pool.clone());
let data = UpdateUserAccount::new(data.name, resources.id);
let doc = uar.insert(data).await.unwrap();
(StatusCode::CREATED, Json(doc))
}