Posted on

(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> として構造体に持たせることが出来ない
#[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::Uuiduuid::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 {}
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))
}