はい、承知いたしました。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 のエコシステムでは tokio
と async-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
シンタックスで記述され、tokio
や async-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
に伝えるための型マーカーです。?
演算子は、connect
がResult
を返すため、エラーが発生した場合に関数から早期リターンするために使用します。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
// … データベース操作 …
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
トレイト
sqlx
は sqlx::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
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)
になります。コードでデータを取り扱う際は、match
や if 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
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::Errorは
StdError` を実装しています。
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
// 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
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
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
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
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
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
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
— );
“`
マイグレーションファイルは、UP
と DOWN
のセクションを持つこともできますが、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
// トランザクションを開始
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
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
は「オフラインモード」を提供しています。
オフラインモードを有効にするには、以下の手順を実行します。
- データベース接続文字列の設定: コンパイル時にデータベースに接続するために、環境変数
DATABASE_URL
を設定します。 sqlx database setup
の実行: プロジェクトルートでsqlx database setup
コマンドを実行します。これにより、.sqlx
というディレクトリが作成され、データベースのスキーマ情報や各クエリの結果に関する情報などがキャッシュされます。.gitignore
への追加:.sqlx
ディレクトリは生成物なので、バージョン管理システム(Gitなど)の管理対象から外すために.gitignore
に追加します。- コンパイル時の検証: これ以降、
cargo build
やcargo check
を実行する際、sqlx
は必要に応じて.sqlx
ディレクトリのキャッシュを利用してクエリ検証を行います。データベースに直接接続する必要はありません。
このオフラインモードを使用することで、CI 環境などでもクエリの正しさをコンパイル時に確認できるようになります。.sqlx
ディレクトリの内容は、スキーマ変更(マイグレーションの適用など)が行われた際に sqlx database setup
や sqlx 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.toml
の sqlx
機能フラグを "sqlite"
に変更し、DATABASE_URL
を sqlite://<ファイルパス>
または 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
を使ってデータベース操作を始める上で役立つ内容を網羅的に解説できたかと思います。