100日後にRustをちょっと知ってる人になる: [Day 96]書籍: Webアプリ開発で学ぶRust言語入門 その6
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を実装する
- 4.1 データベース基礎
- 4.2 sqlxとは
- 4.3 axumとsqlx
- 4.4 todoのCRUD
- 4.5 sqlxのテスト
-
第 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()
マクロで表しているだけですが、この部分の実装を完成させて次回 データベース接続を完成させてみたいと思っています。
Discussion