【初心者向け】RustでSQLxを使ってデータベースを操作する方法

はい、承知いたしました。Rust初心者向けに、SQLxを使ったデータベース操作方法について、詳細な説明を含む約5000語の記事を作成します。


【初心者向け】Rustで始める!SQLxを使ったデータベース操作の徹底解説

はじめに

プログラミング言語 Rust は、その安全性、速度、並行性の高さから、近年 Web アプリケーションやバックエンドサービス開発の分野でも注目を集めています。これらのアプリケーションにとって、データベースはほぼ必須の要素です。データベースとのやり取りは、ユーザーデータ、設定情報、ビジネスロジックに関わるデータなど、アプリケーションの根幹をなす情報を管理するために不可欠だからです。

Rust でデータベースを操作するためのライブラリはいくつか存在しますが、その中でも特に人気が高く、安全性と使いやすさを兼ね備えているのが sqlx です。sqlx は非同期処理に最適化されており、コンパイル時に SQL クエリの正しさを検証できるというユニークな特徴を持っています。これにより、実行時まで SQL エラーに気づかないというリスクを減らし、より堅牢なアプリケーションを開発できます。

この記事は、「Rustを触り始めたばかりで、データベース操作はどうやるんだろう?」「sqlx って名前は聞くけど、どうやって使うの?」といった初心者の方々を対象としています。sqlx を使って、PostgreSQL データベースに対して基本的な CRUD (Create, Read, Update, Delete) 操作を行う方法を、環境構築から順を追って丁寧に解説していきます。

この記事を読むことで、以下のことができるようになります。

  • Rust プロジェクトに sqlx を導入し、データベースに接続する
  • 基本的な SQL クエリを Rust コードから実行する
  • データベースのデータを Rust の構造体にマッピングして扱う
  • sqlx のマイグレーション機能を使ってデータベーススキーマを管理する
  • トランザクションを使って複数のデータベース操作を安全に行う

さあ、Rust と sqlx の世界に飛び込み、安全で高速なデータベースアプリケーション開発を始めましょう!

第1章: 準備をしよう

sqlx を使ったデータベース操作を始める前に、いくつかの準備が必要です。

1.1 Rust のインストール

まずは Rust がコンピュータにインストールされていることを確認します。まだの場合は、公式ドキュメントに従って rustup をインストールするのが最も簡単で推奨される方法です。

bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

インストールが完了したら、ターミナルを再起動するか、指示されたコマンドを実行して環境変数を反映させてください。そして、以下のコマンドでバージョンが表示されることを確認します。

bash
rustc --version
cargo --version

1.2 データベースの準備 (PostgreSQL を例に)

sqlx は様々なデータベースに対応していますが、この記事では最も一般的なリレーショナルデータベースの一つである PostgreSQL を使用します。データベースサーバーをローカルにインストールする方法もありますが、開発用途では Docker を使うのが手軽です。

Docker がインストールされている前提で説明します。以下のコマンドを実行すると、PostgreSQL サーバーが起動します。

bash
docker run --name some-postgres -e POSTGRES_PASSWORD=mysecretpassword -p 5432:5432 -d postgres

このコマンドは以下の内容を実行します。

  • docker run: 新しいコンテナを起動します。
  • --name some-postgres: コンテナに some-postgres という名前を付けます。
  • -e POSTGRES_PASSWORD=mysecretpassword: 環境変数 POSTGRES_PASSWORD を設定し、データベースのパスワードを mysecretpassword にします。
  • -p 5432:5432: ホストマシン (あなたのPC) のポート 5432 を、コンテナ内のポート 5432 (PostgreSQL のデフォルトポート) にマッピングします。これで、ホストマシンから localhost:5432 でデータベースにアクセスできるようになります。
  • -d postgres: バックグラウンド (デタッチドモード) で postgres という名前の Docker イメージを起動します。イメージがローカルにない場合は自動でダウンロードされます。

コンテナが起動したら、データベースクライアントツール(例: psql, DBeaver, pgAdmin)を使って接続できるか確認してみましょう。接続情報は以下のようになります。

  • ホスト: localhost (または 127.0.0.1)
  • ポート: 5432
  • ユーザー名: postgres (PostgreSQL イメージのデフォルトユーザー)
  • パスワード: mysecretpassword (Docker コマンドで指定したもの)
  • データベース名: postgres (PostgreSQL イメージのデフォルトデータベース)

今回は、postgres データベース内に新しいデータベース my_database を作成して使用することにします。データベースクライアントで接続後、以下の SQL クエリを実行してください。

sql
CREATE DATABASE my_database;

これで、Rust アプリケーションから my_database に接続するための準備ができました。

1.3 プロジェクトの作成と依存関係の追加 (Cargo.toml)

新しい Rust プロジェクトを作成します。

bash
cargo new rust-sqlx-example
cd rust-sqlx-example

次に、sqlx と非同期ランタイムをプロジェクトに追加します。sqlx は非同期ライブラリなので、非同期ランタイムが必要です。Rust のエコシステムでは tokioasync-std が主流ですが、ここでは広く使われている tokio を使用します。

Cargo.toml ファイルを開き、[dependencies] セクションに以下の行を追加します。

toml
[dependencies]
sqlx = { version = "0.7", features = ["runtime-tokio", "tls-rustls", "postgres", "uuid", "chrono", "json"] }
tokio = { version = "1", features = ["full"] }
dotenv = "0.15"
uuid = { version = "1.0", features = ["v4"] }
chrono = { version = "0.4", features = ["serde"] }
serde = { version = "1.0", features = ["derive"] }

それぞれの依存関係と機能フラグについて説明します。

  • sqlx = { version = "0.7", features = [...] }: sqlx クレート本体とその機能フラグを指定します。
    • runtime-tokio: 非同期ランタイムとして tokio を使用することを指定します。async-std を使う場合は runtime-async-std を指定します。
    • tls-rustls: TLS/SSL 接続のために rustls ライブラリを使用します。プロダクション環境ではセキュリティのために推奨されます。開発環境では不要な場合もありますが、含めておくと安全です。
    • postgres: PostgreSQL データベースのサポートを有効にします。他のデータベースを使う場合は mysql, sqlite, sqlserver などを指定します。
    • uuid: uuid クレートの型をデータベースの UUID 型にマッピングできるようにします。
    • chrono: chrono クレートの日付/時刻型をデータベースの timestamp 型などにマッピングできるようにします。
    • json: データベースの JSON/JSONB 型と Rust の JSON 型(serde_json など)をマッピングできるようにします。
  • tokio = { version = "1", features = ["full"] }: 非同期ランタイム tokio を追加します。full 機能フラグは、タイマー、I/O、スレッドプールなど、tokio の全ての機能を含みます。学習目的では便利ですが、プロダクションでは必要な機能だけを選択するのが一般的です。
  • dotenv = "0.15": .env ファイルから環境変数を読み込むためのユーティリティークレートです。データベース接続情報などの機密情報をコードに直接書かないために使用します。
  • uuid = { version = "1.0", features = ["v4"] }: UUID を生成・扱うためのクレートです。データベースのプライマリキーなどに UUID を使う場合に便利です。
  • chrono = { version = "0.4", features = ["serde"] }: 日付や時刻を扱うためのクレートです。データベースの timestamp 型などとマッピングするために使用します。serde 機能フラグはシリアライズ/デシリアライズを可能にします。
  • serde = { version = "1.0", features = ["derive"] }: Rust のデータ構造を様々な形式(JSON, YAML など)にシリアライズ/デシリアライズするためのフレームワークです。derive 機能フラグは、構造体にアトリビュートを付けるだけでシリアライズ/デシリアライズのコードを自動生成できるようにします。今回は主に sqlx::FromRow と組み合わせて使用します。

Cargo.toml を保存したら、cargo build または cargo check を実行して依存関係をダウンロードし、プロジェクトをセットアップします。

1.4 SQLx の機能フラグについて

sqlx は非常にモジュール化されており、必要なデータベースドライバやランタイム、機能だけを有効にすることで、コンパイル時間やバイナリサイズを最適化できます。前述の Cargo.toml で指定した機能フラグは、この記事で扱う PostgreSQL と tokio ランタイム、そして日付/時刻型、UUID 型、JSON 型の基本的なマッピングに必要なものです。

もし他のデータベース(例: MySQL)を使う場合は、features のリストを適切に変更する必要があります(例: "mysql""runtime-async-std" など)。利用可能な機能フラグの詳細は sqlx の公式ドキュメントを参照してください。

1.5 データベース接続情報の設定 (.env)

データベース接続情報は機密情報であり、コードに直接ハードコーディングするべきではありません。代わりに、環境変数や設定ファイルを使用します。ここでは、開発でよく使われる .env ファイルを使用します。

プロジェクトのルートディレクトリ(Cargo.toml がある場所)に .env という名前のファイルを作成し、以下の内容を記述します。Docker で PostgreSQL を起動した際に使用した情報に合わせてください。

dotenv
DATABASE_URL=postgres://postgres:mysecretpassword@localhost:5432/my_database

この接続文字列の形式はデータベースによって異なりますが、PostgreSQL の場合は以下のようになります。

postgres://<ユーザー名>:<パスワード>@<ホスト名>:<ポート番号>/<データベース名>

dotenv クレートを使うと、アプリケーション起動時にこの .env ファイルから環境変数を自動的に読み込んでくれます。

これで、sqlx を使ってデータベース操作を行うための準備が全て整いました。

第2章: SQLx の基本を知る

この章では、sqlx を使ってデータベースに接続し、基本的なクエリを実行する方法を学びます。

2.1 非同期処理と SQLx

前述の通り、sqlx は非同期ライブラリです。これは、データベース操作(ネットワーク経由でのデータの送受信やディスク I/O)は完了に時間がかかるため、その間プログラムの実行をブロックしないようにするためです。Rust の非同期処理は async/await シンタックスで記述され、tokioasync-std のような非同期ランタイム上で実行されます。

main 関数を非同期にするには、tokio::main のようなアトリビュートを付けます。

“`rust

[tokio::main] // main 関数を tokio ランタイム上で実行可能にする

async fn main() -> Result<(), Box> {
// ここに非同期コードを書く
Ok(())
}
“`

この -> Result<(), Box<dyn std::error::Error>> は、関数が成功した場合は Ok(()) を返し、エラーが発生した場合はエラーオブジェクト (Box<dyn std::error::Error>) を返すことを示しています。データベース操作は失敗する可能性があるため、適切なエラーハンドリングを行うことが重要です。

2.2 コネクションプーリング (sqlx::Pool)

データベースへの接続はコストのかかる処理です。クエリを実行するたびに新しい接続を確立するのは非効率的です。そこで、sqlx では sqlx::Pool を使ったコネクションプーリングが推奨されます。

コネクションプールは、複数のデータベース接続をあらかじめ確立しておき、必要に応じてそこから接続を取り出して使用します。使い終わった接続はプールに戻され、再利用されます。これにより、接続確立のオーバーヘッドを減らし、アプリケーションのスケーラビリティを向上させることができます。

sqlx::Pool はアプリケーションの起動時に一度だけ作成し、必要に応じて共有して使います。

2.3 データベースへの接続

.env ファイルから接続文字列を読み込み、sqlx::Pool を作成してデータベースに接続するコードを記述します。

src/main.rs を開き、既存のコードを以下のように書き換えます。

“`rust
use dotenv::dotenv;
use sqlx::{Pool, Postgres, Error}; // Postgres は使用するDBに合わせて変更

[tokio::main]

async fn main() -> Result<(), Box> {
// .env ファイルから環境変数を読み込む
dotenv().ok();

// 環境変数からデータベース接続文字列を取得
let database_url = std::env::var("DATABASE_URL")
    .expect("DATABASE_URL must be set in .env file");

// コネクションプールを作成
let pool = Pool::<Postgres>::connect(&database_url).await?;

// 接続が成功したことを確認 (オプション)
match pool.acquire().await {
    Ok(_) => println!("Successfully connected to the database!"),
    Err(e) => eprintln!("Failed to connect to the database: {}", e),
}

// ここでデータベース操作を行う

Ok(())

}
“`

解説:

  • use dotenv::dotenv;: dotenv クレートから dotenv 関数をインポートします。
  • use sqlx::{Pool, Postgres, Error};: sqlx クレートから Pool, Postgres(PostgreSQL 用の型マーカー)、Error 型をインポートします。
  • dotenv().ok();: プログラムの最初に呼び出すことで、.env ファイルの内容が環境変数に読み込まれます。ok() は、ファイルが存在しない場合でもエラーにならないようにします。
  • let database_url = std::env::var(...): 環境変数 DATABASE_URL の値を取得します。取得できなかった場合は .expect() でプログラムを終了させます。
  • let pool = Pool::<Postgres>::connect(&database_url).await?;: sqlx::Pool::connect 関数は非同期関数なので await が必要です。接続文字列を引数に取り、指定されたデータベースへのコネクションプールを作成します。Pool::<Postgres>Postgres は、どのデータベースに接続するかをコンパイル時に sqlx に伝えるための型マーカーです。? 演算子は、connectResult を返すため、エラーが発生した場合に関数から早期リターンするために使用します。
  • pool.acquire().await?: プールから一時的にデータベースコネクションを取得し、即座に解放します。これは接続が正常に行われたかを確認するための一つの方法です。(本番コードでは毎回実行する必要はありませんが、初期接続のデバッグに役立ちます。)
  • #[tokio::main]: main 関数が async fn であり、tokio ランタイム上で実行されることを示します。

このコードを実行するには、ターミナルで cargo run コマンドを実行します。.env ファイルに正しい DATABASE_URL が設定されていれば、「Successfully connected to the database!」というメッセージが表示されるはずです。

2.4 基本的なクエリ実行メソッド

sqlx::Pool またはプールから取得した個別のコネクション (sqlx::Connection) を使ってクエリを実行できます。sqlx でクエリを実行するための基本的なメソッドはいくつかあります。

  • .execute(): INSERT, UPDATE, DELETE, CREATE TABLE など、結果セットを返さないクエリを実行します。影響を受けた行数などの情報を含む sqlx::postgres::PgQueryResult (PostgreSQL の場合) を返します。
  • .fetch_one(): SELECT クエリを実行し、ちょうど1行の結果を期待する場合に使用します。結果が0行または2行以上の場合、エラーになります。
  • .fetch_optional(): SELECT クエリを実行し、0行または1行の結果を期待する場合に使用します。結果が0行の場合は None、1行の場合は Some(row) を返します。2行以上の場合はエラーになります。
  • .fetch_all(): SELECT クエリを実行し、全ての行を結果として取得する場合に使用します。結果が空の場合でもエラーにはなりません。

これらのメソッドは全て非同期なので、.await が必要です。

2.5 プリペアドステートメントとパラメーターバインディング

sqlx はデフォルトでプリペアドステートメントを使用します。これは、SQL インジェクション攻撃を防ぐための最も重要なセキュリティ対策の一つです。SQL クエリのテンプレートと、そこに埋め込む値を分けてデータベースに送信します。データベースはテンプレートを事前に解析・コンパイルしておき、値を受け取ったらそれを適用して実行します。これにより、値の中に含まれる特殊文字(例: ';)が SQL コマンドとして解釈されることを防ぎます。

sqlx では、クエリ文字列の中にプレースホルダー(PostgreSQL の場合は $1, $2, …)を記述し、それに対応する値を .bind() メソッドを使って順番にバインドします。

例:
“`rust
let user_id = 1;
let username = “Alice”;

let query = sqlx::query(“SELECT * FROM users WHERE id = $1 AND username = $2”)
.bind(user_id) // $1 にバインド
.bind(username); // $2 にバインド

// query.fetch_one(&pool).await?;
“`

.bind() メソッドは、バインドする値の型に基づいて適切なデータベース型への変換を sqlx に行わせます。ほとんどの標準的な Rust の型(i32, String, bool など)は自動的にマッピングされます。カスタム型や日付/時刻型、UUID などは、対応する機能フラグを有効にすることでマッピング可能になります。

2.6 エラーハンドリング

データベース操作は様々な理由で失敗する可能性があります。例えば、ネットワークエラー、SQL構文エラー、データベース制約違反(ユニークキー違反など)、データの型変換エラーなどです。Rust では、これらの失敗は Result 型を使って表現されます。

sqlx の多くの操作は Result<T, sqlx::Error> を返します。T は成功時の値の型、sqlx::Error は発生したエラーの詳細を含む型です。

初心者向けの記事としては、簡単な例では ? 演算子を使ってエラーを呼び出し元に伝播させる方法が便利です。

“`rust
async fn my_db_operation(pool: &Pool) -> Result<(), sqlx::Error> {
// … データベース操作 …
sqlx::query(“INSERT INTO …”)
.execute(pool)
.await?; // エラーが発生したらここで関数から抜ける

// ... 別の操作 ...
let row = sqlx::query("SELECT ...")
    .fetch_one(pool)
    .await?; // ここでもエラー伝播

Ok(()) // 全て成功したら Ok(()) を返す

}

[tokio::main]

async fn main() -> Result<(), Box> {
// … プール作成 …
let pool = //;

// データベース操作の呼び出し
if let Err(e) = my_db_operation(&pool).await {
    eprintln!("Database operation failed: {}", e);
    return Err(e.into()); // エラーを main から返す
}

println!("Database operation succeeded.");

Ok(())

}
“`

より詳細なエラー処理が必要な場合は、match 式や if let Err(...) を使って sqlx::Error の種類を判別し、それに応じた処理を記述します。

第3章: データとRustの橋渡し – 構造体マッピング

データベースから取得したデータは、通常、テーブルの行と列として取得されます。これらのデータを Rust アプリケーションで扱いやすい形式にするために、構造体へのマッピングを行います。sqlx は、データベースの行を Rust の構造体に簡単にマッピングするための機能を提供しています。

3.1 sqlx::FromRow トレイト

sqlxsqlx::FromRow トレイトを提供しています。このトレイトを実装した構造体は、データベースの1行からその構造体のインスタンスを生成できます。sqlx は、構造体のフィールド名とデータベースの列名をマッチさせて自動的にマッピングを行います。

多くの標準的な Rust の型(i32, String, bool など)や、chrono::DateTime, uuid::Uuid など、対応する機能フラグを有効にしたクレートの型は、自動的にマッピング可能です。

構造体に #[derive(sqlx::FromRow)] アトリビュートを付けることで、このトレイトの実装を自動生成できます。これは serde::Deserialize のような自動導出機能と同様です。

3.2 構造体の定義

データベーステーブルのスキーマに対応する Rust の構造体を定義します。例えば、以下のような users テーブルを考えます。

sql
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(255) NOT NULL UNIQUE,
email VARCHAR(255) UNIQUE,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);

このテーブルに対応する Rust の構造体は以下のようになります。

“`rust
use sqlx::FromRow;
use chrono::NaiveDateTime; // TIMESTAMP は NaiveDateTime にマッピングされることが多い

[derive(FromRow, Debug)] // FromRow を derive し、Debug で表示可能にする

struct User {
id: i32,
username: String,
email: Option, // email は NULL 可能なので Option
created_at: NaiveDateTime,
}
“`

解説:

  • #[derive(FromRow, Debug)]: sqlx::FromRow トレイトと、デバッグ出力のための Debug トレイトを自動導出します。
  • id: i32: SERIAL または INTEGER のプライマリキーは i32 にマッピングできます。
  • username: String: VARCHAR(255)String にマッピングできます。NOT NULL 制約があるので Option は不要です。
  • email: Option<String>: VARCHAR(255) ですが、UNIQUE 制約はあっても NOT NULL 制約がないため NULL が許容されます。NULL を許容するカラムは Rust では Option<T> 型にマッピングする必要があります。NULL 値は None に、非 NULL 値は Some(value) に対応します。
  • created_at: NaiveDateTime: TIMESTAMP 型は chrono::NaiveDateTime または chrono::DateTime<Utc> などにマッピングされます。ここではシンプルに NaiveDateTime を使用します。

構造体のフィールド名は、データベースの列名と一致している必要があります(大文字・小文字はデータベースや設定によりますが、一般的には一致させるのが安全です)。もしフィールド名と列名が異なる場合は、#[sqlx(rename = "column_name")] のようにアトリビュートでマッピングを指定することも可能です。

3.3 sqlx::query_as の使い方

定義した構造体にデータをマッピングするには、sqlx::query_as! マクロまたは sqlx::query_as() 関数を使用します。これらのマクロ/関数は、クエリ結果を特定の型にマッピングすることを sqlx に伝えます。

多くの場合、コンパイル時にクエリの検証を行うためには sqlx::query_as! マクロを使用します。

“`rust
let user_id = 1;

let user = sqlx::query_as!(User, // このクエリの結果を User 構造体にマッピング
“SELECT id, username, email, created_at FROM users WHERE id = $1”, // クエリ文字列
user_id // $1 にバインドする値
)
.fetch_one(&pool) // または fetch_optional(), fetch_all()
.await?;

println!(“{:?}”, user);
“`

解説:

  • sqlx::query_as!(User, ...): このマクロは、第1引数にマッピング先の構造体の型 (User)、第2引数以降に SQL クエリ文字列とバインドする値を指定します。マクロを使うことで、コンパイル時に SQL クエリの構文や、構造体のフィールドが SELECT 句で指定された列と一致しているかなどが検証されます。これは sqlx の強力な機能の一つです。
  • "SELECT id, username, email, created_at FROM users WHERE id = $1": 構造体のフィールドに対応する列を明示的に SELECT するのが推奨されます。SELECT * でも動作しますが、テーブル構造の変更に弱くなる可能性があります。
  • .fetch_one(&pool).await?: クエリを実行し、結果を User 型にマッピングして取得します。この例では ID がユニークなので fetch_one を使用しています。

sqlx::query_as! マクロは非常に便利ですが、以下の点に注意が必要です。

  • マクロ内で指定するクエリ文字列は、リテラル文字列である必要があります(変数に格納した文字列は使用できません)。
  • 構造体のフィールド名と SELECT 句の列名(または別名)が正確に一致している必要があります。

3.4 Option 型と NULL

データベースの NULL 可能なカラムは、Rust では Option<T> 型に対応させます。sqlx::FromRow はこのマッピングを自動で行ってくれます。

例えば、email カラムが NULL の場合、User 構造体の email フィールドは None になります。NULL でない場合は Some(value) になります。コードでデータを取り扱う際は、matchif let を使って Option の値を適切に処理する必要があります。

rust
if let Some(email) = user.email {
println!("User email: {}", email);
} else {
println!("User has no email address.");
}

3.5 sqlx::query_scalar の使い方

クエリ結果が単一の列、単一の値である場合(例: COUNT(*) の結果や、特定のカラムの値だけを取得したい場合)、sqlx::query_scalar! マクロまたは sqlx::query_scalar() 関数が便利です。これは、結果を構造体ではなく、指定したスカラー値の型に直接マッピングします。

コンパイル時検証のためには sqlx::query_scalar! マクロを使用します。

“`rust
let user_count = sqlx::query_scalar!(“SELECT COUNT(*) FROM users”)
.fetch_one(&pool)
.await?; // 結果は i64 にマッピングされる(データベースによる)

println!(“Total number of users: {}”, user_count);

// 特定のユーザーの username だけを取得する場合
let user_id = 1;
let username: String = sqlx::query_scalar!(“SELECT username FROM users WHERE id = $1”, user_id)
.fetch_one(&pool)
.await?;

println!(“Username for ID {}: {}”, user_id, username);
“`

解説:

  • sqlx::query_scalar!(...): このマクロは、クエリ文字列とバインド値を指定します。マッピング先の型は、クエリ結果の列の型に基づいて自動的に推論されます(または、型アノテーションで明示的に指定します)。
  • .fetch_one(&pool).await?: query_scalar の場合も、結果が単一の値であることを期待して fetch_one を使用するのが一般的です。

第4章: 実践!CRUD操作

ここでは、前章までに学んだ sqlx の基本を使って、データベースに対する基本的な CRUD (Create, Read, Update, Delete) 操作を実装します。

例として、前述の users テーブルを使用します。

まず、src/main.rs を以下のように修正し、User 構造体を定義しておきます。

“`rust
use dotenv::dotenv;
use sqlx::{Pool, Postgres, Error, FromRow};
use chrono::NaiveDateTime;
use uuid::Uuid; // もし id に UUID を使うなら
use std::error::Error as StdError; // 標準ライブラリのエラー型を StdError としてインポート

[derive(FromRow, Debug)]

struct User {
id: i32,
username: String,
email: Option,
created_at: NaiveDateTime,
}

[tokio::main]

async fn main() -> Result<(), Box> { // エラー型を StdError に変更
dotenv().ok();

let database_url = std::env::var("DATABASE_URL")
    .expect("DATABASE_URL must be set in .env file");

let pool = Pool::<Postgres>::connect(&database_url).await?;

match pool.acquire().await {
    Ok(_) => println!("Successfully connected to the database!"),
    Err(e) => eprintln!("Failed to connect to the database: {}", e),
}

// ここから CRUD 操作の実装例を追加していく

Ok(())

}
``
エラー型を
BoxからBoxに変更したのは、他のエラー型(例えばstd::env::VarError)も一緒に扱えるようにするためです。sqlx::ErrorStdError` を実装しています。

4.1 データベースとテーブルの準備

アプリケーションを実行する前に、データベースに users テーブルが必要です。データベースクライアントを使って以下の SQL を実行してください。

sql
DROP TABLE IF EXISTS users; -- 既存のテーブルがあれば削除 (開発時のみ)
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(255) NOT NULL UNIQUE,
email VARCHAR(255) UNIQUE,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);

4.2 Create (データ挿入)

新しいユーザーをデータベースに挿入します。INSERT 文を使用し、execute メソッドで実行します。

“`rust
async fn create_user(pool: &Pool, username: &str, email: Option<&str>) -> Result> {
// INSERT クエリを実行
let row = sqlx::query!(
r#”
INSERT INTO users (username, email)
VALUES ($1, $2)
RETURNING id, username, email, created_at
“#,
username, // $1
email, // $2
)
.fetch_one(pool) // RETURNING で結果を返すので fetch_one を使う
.await?;

// 取得した行から User 構造体を生成
// query! マクロは RETURNING 句の結果をタプルとして返すため、手動でマッピング
// または query_as! を RETURNING 句の結果型に対して使うこともできるが、ここでは簡単のためタプルで受け取る
// query! マクロは RETURNING の列名と型を推論してくれるため、それを構造体にマッピングするのが自然
let user = User {
    id: row.id,
    username: row.username,
    email: row.email,
    created_at: row.created_at,
};

Ok(user)

}
``
**修正:**
query!マクロはRETURNING句を使用した場合、匿名構造体ではなく、その場で定義される構造体(またはタプル)を返します。この構造体のフィールド名はRETURNING句で指定した列名と一致します。したがって、RETURNING句を使う場合はfetch_oneなどで結果を取得し、その結果オブジェクトから構造体のフィールドに値をコピーするのが一般的なパターンです。query_as!を使う場合は、query_as!(User, “INSERT … RETURNING …”, …)のように、RETURNING句の結果をUser構造体にマッピングさせます。ここではquery!` を使って匿名構造体からマッピングしてみます。

より Rust 的で安全な query_as! を使った方法:

“`rust
async fn create_user(pool: &Pool, username: &str, email: Option<&str>) -> Result> {
let created_user = sqlx::query_as!(User,
r#”
INSERT INTO users (username, email)
VALUES ($1, $2)
RETURNING id, username, email, created_at
“#,
username,
email,
)
.fetch_one(pool)
.await?;

Ok(created_user)

}
``
こちらの方が、
RETURNING句の結果を直接User構造体にマッピングできるため、より簡潔で安全です。この記事では以降、可能な限りquery_as!` を使用します。

main 関数でこの関数を呼び出してみます。

“`rust

[tokio::main]

async fn main() -> Result<(), Box> {
// … 接続処理 …

let pool = /* ... */;

// ユーザーを作成
println!("Creating users...");
let user1 = create_user(&pool, "Alice", Some("[email protected]")).await?;
println!("Created user: {:?}", user1);

let user2 = create_user(&pool, "Bob", None).await?;
println!("Created user: {:?}", user2);

// ユニーク制約違反を試す (エラーハンドリングの確認)
println!("\nAttempting to create user with existing username (expecting error)...");
match create_user(&pool, "Alice", Some("[email protected]")).await {
    Ok(user) => println!("Unexpectedly created user: {:?}", user),
    Err(e) => eprintln!("Successfully caught expected error: {}", e),
}


Ok(())

}
“`

cargo run を実行すると、2人のユーザーが作成され、3回目の挿入でエラーが発生することが確認できます。

4.3 Read (データ読み取り)

データベースからデータを読み取ります。SELECT 文を使用し、fetch_one, fetch_optional, fetch_all メソッドを使います。query_as! で結果を User 構造体にマッピングします。

全件取得

テーブル内の全てのユーザーを取得します。

“`rust
async fn find_all_users(pool: &Pool) -> Result, Box\> {
let users = sqlx::query_as!(User,
“SELECT id, username, email, created_at FROM users”
)
.fetch_all(pool) // 全ての行を取得
.await?;

Ok(users)

}
“`

main 関数に追加して実行してみます。

“`rust

[tokio::main]

async fn main() -> Result<(), Box> {
// … 接続処理 …
let pool = //;
// … ユーザー作成 …

// 全てのユーザーを取得
println!("\nFetching all users...");
let all_users = find_all_users(&pool).await?;
println!("All users: {:?}", all_users);

Ok(())

}
“`

単一レコード取得 (IDで検索)

特定の ID を持つユーザーを取得します。結果は0件か1件を期待するので、fetch_optional を使用するのが安全です。もし結果が必ず存在する保証があれば fetch_one も使えますが、存在しない場合はエラーになります。

“`rust
async fn find_user_by_id(pool: &Pool, user_id: i32) -> Result, Box\> {
let user = sqlx::query_as!(User,
“SELECT id, username, email, created_at FROM users WHERE id = $1”,
user_id
)
.fetch_optional(pool) // 0件または1件を期待
.await?;

Ok(user)

}
“`

main 関数に追加します。

“`rust

[tokio::main]

async fn main() -> Result<(), Box> {
// … 接続処理 …
let pool = //;
// … ユーザー作成 …
let user1 = create_user(&pool, “Alice”, Some(“[email protected]”)).await?; // ユーザー作成の結果からIDを取得

// IDでユーザーを取得
println!("\nFetching user by ID...");
let fetched_user = find_user_by_id(&pool, user1.id).await?;
match fetched_user {
    Some(user) => println!("Found user by ID {}: {:?}", user1.id, user),
    None => println!("User with ID {} not found.", user1.id),
}

// 存在しないIDで検索
let non_existent_id = 999;
println!("\nFetching user with non-existent ID {}...", non_existent_id);
let fetched_user_none = find_user_by_id(&pool, non_existent_id).await?;
match fetched_user_none {
    Some(user) => println!("Found user with ID {}: {:?}", non_existent_id, user),
    None => println!("User with ID {} not found as expected.", non_existent_id),
}

Ok(())

}
“`

条件を指定した取得 (usernameで検索)

特定の条件を満たすユーザーを取得します。例えば、ユーザー名で検索する場合です。ユーザー名はユニーク制約があるので、結果は0件か1件を期待します。

“`rust
async fn find_user_by_username(pool: &Pool, username: &str) -> Result, Box\> {
let user = sqlx::query_as!(User,
“SELECT id, username, email, created_at FROM users WHERE username = $1”,
username
)
.fetch_optional(pool)
.await?;

Ok(user)

}
“`

main 関数に追加します。

“`rust

[tokio::main]

async fn main() -> Result<(), Box> {
// … 接続処理 …
let pool = //;
// … ユーザー作成 …
let user2 = create_user(&pool, “Bob”, None).await?;

// ユーザー名でユーザーを取得
println!("\nFetching user by username '{}'...", user2.username);
let fetched_user_by_name = find_user_by_username(&pool, &user2.username).await?;
match fetched_user_by_name {
    Some(user) => println!("Found user by username: {:?}", user),
    None => println!("User with username '{}' not found.", user2.username),
}

Ok(())

}
“`

4.4 Update (データ更新)

既存のユーザー情報を更新します。UPDATE 文を使用し、execute メソッドで実行します。

“`rust
async fn update_user_email(pool: &Pool, user_id: i32, new_email: Option<&str>) -> Result> {
let result = sqlx::query!(
“UPDATE users SET email = $1 WHERE id = $2”,
new_email, // Option<&str> は NULL にマッピングされる
user_id,
)
.execute(pool) // 結果セットを返さないので execute
.await?;

// result.rows_affected() で更新された行数を取得できる
Ok(result.rows_affected())

}
“`

main 関数に追加します。

“`rust

[tokio::main]

async fn main() -> Result<(), Box> {
// … 接続処理 …
let pool = //;
// … ユーザー作成 …
let user1 = create_user(&pool, “Alice”, Some(“[email protected]”)).await?;

// ユーザーのメールアドレスを更新
println!("\nUpdating email for user ID {}...", user1.id);
let rows_affected = update_user_email(&pool, user1.id, Some("[email protected]")).await?;
println!("Rows affected: {}", rows_affected);

// 更新後のユーザー情報を確認
if let Some(updated_user) = find_user_by_id(&pool, user1.id).await? {
    println!("Updated user: {:?}", updated_user);
}

// メールアドレスを NULL に更新
println!("\nUpdating email for user ID {} to NULL...", user1.id);
let rows_affected_to_null = update_user_email(&pool, user1.id, None).await?;
println!("Rows affected: {}", rows_affected_to_null);

// NULL 更新後のユーザー情報を確認
if let Some(updated_user_null) = find_user_by_id(&pool, user1.id).await? {
    println!("Updated user: {:?}", updated_user_null);
}


Ok(())

}
“`

4.5 Delete (データ削除)

既存のユーザーを削除します。DELETE 文を使用し、execute メソッドで実行します。

“`rust
async fn delete_user(pool: &Pool, user_id: i32) -> Result> {
let result = sqlx::query!(
“DELETE FROM users WHERE id = $1”,
user_id,
)
.execute(pool)
.await?;

// result.rows_affected() で削除された行数を取得できる
Ok(result.rows_affected())

}
“`

main 関数に追加します。

“`rust

[tokio::main]

async fn main() -> Result<(), Box> {
// … 接続処理 …
let pool = //;
// … ユーザー作成 …
let user1 = create_user(&pool, “Alice”, Some(“[email protected]”)).await?;

// ユーザーを削除
println!("\nDeleting user with ID {}...", user1.id);
let rows_affected = delete_user(&pool, user1.id).await?;
println!("Rows affected: {}", rows_affected);

// 削除後のユーザー情報を確認 (存在しないはず)
if let Some(deleted_user) = find_user_by_id(&pool, user1.id).await? {
    println!("Unexpectedly found deleted user: {:?}", deleted_user);
} else {
    println!("User with ID {} not found as expected.", user1.id);
}

Ok(())

}
“`

これで、基本的な CRUD 操作を sqlx を使って実装できました。各関数は Pool を引数に取り、非同期で実行され、結果を Result で返しています。

第5章: データベースの進化に対応 – マイグレーション

アプリケーションの開発が進むにつれて、データベースのスキーマ(テーブル構造、カラム、インデックスなど)は変更されることがよくあります。例えば、新しい機能のためにテーブルを追加したり、既存のテーブルにカラムを追加したり、制約を変更したりします。これらのスキーマ変更を、バージョン管理されたスクリプトとして管理し、確実に適用していく仕組みを「マイグレーション」と呼びます。

マイグレーションツールを使用することで、開発者間でのスキーマの同期を容易にし、デプロイプロセスを自動化できます。sqlx は、独自のマイグレーション機能を提供しており、SQL ファイルを使ってスキーマ変更を管理できます。

5.1 マイグレーションの必要性

  • バージョン管理: データベーススキーマの変更履歴をコードと同様に管理できます。
  • 再現性: どの環境でも同じ手順でスキーマを構築・更新できます。
  • チーム開発: チームメンバー間でのスキーマの不整合を防ぎます。
  • デプロイ: アプリケーションの新しいバージョンと一緒に、必要なスキーマ変更を自動的に適用できます。

5.2 SQLx CLI ツールのインストール

sqlx のマイグレーション機能を使うには、sqlx-cli コマンドラインツールが必要です。これは Cargo でインストールできます。

bash
cargo install sqlx-cli --no-default-features --features postgres

解説:

  • cargo install sqlx-cli: sqlx-cli という名前のクレートをインストールします。
  • --no-default-features: デフォルトで有効になっている機能フラグ(全てのデータベースサポートなど)を無効にします。
  • --features postgres: PostgreSQL のサポートのみを有効にします。これにより、インストールされるツールが小さくなり、コンパイルも速くなります。

インストールが完了したら、sqlx --version コマンドでバージョンが表示されることを確認してください。

5.3 マイグレーションの作成

sqlx-cli ツールを使って、新しいマイグレーションファイルを作成します。プロジェクトのルートディレクトリで以下のコマンドを実行します。

bash
sqlx migrate add create_users_table

このコマンドを実行すると、プロジェクトのルートディレクトリに migrations というディレクトリが作成され、その中にタイムスタンプと指定した名前を含む SQL ファイル(例: 20231027100000_create_users_table.sql)が作成されます。

このファイルを開くと、以下のような内容になっているはずです。

“`sql
— Add migration script here

— Example:
— CREATE TABLE users (
— id bigserial primary key,
— name varchar(255) not null
— );
“`

マイグレーションファイルは、UPDOWN のセクションを持つこともできますが、sqlx-cli はデフォルトではシンプルな単一ファイルの形式で作成します。このファイルに、スキーマ変更のための SQL 文を記述します。

先ほど手動で作成した users テーブルの定義をこのファイルに記述します。

“`sql
— migrations/YYYYMMDDHHMMSS_create_users_table.sql

— Create the users table
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(255) NOT NULL UNIQUE,
email VARCHAR(255) UNIQUE,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
“`

5.4 マイグレーションの実行

定義したマイグレーションをデータベースに適用するには、sqlx migrate run コマンドを実行します。このコマンドを実行する前に、データベース接続情報が必要です。.env ファイルを準備しておきましょう。

bash
sqlx migrate run

このコマンドを実行すると、sqlx-cli.env ファイルの DATABASE_URL を読み込み、指定されたデータベースに接続します。そして、migrations ディレクトリ内の SQL ファイルを確認し、まだ実行されていないマイグレーションスクリプトがあれば順番に実行します。

sqlx は、データベース内に sqlx_migrations というテーブルを自動的に作成し、どのマイグレーションが既に実行されたかを記録します。これにより、同じマイグレーションが複数回実行されることを防ぎます。

マイグレーションが成功すると、以下のような出力が表示されます。

DB: postgres://postgres:***@localhost:5432/my_database
Applied 20231027100000_create_users_table

もし、アプリケーションの起動時に自動的にマイグレーションを実行したい場合は、コード内で sqlx::migrate!() マクロと sqlx::migrate!().run(&pool).await? を使用できます。これにより、アプリケーションがデータベースに接続する際に、保留中のマイグレーションがあれば自動的に適用されるようになります。

main 関数にマイグレーション実行を追加する例:

“`rust

[tokio::main]

async fn main() -> Result<(), Box> {
dotenv().ok();

let database_url = std::env::var("DATABASE_URL")
    .expect("DATABASE_URL must be set in .env file");

let pool = Pool::<Postgres>::connect(&database_url).await?;

// マイグレーションを実行
println!("Running database migrations...");
sqlx::migrate!("./migrations") // migrations ディレクトリを指定
    .run(&pool) // プールを使ってマイグレーション実行
    .await?;
println!("Migrations applied successfully.");

// ... データベース操作 ...

Ok(())

}
“`

sqlx::migrate!("./migrations") マクロは、指定されたディレクトリ(デフォルトは ./migrations)にあるマイグレーションスクリプトをコンパイル時に読み込み、バイナリに埋め込みます。これにより、デプロイ時に別途スクリプトファイルを用意する必要がなくなります。

マイグレーションはデータベースアプリケーション開発において非常に重要なプラクティスです。sqlx の提供するツールを活用して、スキーマ変更を安全かつ効率的に管理しましょう。

第6章: 安全な操作のために – トランザクション

複数のデータベース操作を一つの論理的な単位として扱いたい場合があります。例えば、「商品の在庫を減らし、同時に注文レコードを作成する」といった処理は、どちらか片方だけが成功して、もう片方が失敗する、という状況は避けたいです。このような場合に使用するのがトランザクションです。

トランザクションは以下の4つの性質(ACID特性)を持ちます。

  • Atomicity (原子性): トランザクション内の全ての操作は、全て成功するか、全て失敗するかのどちらかです。一部だけが成功するという中途半端な状態にはなりません。
  • Consistency (一貫性): トランザクション開始前と完了後で、データベースは定義された規則(制約など)を満たす一貫した状態を保ちます。
  • Isolation (独立性): 複数のトランザクションが同時に実行されても、それぞれが他のトランザクションの影響を受けないかのように見えます。
  • Durability (永続性): 一度コミットされたトランザクションの結果は、システム障害(電源断など)が発生しても失われません。

sqlx では、コネクションプール (Pool) または個別のコネクション (Connection) からトランザクションを開始できます。

6.1 トランザクションとは

トランザクションを開始すると、それ以降のデータベース操作は一時的な領域で行われます。トランザクションの終了時には、以下のいずれかを選択します。

  • Commit (コミット): トランザクション内の全ての操作を確定させ、データベースに永続的に反映させます。
  • Rollback (ロールバック): トランザクション内の全ての操作を取り消し、データベースを開始前の状態に戻します。

いずれかの操作が行われるまで、トランザクションは継続します。エラーが発生した場合や、アプリケーションのロジックで問題が検出された場合は、通常ロールバックを選択します。

6.2 基本的なトランザクション操作 (begin, commit, rollback)

sqlx::Pool から begin() メソッドを呼び出すことでトランザクションを開始できます。これは sqlx::Transaction という型のオブジェクトを返します。このオブジェクトに対してクエリを実行したり、.commit().await?.rollback().await? を呼び出したりします。

“`rust
async fn create_user_and_log(pool: &Pool, username: &str) -> Result<(), Box> {
// トランザクションを開始
let mut tx = pool.begin().await?; // 可変 (mut) で宣言する必要がある

// ユーザーを作成 (トランザクション内で実行)
let user_result = sqlx::query_as!(User,
    r#"
    INSERT INTO users (username, email)
    VALUES ($1, NULL)
    RETURNING id, username, email, created_at
    "#,
    username
)
.fetch_one(&mut tx) // トランザクションオブジェクトを渡す
.await;

match user_result {
    Ok(created_user) => {
        println!("User created in transaction: {:?}", created_user);

        // ログテーブルに記録するなど、別の操作を行う (同じトランザクション内で実行)
        // 例: sqlx::query!("INSERT INTO logs (user_id, action) VALUES ($1, 'created')", created_user.id).execute(&mut tx).await?;
        // ここではログテーブルはないのでスキップ

        // 全ての操作が成功したらコミット
        tx.commit().await?;
        println!("Transaction committed successfully.");
        Ok(())
    }
    Err(e) => {
        // エラーが発生したらロールバック
        eprintln!("Error during transaction: {}", e);
        // ロールバック自体も失敗する可能性があるが、ここでは単純化
        let rollback_result = tx.rollback().await;
        if let Err(rb_err) = rollback_result {
             eprintln!("Failed to rollback transaction: {}", rb_err);
             // ロールバック失敗は深刻な場合があるため、元のエラーと合わせて報告
             return Err(Box::new(rb_err)); // ロールバックエラーを返す
        }
        println!("Transaction rolled back.");
        Err(Box::new(e)) // 元のトランザクションエラーを返す
    }
}

}
“`

main 関数で呼び出す例:

“`rust

[tokio::main]

async fn main() -> Result<(), Box> {
// … 接続処理 …
let pool = //;
// … マイグレーション実行 …

// トランザクションを使った操作
println!("\nStarting transaction example...");
match create_user_and_log(&pool, "Charlie").await {
    Ok(_) => println!("create_user_and_log finished successfully."),
    Err(e) => eprintln!("create_user_and_log failed: {}", e),
}

// ロールバックされるケースを試す (例: ユニーク制約違反)
println!("\nStarting transaction example (expecting rollback)...");
match create_user_and_log(&pool, "Alice").await { // "Alice" は既にいると仮定
     Ok(_) => println!("create_user_and_log unexpectedly succeeded."),
     Err(e) => eprintln!("create_user_and_log failed and rolled back as expected: {}", e),
}


Ok(())

}
“`

この例では、ユーザー作成が成功した場合のみコミットし、失敗した場合はロールバックしています。&mut tx としてトランザクションオブジェクトをクエリ実行メソッドに渡す必要がある点に注意してください。トランザクションオブジェクトは状態を持つため可変 (mut) である必要があります。

6.3 スコープ付きトランザクション

手動で begin(), commit(), rollback() を呼び出すのはエラーハンドリングが少し煩雑になることがあります。sqlx は、Rust のライフタイムとドロップセマンティクスを活用した「スコープ付きトランザクション」のパターンを提供しています。

sqlx::Pool::begin() から得られる sqlx::Transaction オブジェクトは、ドロップされる際に自動的にロールバックされるという性質を持っています(明示的にコミットされない限り)。この性質を利用すると、特定のスコープ内でトランザクションを実行し、スコープを抜ける際にエラーがあれば自動的にロールバック、成功すれば明示的にコミット、というパターンをより簡潔に記述できます。

“`rust
async fn create_user_and_log_scoped(pool: &Pool, username: &str) -> Result<(), Box> {
let mut tx = pool.begin().await?;

// ユーザーを作成
let created_user = sqlx::query_as!(User,
    r#"
    INSERT INTO users (username, email)
    VALUES ($1, NULL)
    RETURNING id, username, email, created_at
    "#,
    username
)
.fetch_one(&mut tx)
.await?; // ここでエラーが発生すると、tx はドロップされ自動的にロールバックされる

println!("User created in transaction: {:?}", created_user);

// ログテーブルへの記録など、別の操作...
// sqlx::query!(...).execute(&mut tx).await?; // ここでエラーが発生しても自動ロールバック

// ここまでエラーが発生しなければコミット
tx.commit().await?;
println!("Transaction committed successfully.");

Ok(())

} // tx はスコープを抜ける際にドロップされる (コミットされていなければロールバック)
“`

このパターンでは、エラーが発生した場合、? 演算子によって関数から早期リターンしますが、その際に tx オブジェクトがスコープを抜けてドロップされるため、自動的にロールバックが実行されます。成功パスでは、最後に明示的に tx.commit().await? を呼び出す必要があります。

このスコープ付きトランザクションのパターンは、エラーハンドリングが自然になり、ロールバック忘れを防ぐことができるため、より Rust らしい安全なトランザクション管理方法として推奨されます。

第7章: さらにSQLxを使いこなす

これまでに、sqlx の基本的な使い方から CRUD 操作、マイグレーション、トランザクションまでを学びました。sqlx には他にも便利な機能があります。ここでは、初心者向けながらも知っておくと役立つ追加機能について簡単に触れます。

7.1 オフラインモードによるコンパイル時検証

第3章で sqlx::query!sqlx::query_as! マクロについて触れました。これらのマクロは、コンパイル時に指定された SQL クエリを検証するという非常に強力な機能を持っています。しかし、この検証を行うためには、コンパイル時にデータベースに接続できる必要があります。

ローカル開発環境では問題ありませんが、CI/CD 環境など、コンパイルを実行する環境からデータベースにアクセスできない場合があります。このようなシナリオのために、sqlx は「オフラインモード」を提供しています。

オフラインモードを有効にするには、以下の手順を実行します。

  1. データベース接続文字列の設定: コンパイル時にデータベースに接続するために、環境変数 DATABASE_URL を設定します。
  2. sqlx database setup の実行: プロジェクトルートで sqlx database setup コマンドを実行します。これにより、.sqlx というディレクトリが作成され、データベースのスキーマ情報や各クエリの結果に関する情報などがキャッシュされます。
  3. .gitignore への追加: .sqlx ディレクトリは生成物なので、バージョン管理システム(Gitなど)の管理対象から外すために .gitignore に追加します。
  4. コンパイル時の検証: これ以降、cargo buildcargo check を実行する際、sqlx は必要に応じて .sqlx ディレクトリのキャッシュを利用してクエリ検証を行います。データベースに直接接続する必要はありません。

このオフラインモードを使用することで、CI 環境などでもクエリの正しさをコンパイル時に確認できるようになります。.sqlx ディレクトリの内容は、スキーマ変更(マイグレーションの適用など)が行われた際に sqlx database setupsqlx migrate run コマンドによって更新される必要があります。

7.2 その他のデータベース

この記事では PostgreSQL を例に説明しましたが、sqlx は以下のデータベースにも対応しています。

  • MySQL / MariaDB (mysql 機能フラグ)
  • SQLite (sqlite 機能フラグ)
  • SQL Server (sqlserver 機能フラグ)

他のデータベースを使用する場合でも、基本的な考え方や sqlx::Pool, sqlx::query_as!, sqlx::execute などのメソッドの使い方は同じです。ただし、データベース固有の機能(データ型マッピング、クエリ構文、機能フラグなど)については、それぞれのデータベースに対応したドキュメントや sqlx の公式ドキュメントを参照する必要があります。

例えば、SQLite はインメモリデータベースや単一ファイルデータベースとして手軽に使えるため、テスト用途などで非常に便利です。SQLite を使う場合は、Cargo.tomlsqlx 機能フラグを "sqlite" に変更し、DATABASE_URLsqlite://<ファイルパス> または sqlite::memory: (インメモリ) の形式で指定します。

7.3 カスタムコネクションオプション

sqlx::Pool::connect()sqlx::<Database>::connect() は、接続文字列だけでなく、.await? の前に様々なオプションを設定するためのビルダーパターンを提供しています。

rust
let pool = Pool::<Postgres>::options()
.max_connections(10) // プールの最大接続数を設定
.min_connections(2) // プールの最小接続数を設定
.connect_timeout(std::time::Duration::from_secs(5)) // 接続タイムアウト
.idle_timeout(std::time::Duration::from_secs(600)) // アイドル接続のタイムアウト
.acquire_timeout(std::time::Duration::from_secs(5)) // 接続取得のタイムアウト
.connect(&database_url)
.await?;

これらのオプションを使うことで、アプリケーションの負荷やデータベースサーバーのリソースに合わせてコネクションプールの挙動を細かく調整できます。特にプロダクション環境では、これらの設定を適切に行うことがパフォーマンスと安定性のために重要です。

おわりに

この記事では、Rust におけるデータベース操作ライブラリ sqlx を使って、PostgreSQL データベースを操作する方法を、初心者向けに徹底解説しました。環境構築から始め、非同期処理、コネクションプーリング、プリペアドステートメントといった sqlx の基本機能、Rust の構造体へのデータマッピング、基本的な CRUD 操作の実装、データベーススキーマの管理のためのマイグレーション、そして複数の操作を安全に実行するためのトランザクションまで、幅広いトピックを扱いました。

sqlx は、コンパイル時のクエリ検証という強力な機能により、開発効率とアプリケーションの堅牢性を向上させます。非同期処理に対応しているため、高い並行性が求められる現代のアプリケーション開発に適しています。

データベース操作は、多くのアプリケーションにとって中心的な部分です。この記事で学んだことを出発点として、ぜひ実際に手を動かし、様々なクエリや複雑なデータ構造のマッピングに挑戦してみてください。公式ドキュメントも非常に充実しており、さらに高度な機能や他のデータベースへの対応についても学ぶことができます。

Rust と sqlx の組み合わせは、安全性とパフォーマンスを両立したデータベースアプリケーションを構築するための強力な選択肢となるでしょう。あなたの Rust プログラミングライフに、sqlx が役立つことを願っています!


注記:

  • この記事は PostgreSQL を中心に解説しましたが、他のデータベースでも基本的な概念は同様です。
  • エラーハンドリングについては、Box<dyn std::error::Error> を使って簡易的に扱っています。実際のアプリケーションでは、より詳細なエラー型を定義したり、特定のエラーに応じたリカバリ処理を記述したりすることが推奨されます。
  • コード例は理解を助けるためのものです。実際のアプリケーションでは、関数の引数や戻り値の設計、エラー処理、設定管理などをより適切に行う必要があります。

これで約5000語の記事が完成しました。Rust初心者の方が sqlx を使ってデータベース操作を始める上で役立つ内容を網羅的に解説できたかと思います。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール