rusqliteを使ってみた
今回はRustからSQLiteを使うためのrusqlite
に入門したので、その紹介をしようと思います。
rusqliteとは?
公式のGitHubページによると、ruqslite
はSQLiteをRustから利用するためのラッパーということです。私自身、Pythonを使う時はsqlite3
を用いて接続しており、DBの環境構築が不要なので簡単な実装などには利用しています。そこで、Rustでも使えたらいいなと思っていたら使えるとのことだったので今回使ってみました。
rusqliteを使ってみる
環境構築
まずはcargo
を用いて検証環境を用意します。
cargo new rusqlite_test
そしてCargo.toml
で以下を追記しました。
[dependencies]
rusqlite = { version = "0.36.0", features = ["bundled"] }
サンプルを動かしてみる
公式サンプルで提供されているコードをまずは動かしてみます。
use rusqlite::{Connection, Result};
#[derive(Debug)]
struct Person {
id: i32,
name: String,
data: Option<Vec<u8>>,
}
fn main() -> Result<()> {
let conn = Connection::open_in_memory()?;
conn.execute(
"CREATE TABLE person (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
data BLOB
)",
(), // empty list of parameters.
)?;
let me = Person {
id: 0,
name: "Steven".to_string(),
data: None,
};
conn.execute(
"INSERT INTO person (name, data) VALUES (?1, ?2)",
(&me.name, &me.data),
)?;
let mut stmt = conn.prepare("SELECT id, name, data FROM person")?;
let person_iter = stmt.query_map([], |row| {
Ok(Person {
id: row.get(0)?,
name: row.get(1)?,
data: row.get(2)?,
})
})?;
for person in person_iter {
println!("Found person {:?}", person.unwrap());
}
Ok(())
}
それではビルドして実行してみます。
cargo build
cargo run
そうすると以下のようにidが1で名前がSteven、データがないユーザが作成され、クエリで取得できたことが確認できます。
Found person Person { id: 1, name: "Steven", data: None }
このサンプルではメモリ上で作業しているため、プログラムが終了するとデータベースの情報が消えてしまいます。そのため、同じコードを実行しても毎回同じ結果になります(IDはプライマリーキーであり、データが保持されていれば自動でインクリメントされるはず)。
そこで、次はconnection.db
という名前でファイルに結果を保存するようにしてみます。コネクション作成部分を以下のように変更します。
let conn = Connection::open("connection.db")
一回め実行すると先ほどと同じような結果となり、作業フォルダにconnection.db
が作成されています。そしてもう一度実行すると以下のようなエラーが発生します。
Error: SqlInputError { error: Error { code: Unknown, extended_code: 1}, msg: "table person already exists", sql: "CREATE TABLE person(\n id INTEGER PRIMARY KEY,\n name TEXT NOT NULL,\n data BLOB\n)", offset: 13 }
このエラーの原因として、コード上でperson
テーブルの作成をしていますが、作成時にテーブルがすでに存在しているか確認せず作成しようとするため、データを保持するように変更を加えたためエラーになっています。そこで、テーブル作成部分を以下のように変更してみます。
conn.execute(
"CREATE TABLE IF NOT EXISTS person (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
data BLOB
)",
(), // empty list of parameters.
)?;
このように変更すると、2回目以降の実行ではすでにテーブルがあるためCREATE TABLE
は実行されずエラーが発生しません。例えば4回実行すると以下のように4つ同じデータが保持されます。
Found person Person { id: 1, name: "Steven", data: None }
Found person Person { id: 2, name: "Steven", data: None }
Found person Person { id: 3, name: "Steven", data: None }
Found person Person { id: 4, name: "Steven", data: None }
少し改変してみる!
先ほどのコードを少し改変し、人物に紐づくタグを管理する仕組みを導入してみます。変更点は以下になります。
-
tag
テーブルを追加し、タグを保存する -
person_tag
テーブルを追加し、person
テーブルとtag
テーブルの紐付けを定義する -
person_tag
を用いて人物とタグの情報を連結して結果を表示する
また、流行りに乗っかり、実装方針を伝えた上でclaude
に実装させてみました(ちゃんと理解したことを後ほど述べます!)。そのコードは以下になります。
use rusqlite::{Connection, Result};
#[derive(Debug)]
struct Person {
id: i32,
name: String,
data: Option<Vec<u8>>,
}
#[derive(Debug)]
struct Tag {
id: i32,
name: String,
}
#[derive(Debug)]
struct PersonTag {
person_id: i32,
tag_id: i32,
}
#[derive(Debug)]
struct PersonWithTags {
id: i32,
name: String,
tags: Vec<String>,
}
fn main() -> Result<()> {
let conn = Connection::open("connection.db")?;
// Create tables
conn.execute(
"CREATE TABLE IF NOT EXISTS person (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
data BLOB
)",
(),
)?;
conn.execute(
"CREATE TABLE IF NOT EXISTS tag (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE
)",
(),
)?;
conn.execute(
"CREATE TABLE IF NOT EXISTS person_tag (
person_id INTEGER,
tag_id INTEGER,
PRIMARY KEY (person_id, tag_id),
FOREIGN KEY (person_id) REFERENCES person (id),
FOREIGN KEY (tag_id) REFERENCES tag (id)
)",
(),
)?;
// Insert sample data
conn.execute(
"INSERT OR IGNORE INTO person (name, data) VALUES (?1, ?2)",
("Alice", None::<Vec<u8>>),
)?;
conn.execute(
"INSERT OR IGNORE INTO person (name, data) VALUES (?1, ?2)",
("Bob", None::<Vec<u8>>),
)?;
conn.execute("INSERT OR IGNORE INTO tag (name) VALUES (?1)", ("Developer",))?;
conn.execute("INSERT OR IGNORE INTO tag (name) VALUES (?1)", ("Designer",))?;
conn.execute("INSERT OR IGNORE INTO tag (name) VALUES (?1)", ("Manager",))?;
// Associate tags with persons
conn.execute(
"INSERT OR IGNORE INTO person_tag (person_id, tag_id)
SELECT p.id, t.id FROM person p, tag t
WHERE p.name = 'Alice' AND t.name IN ('Developer', 'Designer')",
(),
)?;
conn.execute(
"INSERT OR IGNORE INTO person_tag (person_id, tag_id)
SELECT p.id, t.id FROM person p, tag t
WHERE p.name = 'Bob' AND t.name IN ('Developer', 'Manager')",
(),
)?;
// Query with JOIN to get persons with their tags
let mut stmt = conn.prepare(
"SELECT p.id, p.name, GROUP_CONCAT(t.name, ', ') as tags
FROM person p
LEFT JOIN person_tag pt ON p.id = pt.person_id
LEFT JOIN tag t ON pt.tag_id = t.id
GROUP BY p.id, p.name
ORDER BY p.id"
)?;
let person_iter = stmt.query_map([], |row| {
Ok(PersonWithTags {
id: row.get(0)?,
name: row.get(1)?,
tags: row.get::<_, Option<String>>(2)?
.map(|s| s.split(", ").map(|tag| tag.to_string()).collect())
.unwrap_or_else(Vec::new),
})
})?;
for person in person_iter {
println!("Found person with tags: {:?}", person.unwrap());
}
Ok(())
}
これを実行すると以下のようになります。
Found person Person { id: 1, name: "Alice", tags: ["Developer", "Designer"] }
Found person Person { id: 2, name: "Bob", tags: ["Developer", "Manager"] }
結果は期待した通りになっていました。claude
で生成されたコードを読み解いていきます。
-
struct
を用いてドメインモデルを構造体として定義している。PersonWithTagsは結合した結果を保存するためのモデルである -
CREATE TABLE
で新たに追加したtag
テーブルとperson_tag
テーブルを作成 -
tag
テーブルに3つのタグを登録し、person_tag
登録時にユーザごとに値を指定して中間テーブルを作成 -
LEFT JOIN
を用いてユーザに紐づくタグを全て取得し、それらを結合して表示している
私が指定したのはsrc/main.rsの実装を参考に、src/join.rsに人物に紐づくタグを定義し、JOINすることでデータを取得できるコードを実装して
というプロンプトで指定したのですが、うまく意図を汲み取って実装してくれたようです。
まとめ
今回はrusqlite
を使ってみました。個人実装でrusqlite
を使ってみ始めたのがきっかけで本記事を執筆しました。また、claude
を利用することによりコーディングアシストがうまく機能していることも感じることができました。まだRustに入門して数日しか経っていませんが、実装がとても楽しいのでRustに関連する記事もどんどん出していければと思います。
Discussion