Skip to content

Rustでデータベースにアクセスする#

データベースとは、アプリケーションの情報を保存・取得する場所です。 RustではSQLxやDieselなどのORMライブラリを使用してデータベースにアクセスできます。

一般的にモデルとは、システム上の登場人物(登場するモノ)を指します。

Rustでも、モデルは登場人物単位で作ると良いです。 例えば予約システムだと、モデルには「お客様」と、「予約」という登場人物があり、 「お客様」には、氏名、電話番号、アカウント名があり、 「予約」には、日時、注文内容がある、 ような感じです。

ここでは、「お客様」情報としてUserモデルを作ってみます。SQLiteデータベースを使い、Dieselというライブラリを使用します。

前提#

もし、Rustプロジェクトが手元になければ、こちらの手順でRustプロジェクトを用意してください。

Dieselのセットアップ#

まず、Dieselとそれに必要なライブラリをインストールします。Cargo.tomlファイルに以下の依存関係を追加してください:

[dependencies]
actix-web = "4.3.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
diesel = { version = "2.1.0", features = ["sqlite", "r2d2", "chrono"] }
dotenvy = "0.15"
chrono = { version = "0.4", features = ["serde"] }
r2d2 = "0.8"

次に、Dieselのコマンドラインツールをインストールします:

cargo install diesel_cli --no-default-features --features sqlite

プロジェクトのルートディレクトリに.envファイルを作成し、データベースの接続情報を設定します:

DATABASE_URL=sqlite:db.sqlite3

Dieselをセットアップします:

diesel setup

Userモデルを作成する#

Userモデルの移行ファイルを作成します:

diesel migration generate create_users

これにより、migrationsディレクトリ内にup.sqldown.sqlの2つのファイルが作成されます。up.sqlファイルを開き、以下のSQLを追加します:

CREATE TABLE users (
  id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
  name TEXT NOT NULL,
  tel TEXT NOT NULL,
  account_name TEXT NOT NULL,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

down.sqlファイルには、テーブルを削除するSQLを追加します:

DROP TABLE users;

マイグレーションを実行します:

diesel migration run

これで、データベースにテーブルが作成されました。

スキーマとモデルの定義#

Dieselは、データベーススキーマをsrc/schema.rsに自動生成します。この自動生成されたファイルを確認してください。

次に、src/models.rsファイルを作成して、ユーザーモデルを定義します:

use crate::schema::users;
use chrono::NaiveDateTime;
use diesel::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Queryable, Selectable, Serialize)]
#[diesel(table_name = users)]
pub struct User {
    pub id: i32,
    pub name: String,
    pub tel: String,
    pub account_name: String,
    pub created_at: NaiveDateTime,
    pub updated_at: NaiveDateTime,
}

#[derive(Insertable, Deserialize)]
#[diesel(table_name = users)]
pub struct NewUser {
    pub name: String,
    pub tel: String,
    pub account_name: String,
}

データベース接続の設定#

src/db.rsファイルを作成して、データベース接続プールを設定します:

use diesel::prelude::*;
use diesel::r2d2::{self, ConnectionManager};
use dotenvy::dotenv;
use std::env;

pub type DbPool = r2d2::Pool<ConnectionManager<SqliteConnection>>;

pub fn establish_connection_pool() -> DbPool {
    dotenv().ok();

    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
    let manager = ConnectionManager::<SqliteConnection>::new(database_url);
    r2d2::Pool::builder()
        .build(manager)
        .expect("Failed to create pool.")
}

ユーザーCRUD操作の実装#

src/users.rsファイルを作成して、ユーザーのCRUD(作成、読込、更新、削除)操作を実装します:

use crate::models::{NewUser, User};
use crate::schema::users::dsl::*;
use crate::DbPool;
use diesel::prelude::*;
use diesel::result::Error;

// 新規ユーザー作成
pub fn create_user(pool: &DbPool, new_user: NewUser) -> Result<User, Error> {
    let mut conn = pool.get().expect("couldn't get db connection from pool");

    diesel::insert_into(users)
        .values(&new_user)
        .get_result::<User>(&mut conn)
}

// 全ユーザー取得
pub fn get_all_users(pool: &DbPool) -> Result<Vec<User>, Error> {
    let mut conn = pool.get().expect("couldn't get db connection from pool");

    users.load::<User>(&mut conn)
}

// 特定のユーザーを取得
pub fn get_user(pool: &DbPool, user_id: i32) -> Result<User, Error> {
    let mut conn = pool.get().expect("couldn't get db connection from pool");

    users.find(user_id).get_result::<User>(&mut conn)
}

// ユーザーを更新
pub fn update_user(pool: &DbPool, user_id: i32, user_data: NewUser) -> Result<User, Error> {
    let mut conn = pool.get().expect("couldn't get db connection from pool");

    diesel::update(users.find(user_id))
        .set((
            name.eq(user_data.name),
            tel.eq(user_data.tel),
            account_name.eq(user_data.account_name),
        ))
        .get_result::<User>(&mut conn)
}

// ユーザーを削除
pub fn delete_user(pool: &DbPool, user_id: i32) -> Result<usize, Error> {
    let mut conn = pool.get().expect("couldn't get db connection from pool");

    diesel::delete(users.find(user_id)).execute(&mut conn)
}

メインファイルの修正#

src/main.rsを編集して、モジュールをインポートし、APIエンドポイントを追加します:

mod db;
mod models;
mod schema;
mod users;

use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder};
use models::{NewUser, User};
use serde::{Deserialize, Serialize};

#[get("/api/users")]
async fn get_users(pool: web::Data<db::DbPool>) -> impl Responder {
    match users::get_all_users(&pool) {
        Ok(all_users) => HttpResponse::Ok().json(all_users),
        Err(_) => HttpResponse::InternalServerError().finish(),
    }
}

#[get("/api/users/{id}")]
async fn get_user(pool: web::Data<db::DbPool>, path: web::Path<(i32,)>) -> impl Responder {
    let user_id = path.into_inner().0;
    match users::get_user(&pool, user_id) {
        Ok(user) => HttpResponse::Ok().json(user),
        Err(diesel::NotFound) => {
            HttpResponse::NotFound().json("User not found")
        }
        Err(_) => HttpResponse::InternalServerError().finish(),
    }
}

#[derive(Deserialize)]
struct CreateUserQuery {
    name: String,
    tel: String,
    account_name: String,
}

#[get("/api/users/create")]
async fn create_user_get(
    pool: web::Data<db::DbPool>,
    query: web::Query<CreateUserQuery>,
) -> impl Responder {
    let new_user = NewUser {
        name: query.name.clone(),
        tel: query.tel.clone(),
        account_name: query.account_name.clone(),
    };

    match users::create_user(&pool, new_user) {
        Ok(user) => HttpResponse::Created().json(user),
        Err(_) => HttpResponse::InternalServerError().finish(),
    }
}

#[derive(Deserialize)]
struct UpdateUserQuery {
    name: Option<String>,
    tel: Option<String>,
    account_name: Option<String>,
}

#[get("/api/users/{id}/update")]
async fn update_user_get(
    pool: web::Data<db::DbPool>,
    path: web::Path<(i32,)>,
    query: web::Query<UpdateUserQuery>,
) -> impl Responder {
    let user_id = path.into_inner().0;

    match users::get_user(&pool, user_id) {
        Ok(existing_user) => {
            let updated_user = NewUser {
                name: query.name.clone().unwrap_or(existing_user.name),
                tel: query.tel.clone().unwrap_or(existing_user.tel),
                account_name: query.account_name.clone().unwrap_or(existing_user.account_name),
            };

            match users::update_user(&pool, user_id, updated_user) {
                Ok(user) => HttpResponse::Ok().json(user),
                Err(_) => HttpResponse::InternalServerError().finish(),
            }
        }
        Err(diesel::NotFound) => {
            HttpResponse::NotFound().json("User not found")
        }
        Err(_) => HttpResponse::InternalServerError().finish(),
    }
}

#[get("/api/users/{id}/delete")]
async fn delete_user(pool: web::Data<db::DbPool>, path: web::Path<(i32,)>) -> impl Responder {
    let user_id = path.into_inner().0;

    match users::delete_user(&pool, user_id) {
        Ok(_) => HttpResponse::NoContent().finish(),
        Err(_) => HttpResponse::InternalServerError().finish(),
    }
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // データベース接続プールを作成
    let pool = db::establish_connection_pool();

    println!("サーバーを起動します: http://localhost:8080");
    HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(pool.clone()))
            .service(get_users)
            .service(get_user)
            .service(create_user_get)
            .service(update_user_get)
            .service(delete_user)
            // 既存のルートもここに追加
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

APIの使い方#

サーバーを起動します:

cargo run

以下のURLでAPIを使用できます:

  • ユーザー作成: http://localhost:8080/api/users/create?name=比古清十郎&tel=000(0000)0000&account_name=hiko
  • ユーザー一覧取得: http://localhost:8080/api/users
  • 特定ユーザー取得: http://localhost:8080/api/users/1 (IDは作成されたユーザーのIDに置き換えてください)
  • ユーザー更新: http://localhost:8080/api/users/1/update?name=弥彦&tel=111(1111)1111 (IDは更新したいユーザーのIDに置き換えてください)
  • ユーザー削除: http://localhost:8080/api/users/1/delete (IDは削除したいユーザーのIDに置き換えてください)

これでRustでデータベースを扱うことができました。 次はReactという仕組みを使って画面を作っていきましょう!