Rust   |   Actix-web + Tera + SQLite   |   CRUD

ホーム  

概要

Actix-web + Tera + SQLite の組み合わせで CRUD を実装しました。

SQLite

2025年9月29日 この節を追加しました。

このコーナーでは、データベースとして SQLite を使っています。 SQLite を次のようにしてインストールしてください。


# Debian 系にインストールする
sudo apt install sqlite3
sudo apt install libsqlite3-dev

# RPM 系にインストールする
sudo dnf install sqlite
sudo dnf install sqlite-devel

# macOS にインストールする、次のコマンドだけでデベロッパーライブラリもインストールされます。
brew install sqlite

# Windows にインストールする
SQLite をインストールすることはできますが、デベロッパーライブラリのインストールにはまだ成功していません。
ライブラリのインストールは不要との説もありますが、実際には動作しません。


Cargo.toml


[package]
name = "tera"
version = "0.1.0"
edition = "2024"

[dependencies]
actix-web = "4.11.0"
rusqlite = "0.37.0"
serde = { version = "1.0.226", features = ["derive"] }
tera = "1.20.0"


main.rs


use actix_web::{web, App, Responder, HttpResponse, HttpServer};
use actix_web::web::Redirect;
use tera::Tera;
use rusqlite::{Connection, params};
use serde::{Serialize, Deserialize};


// Todoのデータ構造を定義
#[derive(Serialize, Deserialize)]
struct Todo {
    id: i32,
    title: String,
    completed: bool,
}

#[actix_web::main]
async fn main() -> std::io::Result<()>
{
    let conn = Connection::open("todo.db"); // データベースファイルを開く/作成する
    // テーブルが存在しない場合に作成する
    let _ = conn.expect("REASON").execute(
        "CREATE TABLE IF NOT EXISTS todos (
            id INTEGER PRIMARY KEY,
            title TEXT NOT NULL,
            completed BOOLEAN NOT NULL
        )",
        [],
    );
    HttpServer::new(|| {
        // templatesディレクトリ内のHTMLを読み込む
        let tera = Tera::new("templates/**/*").expect("Failed to initialize Tera");
        App::new()
            .app_data(web::Data::new(tera))
            .service(web::resource("/").to(index))
            .service(web::resource("/create").to(create))
            .service(web::resource("/update").to(update))
            .service(web::resource("/delete").to(delete))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

async fn index(tera: web::Data<Tera>) -> impl Responder
{
    let conn = Connection::open("todo.db").unwrap();
    let mut stat = conn.prepare("SELECT * FROM todos").unwrap();
    let mut rows = stat.query([]).unwrap();

    let mut todos = Vec::new();
    while let Ok(Some(row)) =  rows.next() {
            let i_d: i32 = row.get(0).unwrap();
            let todo: String = row.get(1).unwrap();
            let comp: bool = row.get(2).unwrap();
            todos.push(Todo {id: i_d, title: todo, completed: comp});
    }
    let mut context = tera::Context::new();
    context.insert("todos", &todos);
    let rendered = tera.render("index.html", &context).unwrap(); // index.htmlをレンダリング
    HttpResponse::Ok().body(rendered)
}

async fn create(form: web::Form<Todo>) -> Redirect
{
    let conn = rusqlite::Connection::open("todo.db").unwrap();
    conn. execute ( "INSERT INTO todos (title, completed) VALUES (?1, ?2)", params![form.title, form.completed],).unwrap();
    Redirect::to("/")
}

async fn update(form: web::Form<Todo>) -> Redirect
{
    let conn = rusqlite::Connection::open("todo.db").unwrap();
    conn. execute (
        "UPDATE todos SET completed = ?1 WHERE id = ?2",
        params![form.completed, form.id],).unwrap();
    Redirect::to("/")
}

async fn delete(form: web::Form<Todo>) -> Redirect
{
    let conn = rusqlite::Connection::open("todo.db").unwrap();
    conn. execute (
        "DELETE FROM todos WHERE id = ?1",
        params![form.id],).unwrap();
    Redirect::to("/")
}


index.html

プロジェクトのルートディレクトリに templates というディレクトリを作り、 その中に次の index.html を作ります。


<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Todo</title>
</head>

<body>
    <h1>Todo</h1>
    <form method="post" action="/create">
        <input type="text" name="title" required maxlength="30">
        <input type="hidden" name="completed" value="false">
        <input type="hidden" name="id" value="0">
        <button>Create</button>
    </form>
    <ul>
        {% for todo in todos %}
        <li style="display: flex;">
            <form method="post" action="/update" id=f{{todo.id}}>
                <input type="checkbox" {% if todo.completed %}checked{% endif %} onchange="cbchange('f{{todo.id}}')">
                <input type="hidden" name="id" value={{todo.id}}>
                <input type="hidden" name="title" value={{todo.title}}>
                {% if todo.completed %}
                <input type="hidden" name="completed" value="false">
                {% else %}
                <input type="hidden" name="completed" value="true">
                {% endif %}
                <button hidden>submit</button>
            </form>
            {% if todo.completed %}
            &nbsp;&nbsp;<del>{{ todo.title }}</del>
            {% else %}
            &nbsp;&nbsp;{{ todo.title }}
            {% endif %}
            <form method="post" action="/delete">
                <input type="hidden" name="id" value={{todo.id}}>
                <input type="hidden" name="title" value={{todo.title}}>
                <input type="hidden" name="completed" value={{todo.completed}}>
                &nbsp;&nbsp;<button>x</button>
            </form>
        </li>
        {% else %}
        <li>No todo</li>
        {% endfor %}
    </ul>
    <script>
        function cbchange(f) {
            const form = document.getElementById(f);
            form.submit();
        }
    </script>
</body>

</html>

ターミナルでプロジェクトのルートディレクトリに移動して、cargo run とコマンドしてサーバーを起動してください。 そしてお使いのブラウザで localhost:8080 もしくは 127.0.0.1:8080 を開いてください。



1043 visits
Posted: Sep. 23, 2025
Update: Sep. 29, 2025

ホーム   目次