2023-1-13
Posted by
今回は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有り |
---|---|---|
1 | 0.087 | 0.086 |
10 | 0.845 | 1.106 |
100 | 10.291 | 10.882 |
1000 | 116.074 | 114.726 |
3000 | 353.022 | 349.813 |
測定方法が悪いのかもしれませんが、万単位で無いとprepare
の恩恵が受けられないという結果になりました、使いづらいですね...
気を取り直して、適当なデータを入れましょう
現在以下のようなデータが入っていますから
user
id | name | password |
---|---|---|
1 | apple | 42 |
2 | osaka | 24 |
post
id | user_id | title | body |
---|
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
id | name | password |
---|---|---|
1 | apple | 42 |
2 | osaka | 24 |
Post
id | user_id | title | body |
---|---|---|---|
1 | 1 | なぜリンゴはニュートンの前で落ちたのか | 調べた結果分かりませんでした、今後のリンゴの活躍に期待ですね |
2 | 1 | 神はなぜリンゴを知恵の実にしたのか | 旧約聖書にそうした記述はありませんでした、今後のリンゴの活躍に期待ですね |
3 | 2 | このタイトルに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);
id | user_id | title | body |
---|---|---|---|
1 | 1 | なぜリンゴはニュートンの前で落ちたのか | 調べた結果分かりませんでした、今後のリンゴの活躍に期待ですね |
複数取得する
複数取得する場合は一度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);
id | user_id | title | body |
---|---|---|---|
1 | 1 | なぜリンゴはニュートンの前で落ちたのか | 調べた結果分かりませんでした、今後のリンゴの活躍に期待ですね |
2 | 1 | 神はなぜリンゴを知恵の実にしたのか | 旧約聖書にそうした記述はありませんでした、今後のリンゴの活躍に期待ですね |
データを更新する
データの更新にも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
として使用されているレコード(データ)を削除しようとした時に発生するエラーです
例えば下の状況でUser
2を削除しようとするとPost
3の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
id | name | password |
---|---|---|
2 | osaka | 24 |
Post
id | user_id | title | body |
---|---|---|---|
3 | 2 | このタイトルに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
)
id | name | password |
---|---|---|
1 | apple | 42 |
let mut stmt = conn.prepare("INSERT INTO user (username, password) VALUES (?1, ?2)").unwrap();
stmt.execute(["apple", "042"]).unwrap();
終わり
これで一通り出来ました、多分
リンク
rusqliteのgithub
rusqliteのdocs.rs
このドキュメントどう?