RustからSQLiteにデータを詰め込み、取り出すぞい

2021-12-09

はじめに

EDINET APIで取得するデータをSQLiteに詰め込んでいきたいなと思っています。 ToDOアプリのサンプルがあったのでそれを見ていきましょう。

『実践 Rustプログラミング入門』サンプルプログラム

他人の褌で相撲を取るこの記事です。 1次ソースを基本的には御覧ください。 一番くわしいです。

Rustから呼び出すHTMLを作る

前回書いた記事と核となるところは変えてないですが。 add buttonを押されたら、/addとして用意したAPIを叩きます。

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <title>App</title>
</head>
<body>
    <div>
        {% for entry in entries %}
        <div>
            <div>id: {{ entry.id }}, text: {{ entry.text }}</div>
            <form action="/delete" method="post">
                <input type="hidden" name="id" value="{{ entry.id }}">
                <button>delete</button>
            </form>
        </div>
        {% endfor %}
    </div>

    <form action="/add" method="post">
        <div>
            <input name="text">
        </div>
        <div>
            <button>add</button>
        </div>
    </form>
</body>

</html>

次に、RustでAPIを用意していきます。

RustでSQLiteにアクセスするために

今回はAPIを叩いてSQLiteにアクセスするので、 SQLiteを操作するためにSQLライブラリを導入しましょう。

 cargo add r2d2 r2d2_sqlite

データベースの初期化

Sqliteはローカルにデータベースを保持してデータを溜め込んで行くので その溜め込むデータベースを作りましょう。

let manager = SqliteConnectionManager::file("test.db");
let pool = Pool::new(manager).expect("Failed to initialize the connection pool");
let conn = pool.get().expect("Failed to initialize the connection pool.");

conn.execute(
    "CREATE TABLE IF NOT EXISTS todo (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        text TEXT NOT NULL
    )",
    params![],
).expect("Failed to create a table `todo`.");

thread ‘main’ panicked at ‘Failed to create a table todo.: SqliteFailure(Error { code: Unknown, extended_code: 1 }, Some(“AUTOINCREMENT is only allowed on an INTEGER PRIMARY KEY”))’, src\main.rs:55:7

エラーがでて困ったさんのときは、SQL構文に誤りがあります。 このタイプのエラーはSQL式が間違えているときに出ます。 構文が間違えていないか見直すといいでしょう、

Compiling todolist v0.1.0 (E:\SourceCode\Rust\todolist)
   Finished dev [unoptimized + debuginfo] target(s) in 49.89s
    Running `target\debug\todolist.exe`
thread 'main' panicked at 'Failed to create a table `todo`.: SqliteFailure(Error { code: Unknown, extended_code: 1 }, Some("AUTOINCREMENT is only allowed on an INTEGER PRIMARY KEY"))', src\main.rs:55:7
stack backtrace:
  0:     0x7ff73384b68e - std::backtrace_rs::backtrace::dbghelp::trace
                              at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35\/library\std\src\..\..\backtrace\src\backtrace\dbghelp.rs:98
  1:     0x7ff73384b68e - std::backtrace_rs::backtrace::trace_unsynchronized
                              at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35\/library\std\src\..\..\backtrace\src\backtrace\mod.rs:66
  2:     0x7ff73384b68e - std::sys_common::backtrace::_print_fmt
                              at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35\/library\std\src\sys_common\backtrace.rs:67
  3:     0x7ff73384b68e - std::sys_common::backtrace::_print::impl$0::fmt
                              at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35\/library\std\src\sys_common\backtrace.rs:46
  4:     0x7ff733860d6a - core::fmt::write
                              at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35\/library\core\src\fmt\mod.rs:1150
  5:     0x7ff733845c88 - std::io::Write::write_fmt<std::sys::windows::stdio::Stderr>       
                              at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35\/library\std\src\io\mod.rs:1667
  6:     0x7ff73384e1f6 - std::sys_common::backtrace::_print
                              at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35\/library\std\src\sys_common\backtrace.rs:49
  7:     0x7ff73384e1f6 - std::sys_common::backtrace::print
                              at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35\/library\std\src\sys_common\backtrace.rs:36
  8:     0x7ff73384e1f6 - std::panicking::default_hook::closure$1
                              at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35\/library\std\src\panicking.rs:210
  9:     0x7ff73384dce4 - std::panicking::default_hook
                              at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35\/library\std\src\panicking.rs:227
(略)

Error enumを作る

MyError enumの中にSQLiteのエラーを追加する。

enum MyError {
    #[error("Failed to render HTML")]
    AskamaError(#[from] askama::Error),

    #[error("Failed to get connection")]
    ConnectionPoolError(#[from] r2d2::Error),

    #[error("Failed SQL execution")]
    SQLiteError(#[from] rusqlite::Error),
}

SQLiteのデータを取り出す

ルートパスを叩くと、自動でSQLiteからデータを抜き出すようにします。

コネクタを作り、そのコネクタを使ってデータベースにアクセスします。

let conn = db.get()?;

データから抜き出すのはこう書きました。 抜き出したパラメータをrowsに詰め込んでいます。

let mut statement = conn.prepare("SELECT id, text FROM todo")?;
let rows = statement.query_map(params![], |row| {
    let id = row.get(0)?;
    let text = row.get(1)?;
    Ok(TodoEntry { id, text })
})?;

あとは取り出す所まとめて、こう書きました。

#[get("/")]
async fn index(db: web::Data<Pool<SqliteConnectionManager>>) -> Result<HttpResponse, MyError> {
    let conn = db.get()?;
    let mut statement = conn.prepare("SELECT id, text FROM todo")?;
    let rows = statement.query_map(params![], |row| {
        let id = row.get(0)?;
        let text = row.get(1)?;
        Ok(TodoEntry { id, text })
    })?;

    let mut entries = Vec::new();
    for row in rows {
        entries.push(row?);
    }
    let html = IndexTemplate { entries };
    let response_body = html.render()?;
    Ok(HttpResponse::Ok()
        .content_type("text/html")
        .body(response_body))
}

SQLiteにデータを詰め込む

addが呼ばれたら、Inputタグで指定された文字列を取得して、 その文字列をINSERTでDBに詰め込みます。

#[post("/add")]
async fn add_todo(
    params: web::Form<AddParams>,
    db: web::Data<r2d2::Pool<SqliteConnectionManager>>,
) -> Result<HttpResponse, MyError> {
    let conn = db.get()?;
    conn.execute("INSERT INTO todo (text) VALUES (?)", &[&params.text])?;
    Ok(HttpResponse::SeeOther()
        .header(header::LOCATION, "/")
        .finish())
}

SQLiteのデータを削除する

消すのはこうです。詰め込むのとやってるのは同じです。

#[post("/delete")]
async fn delete_todo(
    params: web::Form<DeleteParams>,
    db: web::Data<r2d2::Pool<SqliteConnectionManager>>,
) -> Result<HttpResponse, MyError> {
    let conn = db.get()?;
    conn.execute("DELETE FROM todo WHERE id=?", &[&params.id])?;
    Ok(HttpResponse::SeeOther()
        .header(header::LOCATION, "/")
        .finish())
}

全文

Rust部分はこんな感じです。

use actix_web::{get, http::header, post, web, App, HttpResponse, HttpServer, ResponseError};
use askama::Template;
use r2d2::Pool;
use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::params;
use serde::Deserialize;
use thiserror::Error;

#[derive(Deserialize)]
struct AddParams {
    text: String,
}

#[derive(Deserialize)]
struct DeleteParams {
    id: u32,
}
struct TodoEntry {
    id: u32,
    text: String,
}

#[derive(Template)]
#[template(path = "index.html")]
struct IndexTemplate {
    entries: Vec<TodoEntry>,
}
#[derive(Error, Debug)]
enum MyError {
    #[error("Failed to render HTML")]
    AskamaError(#[from] askama::Error),

    #[error("Failed to get connection")]
    ConncectionPoolError(#[from] r2d2::Error),

    #[error("Failed SQL execution")]
    SQLiteError(#[from] rusqlite::Error),
}

impl ResponseError for MyError {}

#[post("/add")]
async fn add_todo(
    params: web::Form<AddParams>,
    db: web::Data<r2d2::Pool<SqliteConnectionManager>>,
) -> Result<HttpResponse, MyError> {
    let conn = db.get()?;
    conn.execute("INSERT INTO todo (text) VALUES (?)", &[&params.text])?;
    Ok(HttpResponse::SeeOther()
        .header(header::LOCATION, "/")
        .finish())
}

#[post("/delete")]
async fn delete_todo(
    params: web::Form<DeleteParams>,
    db: web::Data<r2d2::Pool<SqliteConnectionManager>>,
) -> Result<HttpResponse, MyError> {
    let conn = db.get()?;
    conn.execute("DELETE FROM todo WHERE id=?", &[&params.id])?;
    Ok(HttpResponse::SeeOther()
        .header(header::LOCATION, "/")
        .finish())
}

#[get("/")]
async fn index(db: web::Data<Pool<SqliteConnectionManager>>) -> Result<HttpResponse, MyError> {
    let conn = db.get()?;
    let mut statement = conn.prepare("SELECT id, text FROM todo")?;
    let rows = statement.query_map(params![], |row| {
        let id = row.get(0)?;
        let text = row.get(1)?;
        Ok(TodoEntry { id, text })
    })?;

    let mut entries = Vec::new();
    for row in rows {
        entries.push(row?);
    }
    let html = IndexTemplate { entries };
    let response_body = html.render()?;
    Ok(HttpResponse::Ok()
        .content_type("text/html")
        .body(response_body))
}

#[actix_web::main]
async fn main() -> Result<(), actix_web::Error> {
    let manager = SqliteConnectionManager::file("todo.db");
    let pool = Pool::new(manager).expect("Failed to initialize the connection pool.");
    let conn = pool
        .get()
        .expect("Failed to get the connection from the pool.");
    conn.execute(
        "CREATE TABLE IF NOT EXISTS todo (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            text TEXT NOT NULL
        )",
        params![],
    )
    .expect("Failed to create a table `todo`.");
    HttpServer::new(move || {
        App::new()
            .service(index)
            .service(add_todo)
            .service(delete_todo)
            .data(pool.clone())
    })
        .bind("127.0.0.1:1002")?
        .run()
        .await?;
    Ok(())
}

結果出力

こんな感じです。

まとめ

これでRustからデータベース(SQLite)を使えるようになりましたね。 データは原油と言われるぐらい最近重要性をましてますからね。 まぁ原油も精油しないと使えないようにただあるだけだと、ゴミでいかにうまく加工できるかが大切らしいですね。

今日はこんな所で!!おわり!!!