Rustで学ぶデータベース操作におけるエラーハンドリング設計ガイド

Rustでデータベース操作を行う際、エラーハンドリングは極めて重要です。Rustは安全性と効率性を重視する言語であり、特にデータベース操作では、接続エラー、クエリの失敗、データの不整合といった問題が発生する可能性があります。これらのエラーを適切に処理しないと、プログラムが予期せぬ動作をしたり、システム全体がクラッシュする危険性があります。

本記事では、Rustにおけるエラーハンドリングの基本概念から、データベース操作でよく発生するエラー、そして実践的なエラーハンドリング設計までを詳しく解説します。Result型やOption型の使い方、外部クレートを活用したエラー処理、非同期処理時の注意点、さらにはエラーリカバリの設計方法まで取り上げます。Rustで堅牢なデータベース操作を行いたい開発者にとって必見の内容です。

目次

Rustにおけるエラーハンドリングの基本概念


Rustでは、エラー処理のために主にResult型とOption型が利用されます。これらの型は、エラーが発生する可能性のある処理を安全に管理するための仕組みです。

`Result`型の概要


Result型は、処理が成功するか失敗するかを明示的に示します。以下のように定義されています。

enum Result<T, E> {
    Ok(T),      // 成功時の値
    Err(E),     // 失敗時のエラー値
}

使用例

fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err("ゼロで割ることはできません".to_string())
    } else {
        Ok(a / b)
    }
}

fn main() {
    match divide(10.0, 2.0) {
        Ok(result) => println!("結果: {}", result),
        Err(e) => println!("エラー: {}", e),
    }
}

`Option`型の概要


Option型は、値が存在するかしないかを示すために使用します。以下のように定義されています。

enum Option<T> {
    Some(T),    // 値が存在する場合
    None,       // 値が存在しない場合
}

使用例

fn find_element(elements: &[i32], target: i32) -> Option<usize> {
    elements.iter().position(|&x| x == target)
}

fn main() {
    let numbers = [1, 2, 3, 4, 5];
    match find_element(&numbers, 3) {
        Some(index) => println!("見つけたインデックス: {}", index),
        None => println!("要素が見つかりませんでした"),
    }
}

`unwrap`と`expect`の注意点


Result型やOption型には、簡易的にエラー処理を行うためのunwrapexpectメソッドがありますが、エラーが発生するとプログラムがパニックを起こします。プロダクションコードではこれらの使用は避け、適切なエラーハンドリングを行うようにしましょう。

例: unwrapの使用

let value = Some(42);
println!("{}", value.unwrap());  // 42を出力、Noneならパニック

例: expectの使用

let result: Result<i32, &str> = Err("エラーが発生しました");
println!("{}", result.expect("処理に失敗しました"));  // エラーメッセージを出力しパニック

Rustにおけるエラーハンドリングは、安全で堅牢なアプリケーションを作るために不可欠です。ResultOptionを適切に使うことで、予期しないエラーを未然に防ぐことができます。

データベース操作時のよくあるエラーと原因

Rustでデータベース操作を行う際に頻繁に発生するエラーと、その原因について理解しておくことは重要です。ここでは代表的なエラーとその対処方法について解説します。

1. 接続エラー


データベースへの接続が確立できない場合に発生します。主な原因は以下の通りです。

  • 原因:
  • データベースサーバーが起動していない
  • 接続情報(ホスト、ポート、ユーザー名、パスワード)が間違っている
  • ネットワークの問題

エラー例

Err("failed to connect to database")

対処法
接続情報を確認し、データベースが正しく起動しているかチェックしましょう。

2. SQLクエリエラー


クエリの構文ミスや、存在しないテーブルにアクセスしようとした場合に発生します。

  • 原因:
  • クエリの文法エラー
  • テーブル名やカラム名の誤り
  • データ型の不一致

エラー例

Err("syntax error at or near \"FROMM\"")

対処法
SQLクエリの文法を見直し、正しいテーブルやカラム名を指定しているか確認しましょう。

3. タイムアウトエラー


クエリの実行や接続に時間がかかりすぎてタイムアウトした場合に発生します。

  • 原因:
  • データベースサーバーが過負荷状態
  • 複雑なクエリやインデックスの欠如
  • ネットワーク遅延

エラー例

Err("operation timed out")

対処法
クエリの最適化や、接続タイムアウト時間の設定を見直しましょう。

4. 外部キー制約エラー


データの整合性を守るための外部キー制約に違反した場合に発生します。

  • 原因:
  • 親テーブルに存在しないIDを子テーブルに挿入しようとした

エラー例

Err("foreign key constraint violation")

対処法
データ挿入前に関連データが存在するか確認しましょう。

5. データ型不一致エラー


クエリで指定したデータ型とテーブル定義のデータ型が一致しない場合に発生します。

  • 原因:
  • 整数型カラムに文字列を挿入しようとした

エラー例

Err("column \"age\" is of type integer but expression is of type text")

対処法
データ型を正しく指定し、必要に応じて型変換を行いましょう。

まとめ


これらのエラーは、データベース操作において頻繁に発生する問題です。原因を正確に把握し、適切にエラーハンドリングを設計することで、Rustアプリケーションの安定性を向上させることができます。

Rustのエラーハンドリングツールとクレート

Rustには、効率的なエラーハンドリングを支援する多くのツールやクレートがあります。これらを活用することで、データベース操作時のエラー処理がより簡潔かつ明確になります。

`thiserror`クレート


thiserrorはカスタムエラー型を簡単に作成するためのクレートです。deriveマクロを利用してエラー型の定義がシンプルになります。

インストール
Cargo.tomlに以下を追加します。

[dependencies]
thiserror = "1.0"

使用例

use thiserror::Error;

#[derive(Error, Debug)]
pub enum DatabaseError {
    #[error("接続エラー: {0}")]
    ConnectionError(String),
    #[error("SQLクエリエラー: {0}")]
    QueryError(String),
}

fn connect_to_db() -> Result<(), DatabaseError> {
    Err(DatabaseError::ConnectionError("ホストが見つかりません".to_string()))
}

fn main() {
    match connect_to_db() {
        Ok(_) => println!("接続成功"),
        Err(e) => println!("エラー: {}", e),
    }
}

`anyhow`クレート


anyhowはシンプルで使いやすいエラー処理を提供するクレートです。エラーの詳細な情報を含めることができ、プロトタイプや小規模なプロジェクトに適しています。

インストール
Cargo.tomlに以下を追加します。

[dependencies]
anyhow = "1.0"

使用例

use anyhow::{Context, Result};

fn execute_query() -> Result<()> {
    let result = std::fs::read_to_string("non_existent_file.txt")
        .context("ファイル読み込み中にエラーが発生しました")?;
    println!("内容: {}", result);
    Ok(())
}

fn main() {
    if let Err(e) = execute_query() {
        println!("エラー: {:?}", e);
    }
}

`snafu`クレート


snafuは柔軟なエラーハンドリングを提供するクレートで、エラー型を細かく定義したい場合に便利です。

インストール
Cargo.tomlに以下を追加します。

[dependencies]
snafu = "0.7"

使用例

use snafu::{ResultExt, Snafu};

#[derive(Debug, Snafu)]
enum Error {
    #[snafu(display("ファイルが見つかりません: {}", source))]
    FileNotFound { source: std::io::Error },
}

fn read_file() -> Result<String, Error> {
    std::fs::read_to_string("non_existent_file.txt").context(FileNotFound)
}

fn main() {
    match read_file() {
        Ok(content) => println!("内容: {}", content),
        Err(e) => println!("エラー: {}", e),
    }
}

どのクレートを選ぶべきか

  • thiserror:カスタムエラー型を定義したい場合に最適。エラーが明確で型安全です。
  • anyhow:素早くエラー処理を実装したい場合や、小規模なプロジェクト向きです。
  • snafu:詳細で柔軟なエラー処理が必要な場合に適しています。

まとめ


Rustにはエラーハンドリングを効率化する強力なツールが揃っています。プロジェクトの規模や要件に応じて適切なクレートを選び、堅牢なエラーハンドリング設計を実現しましょう。

SQLクエリの実行におけるエラーハンドリング

Rustでデータベース操作を行う際、SQLクエリの実行はエラーが発生しやすいポイントです。適切なエラーハンドリングを行うことで、アプリケーションの信頼性を向上させることができます。ここでは、SQLクエリの実行時のエラーハンドリング方法について具体例とともに解説します。

データベース接続とクエリの実行

例:PostgreSQLをsqlxクレートで操作する場合

まず、sqlxクレートをCargo.tomlに追加します。

[dependencies]
sqlx = { version = "0.6", features = ["runtime-tokio", "postgres"] }
tokio = { version = "1", features = ["full"] }

基本的な接続とクエリ実行の例

use sqlx::{postgres::PgPoolOptions, Error};
use tokio;

#[tokio::main]
async fn main() -> Result<(), Error> {
    let database_url = "postgres://user:password@localhost/test_db";

    // データベース接続
    let pool = PgPoolOptions::new()
        .max_connections(5)
        .connect(database_url)
        .await?;

    // SQLクエリの実行
    let row = sqlx::query!("SELECT id, name FROM users WHERE id = $1", 1)
        .fetch_optional(&pool)
        .await?;

    match row {
        Some(record) => println!("ID: {}, Name: {}", record.id, record.name),
        None => println!("該当するレコードが見つかりませんでした"),
    }

    Ok(())
}

クエリ実行時のエラーハンドリングのポイント

  1. クエリの構文エラーの処理
    SQLクエリに文法エラーがあるとsqlx::Errorが返ります。エラー内容を確認し、構文を修正しましょう。
   if let Err(e) = sqlx::query!("SELECT * FROM invalid_table").execute(&pool).await {
       eprintln!("クエリの構文エラー: {}", e);
   }
  1. データ型の不一致エラーの処理
    カラムのデータ型とRustの型が一致しない場合、型エラーが発生します。
   if let Err(e) = sqlx::query!("SELECT age FROM users WHERE id = $1", 1)
       .fetch_one(&pool)
       .await
   {
       eprintln!("データ型不一致エラー: {}", e);
   }
  1. タイムアウトエラーの処理
    長時間実行されるクエリはタイムアウトする可能性があります。接続プールのタイムアウト設定を調整し、適切にエラー処理を行いましょう。
   let pool = PgPoolOptions::new()
       .acquire_timeout(std::time::Duration::from_secs(5))
       .connect(database_url)
       .await?;

   if let Err(e) = sqlx::query!("SELECT * FROM large_table").fetch_all(&pool).await {
       eprintln!("タイムアウトエラー: {}", e);
   }
  1. トランザクションエラーの処理
    トランザクション内でエラーが発生した場合、自動的にロールバックするようにします。
   let mut tx = pool.begin().await?;

   if let Err(e) = sqlx::query!("INSERT INTO users (name) VALUES ($1)", "Alice")
       .execute(&mut tx)
       .await
   {
       eprintln!("トランザクションエラー: {}", e);
       tx.rollback().await?;
   } else {
       tx.commit().await?;
   }

エラーの種類に応じた分岐処理

エラーの種類に応じて異なる処理を行いたい場合、match文を使ってエラーを分類できます。

match sqlx::query!("SELECT * FROM users WHERE id = $1", 1).fetch_one(&pool).await {
    Ok(row) => println!("ユーザー名: {}", row.name),
    Err(sqlx::Error::RowNotFound) => eprintln!("レコードが見つかりませんでした"),
    Err(e) => eprintln!("その他のエラー: {}", e),
}

まとめ

RustでのSQLクエリ実行時に適切なエラーハンドリングを行うことで、クエリの失敗や予期せぬ動作を防ぎ、アプリケーションの安定性を向上させることができます。クレートの機能を活用し、エラーの種類ごとに適切な処理を設計しましょう。

接続プールと接続エラーの管理

データベース操作において、効率的な接続管理はパフォーマンスと安定性を維持するために重要です。Rustでは、接続プールを利用してデータベース接続を効率よく再利用し、接続エラーを適切に処理できます。ここでは、接続プールの概要、設定方法、および接続エラーの管理方法について解説します。

接続プールとは何か

接続プールは、複数のデータベース接続を管理し、必要に応じて再利用する仕組みです。新しい接続を毎回確立する代わりに、接続済みのコネクションをプールから取り出して利用するため、接続コストを削減し、アプリケーションのパフォーマンスを向上させます。

Rustでの接続プールの作成

sqlxクレートを使って接続プールを作成する手順を紹介します。

Cargo.tomlに依存クレートを追加

[dependencies]
sqlx = { version = "0.6", features = ["runtime-tokio", "postgres"] }
tokio = { version = "1", features = ["full"] }

接続プールの作成コード

use sqlx::{postgres::PgPoolOptions, Error};
use tokio;

#[tokio::main]
async fn main() -> Result<(), Error> {
    let database_url = "postgres://user:password@localhost/test_db";

    // 接続プールの作成
    let pool = PgPoolOptions::new()
        .max_connections(5) // 最大接続数
        .acquire_timeout(std::time::Duration::from_secs(5)) // タイムアウト設定
        .connect(database_url)
        .await?;

    println!("データベースに接続しました");

    Ok(())
}

接続エラーの管理

接続時にはさまざまなエラーが発生する可能性があります。代表的なエラーとその対処法を見ていきましょう。

1. 接続タイムアウトエラー

接続に時間がかかりすぎるとタイムアウトエラーが発生します。

エラー例

error: connection timed out

対処法

接続プールのタイムアウト設定を調整します。

let pool = PgPoolOptions::new()
    .acquire_timeout(std::time::Duration::from_secs(10))
    .connect(database_url)
    .await?;

2. 認証エラー

接続情報(ユーザー名、パスワード)が間違っていると認証エラーが発生します。

エラー例

error: invalid password for user "postgres"

対処法

正しい認証情報を確認し、設定ファイルや環境変数で管理しましょう。

3. データベースが存在しないエラー

指定したデータベースが存在しない場合に発生します。

エラー例

error: database "test_db" does not exist

対処法

データベースが作成されているか確認し、必要ならば作成します。

接続プールの設定項目

PgPoolOptionsにはいくつかの設定項目があります。

  • max_connections:プール内の最大接続数(例:max_connections(10)
  • min_connections:プール内の最小接続数(例:min_connections(2)
  • acquire_timeout:接続取得のタイムアウト(例:acquire_timeout(Duration::from_secs(5))
  • idle_timeout:アイドル接続のタイムアウト(例:idle_timeout(Duration::from_secs(30))

let pool = PgPoolOptions::new()
    .max_connections(10)
    .min_connections(2)
    .idle_timeout(std::time::Duration::from_secs(60))
    .connect(database_url)
    .await?;

接続プールのクリーンアップ

プログラム終了時には接続プールをクリーンアップすることが重要です。drop関数で明示的にプールを破棄できます。

drop(pool);
println!("接続プールをクリーンアップしました");

まとめ

接続プールを利用することで、データベース接続のオーバーヘッドを削減し、効率的なリソース管理が可能になります。適切な設定とエラーハンドリングを行うことで、Rustアプリケーションのパフォーマンスと安定性を向上させましょう。

非同期データベース操作とエラーハンドリング

Rustでは非同期プログラミングが強力にサポートされており、データベース操作を非同期で実行することでパフォーマンスを向上させることができます。しかし、非同期処理には特有のエラーハンドリングの注意点があります。ここでは、非同期データベース操作の基本と、エラーハンドリング方法について解説します。

非同期データベース操作の概要

Rustでは、async/await構文とTokioasync-stdといった非同期ランタイムを使用して非同期データベース操作を実現します。sqlxクレートは非同期データベース操作をサポートする代表的なクレートです。

非同期データベース操作の例

以下はsqlxTokioを使用した非同期データベース操作の基本例です。

Cargo.tomlに依存クレートを追加

[dependencies]
sqlx = { version = "0.6", features = ["runtime-tokio", "postgres"] }
tokio = { version = "1", features = ["full"] }

非同期クエリの実行

use sqlx::{postgres::PgPoolOptions, Error};
use tokio;

#[tokio::main]
async fn main() -> Result<(), Error> {
    let database_url = "postgres://user:password@localhost/test_db";

    // 接続プールを作成
    let pool = PgPoolOptions::new()
        .max_connections(5)
        .connect(database_url)
        .await?;

    // 非同期でクエリを実行
    let row = sqlx::query!("SELECT id, name FROM users WHERE id = $1", 1)
        .fetch_optional(&pool)
        .await?;

    match row {
        Some(record) => println!("ID: {}, Name: {}", record.id, record.name),
        None => println!("該当するレコードが見つかりませんでした"),
    }

    Ok(())
}

非同期処理におけるエラーハンドリングのポイント

1. `Result`型を活用する


非同期処理でもResult型を使ってエラー処理を行います。エラーが発生した場合、適切にmatch文で処理しましょう。

let result = sqlx::query!("SELECT * FROM users WHERE id = $1", 1)
    .fetch_one(&pool)
    .await;

match result {
    Ok(row) => println!("ユーザー名: {}", row.name),
    Err(e) => eprintln!("エラー: {}", e),
}

2. 非同期エラーの`?`演算子


?演算子を使うことで、エラーが発生した場合に即座に呼び出し元にエラーを返すことができます。

async fn get_user_by_id(pool: &sqlx::PgPool, user_id: i32) -> Result<(), Error> {
    let row = sqlx::query!("SELECT id, name FROM users WHERE id = $1", user_id)
        .fetch_one(pool)
        .await?;

    println!("ID: {}, Name: {}", row.id, row.name);
    Ok(())
}

3. タイムアウト処理


非同期クエリが長時間実行される場合、タイムアウト処理を追加することで、システムのパフォーマンス低下を防ぎます。

use tokio::time::{timeout, Duration};

let result = timeout(
    Duration::from_secs(5),
    sqlx::query!("SELECT * FROM users WHERE id = $1", 1).fetch_one(&pool)
)
.await;

match result {
    Ok(Ok(row)) => println!("ユーザー名: {}", row.name),
    Ok(Err(e)) => eprintln!("クエリエラー: {}", e),
    Err(_) => eprintln!("クエリがタイムアウトしました"),
}

4. 非同期エラーの分類


非同期処理ではネットワークエラーやタイムアウトエラーなど、さまざまな種類のエラーが発生します。エラーを分類して適切に対処することが重要です。

use sqlx::Error;

match sqlx::query!("SELECT * FROM users WHERE id = $1", 1).fetch_one(&pool).await {
    Ok(row) => println!("ユーザー名: {}", row.name),
    Err(Error::RowNotFound) => eprintln!("レコードが見つかりませんでした"),
    Err(Error::Database(e)) => eprintln!("データベースエラー: {}", e),
    Err(e) => eprintln!("その他のエラー: {}", e),
}

非同期処理のベストプラクティス

  1. 適切なタイムアウト設定:長時間のクエリ実行を避け、適切なタイムアウト値を設定しましょう。
  2. エラーの詳細ログ:エラーの詳細をログに記録し、トラブルシューティングを容易にしましょう。
  3. 接続プールの活用:接続プールを使うことで、効率的な接続管理が可能になります。
  4. 非同期タスクの並行処理:複数のクエリを並行して実行し、パフォーマンスを向上させましょう。

まとめ

非同期データベース操作はRustのパフォーマンス向上に大きく貢献しますが、適切なエラーハンドリングが必要です。Result型、タイムアウト処理、エラー分類を活用し、堅牢な非同期処理を設計することで、信頼性の高いデータベース操作が実現できます。

エラーログとエラーリカバリの設計

データベース操作におけるエラーが発生した場合、適切にログを記録し、エラーリカバリ(復旧)を設計することで、システムの信頼性と保守性を向上させることができます。ここでは、エラーログの効果的な記録方法とエラーリカバリの設計について解説します。

エラーログの重要性

エラーログは、発生した問題を特定し、迅速に対処するための重要な情報源です。エラーログには以下の情報を含めると効果的です。

  • エラー発生時刻
  • エラーの種類と詳細
  • エラーが発生した場所(関数名、ファイル名、行番号)
  • スタックトレース(可能ならば)

Rustでエラーログを記録する方法

Rustでエラーログを記録するには、logクレートやtracingクレートが便利です。

Cargo.tomlに依存クレートを追加

[dependencies]
log = "0.4"
env_logger = "0.10"

基本的なエラーログの記録例

use log::{error, info};
use std::fs::File;

fn read_file(filename: &str) -> Result<(), std::io::Error> {
    match File::open(filename) {
        Ok(_) => {
            info!("ファイルが正常に読み込まれました: {}", filename);
            Ok(())
        }
        Err(e) => {
            error!("ファイルの読み込みに失敗しました: {} - {}", filename, e);
            Err(e)
        }
    }
}

fn main() {
    env_logger::init(); // ログの初期化

    if let Err(_) = read_file("non_existent_file.txt") {
        println!("エラーが発生しました。詳細はログを確認してください。");
    }
}

実行結果のログ

[2024-06-14 12:34:56] ERROR: ファイルの読み込みに失敗しました: non_existent_file.txt - No such file or directory

エラーログの詳細化

ログには、可能な限り詳細な情報を含めることで、デバッグやトラブルシューティングが容易になります。

  • エラー発生時刻env_loggerは自動でタイムスタンプを記録します。
  • 関数名や行番号logマクロにfile!()line!()マクロを追加することで、エラーの発生位置を明確にできます。
error!("エラー発生 - ファイル: {}, 行: {}", file!(), line!());

エラーリカバリの設計

エラーリカバリは、システムがエラーから自動的に復旧するための仕組みです。データベース操作における代表的なリカバリ方法を紹介します。

1. 再試行(リトライ)処理

一時的な接続エラーやタイムアウトエラーの場合、一定回数再試行することで問題を解決できることがあります。

再試行処理の例

use std::time::Duration;
use tokio::time::sleep;

async fn retry_query<F, Fut>(mut operation: F, retries: u32) -> Result<(), sqlx::Error>
where
    F: FnMut() -> Fut,
    Fut: std::future::Future<Output = Result<(), sqlx::Error>>,
{
    for attempt in 1..=retries {
        match operation().await {
            Ok(_) => return Ok(()),
            Err(e) => {
                eprintln!("試行 {} でエラー: {}。再試行します...", attempt, e);
                sleep(Duration::from_secs(2)).await;
            }
        }
    }
    Err(sqlx::Error::Protocol("再試行回数を超えました".into()))
}

使用例

retry_query(|| async {
    sqlx::query!("SELECT * FROM users").execute(&pool).await.map(|_| ())
}, 3)
.await?;

2. フォールバック処理

データベースに接続できない場合、代替手段(フォールバック)を提供します。

例:キャッシュからの読み取り

fn get_user_data_from_cache(user_id: i32) -> Option<String> {
    Some(format!("キャッシュから取得したユーザーID: {}", user_id))
}

async fn get_user_data(pool: &sqlx::PgPool, user_id: i32) -> String {
    match sqlx::query!("SELECT name FROM users WHERE id = $1", user_id)
        .fetch_one(pool)
        .await
    {
        Ok(record) => record.name,
        Err(_) => {
            eprintln!("データベース接続に失敗。キャッシュからデータを取得します。");
            get_user_data_from_cache(user_id).unwrap_or_else(|| "データなし".to_string())
        }
    }
}

3. トランザクションのロールバック

エラーが発生した場合にトランザクションをロールバックすることで、データの整合性を保ちます。

let mut tx = pool.begin().await?;

if let Err(e) = sqlx::query!("INSERT INTO users (name) VALUES ($1)", "Alice")
    .execute(&mut tx)
    .await
{
    eprintln!("エラーが発生したためロールバックします: {}", e);
    tx.rollback().await?;
} else {
    tx.commit().await?;
}

まとめ

エラーログとエラーリカバリの適切な設計は、システムの安定性と保守性を高めます。エラーの詳細なログ記録、再試行処理、フォールバック、トランザクションのロールバックを活用し、データベース操作のエラーに柔軟に対応できるシステムを構築しましょう。

応用例:実際のプロジェクトでのエラーハンドリング実装

Rustでデータベース操作を行う実際のプロジェクトにおけるエラーハンドリングの実装例を紹介します。この例では、sqlxクレートを使用して、ユーザー管理システムを構築し、エラー処理のベストプラクティスを実践します。


プロジェクト概要

  • 目的:ユーザー情報をデータベースに保存・取得するAPIの作成
  • データベース:PostgreSQL
  • クレートsqlxtokiothiserrorwarp(Webフレームワーク)
  • エラーハンドリング:接続エラー、クエリエラー、入力データの検証エラー

1. 必要な依存クレート

Cargo.toml

[dependencies]
tokio = { version = "1", features = ["full"] }
sqlx = { version = "0.6", features = ["runtime-tokio", "postgres"] }
thiserror = "1.0"
warp = "0.3"

2. エラー型の定義

thiserrorを使用してカスタムエラー型を定義します。

errors.rs

use thiserror::Error;

#[derive(Error, Debug)]
pub enum AppError {
    #[error("データベース接続エラー: {0}")]
    DatabaseError(#[from] sqlx::Error),

    #[error("リクエストデータの検証エラー: {0}")]
    ValidationError(String),
}

3. データベース接続の設定

データベース接続プールを作成します。

db.rs

use sqlx::{postgres::PgPoolOptions, PgPool};
use std::env;

pub async fn get_db_pool() -> PgPool {
    let database_url = env::var("DATABASE_URL").expect("DATABASE_URLが設定されていません");
    PgPoolOptions::new()
        .max_connections(5)
        .connect(&database_url)
        .await
        .expect("データベースへの接続に失敗しました")
}

4. ユーザー構造体とクエリ関数

models.rs

use serde::{Deserialize, Serialize};
use sqlx::FromRow;

#[derive(Serialize, FromRow)]
pub struct User {
    pub id: i32,
    pub name: String,
    pub email: String,
}

#[derive(Deserialize)]
pub struct CreateUser {
    pub name: String,
    pub email: String,
}

queries.rs

use crate::models::{CreateUser, User};
use crate::errors::AppError;
use sqlx::PgPool;

pub async fn add_user(pool: &PgPool, new_user: CreateUser) -> Result<User, AppError> {
    if new_user.name.is_empty() || new_user.email.is_empty() {
        return Err(AppError::ValidationError("名前とメールは必須です".to_string()));
    }

    let user = sqlx::query_as!(
        User,
        "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, email",
        new_user.name,
        new_user.email
    )
    .fetch_one(pool)
    .await?;

    Ok(user)
}

5. Web APIエンドポイント

warpを使用してエンドポイントを作成します。

main.rs

mod db;
mod errors;
mod models;
mod queries;

use db::get_db_pool;
use errors::AppError;
use models::CreateUser;
use queries::add_user;
use warp::{http::StatusCode, Filter};

#[tokio::main]
async fn main() {
    let pool = get_db_pool().await;

    let create_user_route = warp::post()
        .and(warp::path("users"))
        .and(warp::body::json())
        .and(warp::any().map(move || pool.clone()))
        .and_then(create_user_handler);

    warp::serve(create_user_route).run(([127, 0, 0, 1], 3030)).await;
}

async fn create_user_handler(new_user: CreateUser, pool: sqlx::PgPool) -> Result<impl warp::Reply, warp::Rejection> {
    match add_user(&pool, new_user).await {
        Ok(user) => Ok(warp::reply::json(&user)),
        Err(AppError::ValidationError(msg)) => Ok(warp::reply::with_status(
            warp::reply::json(&msg),
            StatusCode::BAD_REQUEST,
        )),
        Err(AppError::DatabaseError(err)) => {
            eprintln!("データベースエラー: {:?}", err);
            Ok(warp::reply::with_status(
                warp::reply::json(&"内部サーバーエラー"),
                StatusCode::INTERNAL_SERVER_ERROR,
            ))
        }
    }
}

6. エラーハンドリングの動作確認

正常なリクエスト例

curl -X POST -H "Content-Type: application/json" -d '{"name": "Alice", "email": "alice@example.com"}' http://localhost:3030/users

バリデーションエラー例

curl -X POST -H "Content-Type: application/json" -d '{"name": "", "email": ""}' http://localhost:3030/users

結果

"名前とメールは必須です"

まとめ

この実践例では、非同期データベース操作におけるエラーハンドリングのベストプラクティスを示しました。thiserrorを使用したカスタムエラー、データベース接続プール、バリデーションエラーの処理、Web APIのエラーハンドリングを組み合わせることで、堅牢で保守しやすいシステムを構築できます。

まとめ

本記事では、Rustにおけるデータベース操作のエラーハンドリング設計について解説しました。Rustの強力な型システムと非同期プログラミングサポートを活用し、Result型やOption型によるエラー処理、thiserroranyhowといったクレートの利用方法、接続プール管理、非同期クエリのエラーハンドリング、そしてエラーログやエラーリカバリの設計について具体例と共に紹介しました。

適切なエラーハンドリングを実装することで、データベース操作時のエラーを効率よく管理し、システムの安定性と保守性を向上させることができます。実際のプロジェクトでこれらの手法を活用し、信頼性の高いRustアプリケーションを構築しましょう。

コメント

コメントする

目次