🦀

100日後にRustをちょっと知ってる人になる: [Day 96]書籍: Webアプリ開発で学ぶRust言語入門 その6

2023/01/04に公開

Day 96 のテーマ

Day 91 から読み始めた Webアプリ開発で学ぶ Rust言語入門 ですが、年末年始休暇に入ってから少し滞っていました。本自体は読み終えたので、実際にコードも書きつつ復習してみたいと思います。

  • 第 1 章 RustとWeb開発
    • 1.1 Rustでの開発の準備
  • 第 2 章 Rust基礎
    • 2.1 変数とデータ型
    • 2.2 関数の実装
    • 2.3 制御構造
    • 2.4 所有権による安全性
    • 2.5 データ構造
    • 2.6 async/await
    • 2.7 クレートとモジュール
    • 2.8 テスト
    • 2.9 よく使うライブラリ
  • 第 3 章 axumを使ってhttpリクエストを処理する
  • 第 4 章 sqlxを使ってCRUDを実装する
  • 第 5 章 Todoアプリの体裁を整える
    • 5.1 フロントエンド開発
    • 5.2 React環境構築
    • 5.3 TodoアプリのUI実装
    • 5.4 外部APIとの通信(1)
    • 5.5 外部APIとの通信(2)
  • 第 6 章 Todoにラベルをつける
    • 6.1 ラベルのCRUD
    • 6.2 TodoRepositoryのラベル対応
    • 6.3 ラベル機能を画面に追加する
    • 6.4 さらなる機能拡張

第 4 章 sqlxを使ってCRUDを実装する - 4.3 axumとsqlx

Day 95 で確認をしてみた sqlx を使用してデータベースに情報を永続化できるようにアプリケーションを修正していきます。

sqlx の準備

Cargo.toml に依存関係を追加していきます。

  • sqlx
    • 以下のフィーチャー
      • postgres
      • any
      • runtime-tokio-rustls
  • dotenv
cargo add sqlx --featurescargo add sqlx --features "postgres,any,runtime-tokio-rustls"
cargo add dotenv

リポジトリの非同期対応

sqlx は非同期処理に対応をしています。

SQLx は非同期な SQL クレートで、DSL を使わずにコンパイル時にクエリをチェックするのが特徴です。

  • 最大の同時実行性を実現するために、async/await を使って構築されています。

作成していた以下のコードは非同期には非同期になっていません。これらのメソッドを非同期として定義したいため、#[async_trait] マクロを付け足します。

このマクロにより、async fn ... という記法ができるようになるので、以下のコードを修正します。

  • 修正前
pub trait TodoRepository: Clone + std::marker::Send + std::marker::Sync + 'static {
    fn create(&self, payload: CreateTodo) -> Todo;
    fn find(&self, id: i32) -> Option<Todo>;
    fn all(&self) -> Vec<Todo>;
    fn update(&self, id: i32, payload: UpdateTodo) -> anyhow::Result<Todo>;
    fn delete(&self, id: i32) -> anyhow::Result<()>;
}
  • 修正後
#[async_trait]
pub trait TodoRepository: Clone + std::marker::Send + std::marker::Sync + 'static {
    async fn create(&self, payload: CreateTodo) -> anyhow::Result<Todo>;
    async fn find(&self, id: i32) -> anyhow::Result<Todo>;
    async fn all(&self) -> anyhow::Result<Vec<Todo>>;
    async fn update(&self, id: i32, payload: UpdateTodo) -> anyhow::Result<Todo>;
    async fn delete(&self, id: i32) -> anyhow::Result<()>;
}

非同期対応にあわせて、SQL 実行に際して実際には SQL 実行時エラーも発生しうるので戻り値を anyhoe::Result 型にしています。

  • Todo から Result<Todo>
  • Vec<Todo> から Result<Vec<Todo>>

コンパイルエラー対応

メソッドを非同期対応すると、次のようなコンパイルエラーが発生するようになるため、それぞれ修正を行っていきます。

  • E0277
    • あるトレイトを実装していない型を、そのトレイトを期待する場所で使おうとした
  • E0599
    • メソッドを実装していない型に対してメソッドを使用した
  • E0195
    • メソッドのライフタイムパラメータが trait 宣言と一致しない
error[E0277]: the trait bound `(StatusCode, Json<Pin<Box<dyn Future<Output = Result<Todo, anyhow::Error>> + Send>>>): IntoResponse` is not satisfied
  --> src/handlers.rs:17:24
   |
17 |   ) -> impl IntoResponse {
   |  ________________________^
18 | |     let todo = repository.create(payload);
19 | |
20 | |     (StatusCode::CREATED, Json(todo))
21 | | }
   | |_^ the trait `IntoResponse` is not implemented for `(StatusCode, Json<Pin<Box<dyn Future<Output = Result<Todo, anyhow::Error>> + Send>>>)`
error[E0599]: no method named `or` found for struct `Pin<Box<dyn Future<Output = Result<Todo, anyhow::Error>> + Send>>` in the current scope
  --> src/handlers.rs:45:10
   |
45 |         .or(Err(StatusCode::NOT_FOUND))?;
   |          ^^ method not found in `Pin<Box<dyn Future<Output = Result<Todo, anyhow::Error>> + Send>>`
error[E0195]: lifetime parameters or bounds on method `create` do not match the trait declaration
  --> src/repositories.rs:91:14
   |
21 |     async fn create(&self, payload: CreateTodo) -> anyhow::Result<Todo>;
   |              ---------------------------------- lifetimes in impl do not match this method in trait
...
91 |     fn create(&self, payload: CreateTodo) -> Todo {
   |              ^ lifetimes do not match method in trait

ハンドラの修正

リポジトリの修正と同様な考え方で handlers.rs の修正を行います。

pub async fn create_todo<T: TodoRepository>(
    ValidatedJson(payload): ValidatedJson<CreateTodo>,
    Extension(repository): Extension<Arc<T>>,
) -> impl IntoResponse {
    let todo = repository.create(payload);

    (StatusCode::CREATED, Json(todo))
}
pub async fn create_todo<T: TodoRepository>(
    ValidatedJson(payload): ValidatedJson<CreateTodo>,
    Extension(repository): Extension<Arc<T>>,
) -> Result<impl IntoResponse, StatusCode> {
    let todo = repository
        .create(payload)
        .await
        .or(Err(StatusCode::NOT_FOUND))?;

    Ok((StatusCode::CREATED, Json(todo)))
}

データベース永続化処理の実装

いままではメモリ上のハッシュテーブルに対してデータ処理をおこなう、TodoRepositoryForMemory 作成し使用していました。ここでは、実際にDBに格納する TodoRepositoryForDb を実装していきます。

以下のようなインターフェースでデータベース処理を行うメソッドを用意していきます。(以下でメソッド内容は todo!() マクロで表しています)

#[derive(Debug, Clone)]
pub struct TodoRepositoryForDb {
    pool: PgPool,
}

#[async_trait]
impl TodoRepository for TodoRepositoryForDb {
    async fn create(&self, payload: CreateTodo) -> anyhow::Result<Todo> {
      todo!()
    }

    async fn find(&self, id: i32) -> anyhow::Result<Todo> {
        todo!()
    }

    async fn all(&self) -> anyhow::Result<Vec<Todo>> {
        todo!()
    }

    async fn update(&self, id: i32, payload: UpdateTodo) -> anyhow::Result<Todo> {
        todo!()
    }

    async fn delete(&self, id: i32) -> anyhow::Result<()> {
        todo!()
    }
}

Day 96 のまとめ

実際に Rust のコードを触ったのは、1 週間ぶりくらいではあったのですが、なんとか思い出しながらサンプルコードを修正していく事ができました。
今回は、メモリ上のハッシュコードに対するデータ処理から、実際のデータベースを用いたデータ処理への切り替えのためのリポジトリを準備していきました。最終的なデータベースに対する処理を発行する部分はまだ todo() マクロで表しているだけですが、この部分の実装を完成させて次回 データベース接続を完成させてみたいと思っています。

GitHubで編集を提案

Discussion