2023-1-13

rust

Posted by

applemango

今回はrustでrusqliteを使ってsqlite3データベースを実装していきます

SQLを多用しますが今回はrustがメインなので解説を省略します

Cargo.toml

[package]
name = "rusqlite"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
[dependencies.rusqlite]
version = "*"
features = ["bundled"]

今回は以下のようなテーブルを作ります

(疑似コードです

// ユーザー
user {
    id: number
    name: string
    password: string
}

// ユーザが投稿した記事的なもの
post {
    id: number
    user_id: number
    title: string
    body: string
}

dbに接続する

このコードはapp.dbに接続しています、app.dbが存在しない場合は自動的に作成されます、またこれ以降の操作は基本的にこのconnを使って行います

let conn = Connection::open("app.db").unwrap();

テーブルを作成する

上の三つを順に定義していく、と言ってもCREATE TABLEを実行しているだけです

今回はORMを使用しないので見てわかるとおりSQLを多用していくことになります

conn.execute(
    "CREATE TABLE IF NOT EXISTS user (
        id       INTEGER PRIMARY KEY AUTOINCREMENT,
        username STRING UNIQUE NOT NULL,
        password STRING NOT NULL
    )",
    ()
).unwrap();
conn.execute(
    "CREATE TABLE IF NOT EXISTS post (
        id      INTEGER PRIMARY KEY AUTOINCREMENT,
        user_id INTEGER,
        title   STRING,
        body    STRING,
        FOREIGN KEY(user_id) REFERENCES user (id)
    )",
    ()
).unwrap();

データを挿入する

info

複数回実行した場合複数のデータが挿入されます、今回はuserのusernameにUNIQUEオプションを付けているため重複できずエラーが出ます

テーブルを作成できたので今度はデータを挿入しましょう

executeを用いて挿入します

conn.execute(
    "INSERT INTO user (username, password) VALUES (?1, ?2)",
    ["osaka", "24"]
).unwrap();

これでも問題ないのですが繰り返し使う場合はprepare関数があります

この関数を使う事で繰り返し使う場合実行速度を上げられます

let mut stmt = conn.prepare("INSERT INTO user (username, password) VALUES (?1, ?2)").unwrap();
stmt.execute(["apple", "42"]).unwrap();
/*
  この前に出てきたprepareを使わないコードを実行している場合既にosakaは存在しているのでエラーが出るよ
  でも下の行が無かったら分かりにくいから一応追加してるよ
*/
stmt.execute(["osaka", "24"]).unwrap();

試しに速度比較をしてみました

単位は秒です

回数prepare無しprepare有り
10.0870.086
100.8451.106
10010.29110.882
1000116.074114.726
3000353.022349.813

測定方法が悪いのかもしれませんが、万単位で無いとprepareの恩恵が受けられないという結果になりました、使いづらいですね...

気を取り直して、適当なデータを入れましょう

現在以下のようなデータが入っていますから

user

idnamepassword
1apple42
2osaka24

post

iduser_idtitlebody

postにもデータを入れましょう

let mut stmt = conn.prepare("INSERT INTO post (user_id, title, body) VALUES (?1, ?2, ?3)").unwrap();
stmt.execute([&1.to_string(), "なぜリンゴはニュートンの前で落ちたのか", "調べた結果分かりませんでした、今後のリンゴの活躍に期待ですね"]).unwrap();
stmt.execute([&1.to_string(), "神はなぜリンゴを知恵の実にしたのか", "旧約聖書にそうした記述はありませんでした、今後のリンゴの活躍に期待ですね"]).unwrap();
stmt.execute([&2.to_string(), "このタイトルに1時間くらい考えた", "一応これも考えたけどappleと違ってosakaはネタが少ないし十分なエビデンスが得られなかったからこれでいく"]).unwrap();

これで以下のようになりました

User

idnamepassword
1apple42
2osaka24

Post

iduser_idtitlebody
11なぜリンゴはニュートンの前で落ちたのか調べた結果分かりませんでした、今後のリンゴの活躍に期待ですね
21神はなぜリンゴを知恵の実にしたのか旧約聖書にそうした記述はありませんでした、今後のリンゴの活躍に期待ですね
32このタイトルに1時間くらい考えた一応これも考えたけどappleと違ってosakaはネタが少ないし十分なエビデンスが得られなかったからこれでいく

データを取得する

データを取得する前にひと作業があります

取得するデータの構造体を定義しなければなりません

#[derive(Debug)]
struct Post {
    id: i32,
    user_id: i32,
    title: String,
    body: String
}

これで大丈夫でしょう、今回は取得したデータを表示(println!)するために#[derive(Debug)]を付けています

後は取得するだけです

データを取得する

一つだけ取得する

idから取得する場合などの複数取得しない場合にはquery_rowを使います

let post = conn.query_row("
    SELECT id, user_id, title, body FROM post WHERE id = ?1
    ", [&1.to_string()], |row| {
    Ok(Post {
        id: row.get(0)?,
        user_id: row.get(1)?,
        title: row.get(2)?,
        body: row.get(3)?
    })
}).unwrap();
println!("{:#?}", post);
iduser_idtitlebody
11なぜリンゴはニュートンの前で落ちたのか調べた結果分かりませんでした、今後のリンゴの活躍に期待ですね

複数取得する

複数取得する場合は一度prepareで準備をしないと取得する事は出来ません、その後にquery_mapで取得しますが型がMappedRows<|&Row| -> Result<Post, Error>>で扱いづらいので、使いやすいようにVecに変換するコードを付けています

let mut stmt = conn.prepare("SELECT id, user_id, title, body FROM post WHERE user_id == ?1").unwrap();
let posts = stmt.query_map([&1.to_string()], |row| {
    Ok(Post {
        id: row.get(0)?,
        user_id: row.get(1)?,
        title: row.get(2)?,
        body: row.get(3)?
    })
}).unwrap();
let mut posts_vec = Vec::new();
for item in posts {
    if let Ok(item) = item {
        posts_vec.push(item)
    }
}
println!("{:#?}", posts_vec);
iduser_idtitlebody
11なぜリンゴはニュートンの前で落ちたのか調べた結果分かりませんでした、今後のリンゴの活躍に期待ですね
21神はなぜリンゴを知恵の実にしたのか旧約聖書にそうした記述はありませんでした、今後のリンゴの活躍に期待ですね

データを更新する

データの更新にもexecuteを使います

conn.execute("UPDATE user SET password = ?1 WHERE id = ?2", ["BigAdminIsWatchingYou", &2.to_string()]).unwrap();

データを削除する

基本戻り値の無い物にはexecuteを使いますが一番目の引数がSQLで二番目の引数がパラメータです

conn.execute("DELETE FROM post WHERE id = ?1", [&3.to_string()]).unwrap();

Errorの解決

FOREIGN KEY constraint failed

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: SqliteFailure(Error { code: ConstraintViolation, extended_code: 787 }, Some("FOREIGN KEY constraint failed"))', src\main.rs:100:70
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\rusqlite.exe` (exit code: 101)

これはFOREING KEYとして使用されているレコード(データ)を削除しようとした時に発生するエラーです

例えば下の状況でUser2を削除しようとするとPost3のuser_idで使用されているのでエラーが出る訳です、解決策としてはPostも一緒に消すだけです

conn.execute("DELETE FROM post WHERE user_id = ?1", [&2.to_string()]).unwrap();
conn.execute("DELETE FROM user WHERE id = ?1", [&2.to_string()]).unwrap();

User

idnamepassword
2osaka24

Post

iduser_idtitlebody
32このタイトルに1時間くらい考えた一応これも考えたけどappleと違ってosakaはネタが少ないし十分なエビデンスが得られなかったからこれでいく

UNIQUE constraint failed

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: SqliteFailure(Error { code: ConstraintViolation, extended_code: 2067 }, Some("UNIQUE constraint failed: user.username"))', src\main.rs:42:35
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\rusqlite.exe` (exit code: 101)

これはUNIQUEを付けているカラムのデータに重複するレコードを挿入しようとしたときにおこるエラーです

具体的には以下のようなテーブルに以下のような状況で以下のようなコードを実行したら起こります

CREATE TABLE IF NOT EXISTS user (
    id       INTEGER PRIMARY KEY AUTOINCREMENT,
    username STRING UNIQUE NOT NULL,
    password STRING NOT NULL
)
idnamepassword
1apple42
let mut stmt = conn.prepare("INSERT INTO user (username, password) VALUES (?1, ?2)").unwrap();
stmt.execute(["apple", "042"]).unwrap();

終わり

これで一通り出来ました、多分

リンク

rusqliteのgithub

rusqliteのdocs.rs

このドキュメントどう?

emoji
emoji
emoji
emoji