😊

rusqliteを使ってみた

に公開

今回はRustからSQLiteを使うためのrusqliteに入門したので、その紹介をしようと思います。

rusqliteとは?

公式のGitHubページによると、ruqsliteはSQLiteをRustから利用するためのラッパーということです。私自身、Pythonを使う時はsqlite3を用いて接続しており、DBの環境構築が不要なので簡単な実装などには利用しています。そこで、Rustでも使えたらいいなと思っていたら使えるとのことだったので今回使ってみました。

https://github.com/rusqlite/rusqlite

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