100日後にRustをちょっと知ってる人になる: [Day 97]書籍: Webアプリ開発で学ぶRust言語入門 その7
Day 97 のテーマ
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.4 todoのCRUD
データベースの操作に関する実装は、sqlx を用いて行います。
sqlx 公式サンプル
書籍の方で参考にしている公式のサンプルを少し眺めてみたいと思います。
use sqlx::postgres::PgPoolOptions;
#[async_std::main]
async fn main() -> Result<(), sqlx::Error> {
let pool = PgPoolOptions::new()
.max_connections(5)
.connect("postgres://postgres:password@localhost/test").await?;
let row: (i64,) = sqlx::query_as("SELECT $1")
.bind(150_i64)
.fetch_one(&pool).await?;
assert_eq!(row.0, 150);
Ok(())
}
sqlx のクエリの実行は直感的に分かりやすい構造になっています。
-
query_as
- SQL 文字列の初期化
- sqlx::query_as
-
bind
- SQL 文字列中に定義しているパラメータに対する値のバインド
- pub fn bind<T>
-
fetch_one
- 該当するデータを 1 つのみ取得
-
pub async fn fetch_one<'e, 'c, E>
- 変更をじっこうする場合は
execute
, 該当レコードをStream
で取得するばあいはfetch
- 変更をじっこうする場合は
use futures::TryStreamExt;
let mut rows = sqlx::query("SELECT * FROM users WHERE email = ?")
.bind(email)
.fetch(&mut conn);
while let Some(row) = rows.try_next().await? {
let email: &str = row.try_get("email")?;
}
上記のように fetch
により取得した Row
を while
ループにより row.get()
でレコードを取得します。
Todo アプリケーションの実装
先日までに作成してきていた Todo アプリケーションのデータベース操作部分の実装を行います。
Create - データ挿入
まず、データの挿入操作についての実装を行います。SQL 文は次のようなものになります。戻り値を扱いたいため、returning *
を設定しています。
insert into todos (text, completed) values ($1, false) returning *
query_as::<_, Todo>
としているので、fetch_one
の結果の戻り値を Todo
にバインドしています。
async fn create(&self, payload: CreateTodo) -> anyhow::Result<Todo> {
let todo = sqlx::query_as::<_, Todo>(
r#"
insert into todos (text, completed)
values ($1, false)
returning *
"#,
)
.bind(payload.text.clone())
.fetch_one(&self.pool)
.await?;
Ok(todo)
}
Find - データ取得
データの取得用のメソッドを実装します。SQL 文は次のようなものになります。
select * from todos where id=$1
ID を指定して特定のレコードの取得を行います。そのため、SQL 文の実行は fetch_one
を使用します。
async fn find(&self, id: i32) -> anyhow::Result<Todo> {
let todo = sqlx::query_as::<_, Todo>(
r#"
select * from todos where id=$1
"#,
)
.bind(id)
.fetch_one(&self.pool)
.await
.map_err(|e| match e {
sqlx::Error::RowNotFound => RepositoryError::NotFound(id),
_ => RepositoryError::Unexpected(e.to_string()),
})?;
Ok(todo)
}
Update - データ更新
データ更新には次の SQL 文を使用します。
update todos set text=$1, completed=$2 where id=$3 returning *
更新前の状態を取得してから更新を行っています。
async fn update(&self, id: i32, payload: UpdateTodo) -> anyhow::Result<Todo> {
let old_todo = self.find(id).await?;
let todo = sqlx::query_as::<_, Todo>(
r#"
update todos set text=$1, completed=$2
where id=$3
returning *
"#,
)
.bind(payload.text.unwrap_or(old_todo.text))
.bind(payload.completed.unwrap_or(old_todo.completed))
.bind(id)
.fetch_one(&self.pool)
.await?;
Ok(todo)
}
Delete - データ削除
データの削除を行うには次の SQL 文を使用します。
delete from todos where id=$1
特定の ID を指定してデータを削除します。ここでは特に戻り値を期待しないので、execute
を使用しています。
async fn delete(&self, id: i32) -> anyhow::Result<()> {
sqlx::query(
r#"
delete from todos where id=$1
"#,
)
.bind(id)
.execute(&self.pool)
.await
.map_err(|e| match e {
sqlx::Error::RowNotFound => RepositoryError::NotFound(id),
_ => RepositoryError::Unexpected(e.to_string()),
})?;
Ok(())
}
Day 97 のまとめ
今回の実装で sqlx を使ったデータベース操作に関する実装を完成することができました。sqlx を用いると データベースに対する接続や SQL 文の実行など、かなりパターン化された実装をすることができることが分かりました。また、予め非同期対応されている点も Web アプリケーションとして使用するのにとても相性がいいかなと思います。
かなり実装周りはパターン化されてると思いますが、まだ今回のアプリケーションの実装でしか使ったことがないのでもう少し繰り返し使って慣れていきたいなと思いました。
Discussion