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.sql
とdown.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という仕組みを使って画面を作っていきましょう!