こんにちは!大垣です!
前回に引き続き、RustでCRUD処理を作って行きたいと思います。
今回はバックエンド(WebAPI)の基礎部分を作っていきます。
構成
・WebAPIエンジン :actix-web
・DBドライバ :sqlx
・WebAPI :REST API
プロジェクト作成
前回作成した、rustsourceフォルダ内にbackフォルダを作成し、
この中にWebAPIのプログラムを作って行きます。
コマンドプロンプトを開き、以下のコマンドを実行します。
docker ps
※rust-rust-appとなっている左のIDを確認する
docker exec -it ↑で確認したID bash
cargo new back
クレートの追加
プロジェクトに必要なクレート(ライブラリのようなもの)を追加します。
先ほどのコマンドプロンプトの続きで、以下のコマンドを実行します。
cd back
cargo add actix-cors actix-web dotenv json jsonwebtoken
cargo add sqlx --features "runtime-actix-native-tls, postgres"
ファイル構成
今回は、以下のファイル構成にします。
cargo.toml以下のファイルは、自動生成されますので編集は不要です。
📁back
├📁src
│├📁controllers
││├📝mod.rs・・・・・・⑩
││└📝taskcontroller.rs・・⑨
│├📁entities
││├📝mod.rs・・・・・・・④
││└📝ttask.rs・・・・・・・③
│├📁repositories
││├📝mod.rs・・・・・・・⑥
││└📝taskrepository.rs・・⑤
│├📁services
││├📝mod.rs・・・・・・⑧
││└📝taskservice.rs・・・⑦
│└📝main.rs・・・・・・・②
├📝.env・・・・・・・・・①
├📝cargo.toml
├📝cargo.lock
└📝.gitignore
①.envファイル
.envファイルは、設定情報を記述するファイルです。
.envファイルに、以下の内容を記述し、保存します。
HTTP_PORT=9000
HTTP_URL=0.0.0.0
DB_HOST=rust-postgres-1
DB_PORT=5432
DB_USER=user
DB_PASS=password
DB_TARGET=rusttest
②src/main.rsファイル
拡張子が.rsのファイルは、Rustのプログラムファイルです。
main.rsには、初期化処理とアプリケーションの起動処理を記述します。
main.rsに、以下の内容を記述し、保存します。
// 各要素読み込み
pub mod controllers; // コントローラー
pub mod services; // サービス
pub mod repositories; // リポジトリ
pub mod entities; // エンティティ
// クレート宣言
use dotenv::dotenv; // .envファイル読み込み用クレート
use std::env; // 環境変数
use actix_web::{App, HttpServer, http::header}; // actix_webの各クレート
use actix_cors::Cors; // actixのクロスオリジン制御クレート
/// メイン処理
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// .envファイル読み込み
dotenv().ok();
// 変数宣言
let l_str_url: String = env::var("HTTP_URL").unwrap();
let l_str_port: String = env::var("HTTP_PORT").unwrap();
// Httpサーバ起動
HttpServer::new(|| {
// クロスオリジン設定
// フロントエンド側からWebAPIアクセスを可能にするため
// localhost:9001を許可しています
let cors = Cors::default()
.allowed_origin("http://localhost:9001")
.allowed_methods(vec!["GET", "POST", "PUT", "DELETE"])
.allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT])
.allowed_header(header::CONTENT_TYPE)
.supports_credentials()
.max_age(3600);
// WebAPI起動設定
// クロスオリジンの設定と、各コントローラーの読み込みを
// 行っています。
App::new()
.wrap(cors)
.service(controllers::taskcontroller::ctl_get_task)
.service(controllers::taskcontroller::ctl_put_task)
.service(controllers::taskcontroller::ctl_post_task)
.service(controllers::taskcontroller::ctl_delete_task)
})
.bind((&l_str_url as &str, l_str_port.parse().unwrap()))? // URL、ポート指定
.run() // Webサービス起動
.await
}
③src/entities/ttask.rsファイル
ttask.rsファイルには、タスクのエンティティ(データベースに対応するデータ)を記述します。
エンティティの各メンバがデータベーステーブルの各カラムに対応します。
ttask.rsファイルに、以下の内容を記述し、保存します。
// クレート宣言
use sqlx::FromRow;
/// T_TASKテーブルのエンティティ
#[derive(FromRow)]
pub struct TTask {
pub taskid: String, // タスクID
pub userid: String, // 登録ユーザID
pub title: String, // タスクタイトル
pub description: String, // タスク詳細
pub adddate: String, // 登録日
pub moddate: String // 更新日
}
④src/entities/mod.rsファイル
entitiesフォルダ内のmod.rsには、同フォルダ内のプログラムファイルを
読み込む処理を記述します。
enititesフォルダ内のmod.rsに、以下の内容を記述し、保存します。
// 各ファイル読み込み
pub mod ttask; // ttask.rs
⑤src/repositories/taskrepository.rsファイル
taskrepository.rsファイルには、タスクテーブルへのアクセスを行う処理を記述します。
taskrepository.rsファイルに、以下の内容を記述し、保存します。
// クレート宣言
use sqlx::postgres::{PgRow};
use sqlx::{Row, query, Error, QueryBuilder, Postgres, Execute};
use crate::repositories::connect;
use crate::entities::ttask::TTask;
// 全タスク取得
pub async fn db_get_all_task() -> Result<Vec<TTask>, Error> {
// 接続
let pool = connect().await.unwrap();
// SQL生成
let mut query_builder: QueryBuilder<Postgres> = QueryBuilder::new(
"
select
taskid,
userid,
title,
description,
to_char(adddate, 'YYYY/MM/DD hh:mm:ss') as adddate,
to_char(moddate, 'YYYY/MM/DD hh:mm:ss') as moddate
from
t_task
"
);
let query_string = query_builder.build();
let sql = query_string.sql();
// 結果を取得
let _data: Vec<TTask> = query(&sql)
.map(|row: PgRow| TTask {
taskid: row.get("taskid"),
userid: row.get("userid"),
title: row.get("title"),
description: row.get("description"),
adddate: row.get("adddate"),
moddate: row.get("moddate")
})
.fetch_all(&pool)
.await?;
// 結果を返す
Ok(_data)
}
// タスク取得
pub async fn db_get_task(task_id: &str) -> Result<Vec<TTask>, Error> {
// 接続
let pool = connect().await.unwrap();
// SQL生成
let mut query_builder: QueryBuilder<Postgres> = QueryBuilder::new(
"
select
taskid,
userid,
title,
description,
to_char(adddate, 'YYYY/MM/DD hh:mm:ss') as adddate,
to_char(moddate, 'YYYY/MM/DD hh:mm:ss') as moddate
from
t_task
where
t_task.taskid = $1
"
);
let query_string = query_builder.build();
let sql = query_string.sql();
let task_data: Vec<TTask> = query(&sql)
.bind(&task_id)
.map(|row: PgRow| TTask {
taskid: row.get("taskid"),
userid: row.get("userid"),
title: row.get("title"),
description: row.get("description"),
adddate: row.get("adddate"),
moddate: row.get("moddate")
})
.fetch_all(&pool)
.await?;
Ok(task_data)
}
// タスク追加
pub async fn db_add_task(task_id: &str, user_id: &str, title: &str, description: &str) -> Result<(), Error> {
// 接続
let pool = connect().await.unwrap();
// SQL生成
let mut query_builder: QueryBuilder<Postgres> = QueryBuilder:: new(
"insert into t_task values("
);
let mut separated = query_builder.separated(", ");
separated.push_bind(&task_id);
separated.push_bind(&user_id);
separated.push_bind(&title);
separated.push_bind(&description);
separated.push_unseparated(", now()");
separated.push_unseparated(", now()");
separated.push_unseparated(") ");
let query_string = query_builder.build();
let sql = query_string.sql();
// SQL実行
query(&sql)
.bind(&task_id)
.bind(&user_id)
.bind(&title)
.bind(&description)
.fetch_all(&pool)
.await?;
Ok(())
}
/// タスク変更
pub async fn db_mod_task(task_id: &str, title: &str, description: &str) -> Result<(), Error>{
// 接続
let pool = connect().await.unwrap();
// SQL生成
let mut query_builder: QueryBuilder<Postgres> = QueryBuilder::new(
"
update t_task set
title = $1
,description = $2
,moddate = now()
where
taskid = $3
"
);
let query_string = query_builder.build();
let sql = query_string.sql();
// SQL実行
query(&sql)
.bind(&title)
.bind(&description)
.bind(&task_id)
.fetch_all(&pool)
.await?;
Ok(())
}
// タスク削除
pub async fn db_del_task(task_id: &str) -> Result<(), Error> {
// 接続
let pool = connect().await.unwrap();
// SQL生成
let mut query_builder: QueryBuilder<Postgres> = QueryBuilder::new(
"
delete from t_task
where
taskid = $1
"
);
let query_string = query_builder.build();
let sql = query_string.sql();
// SQL実行
query(&sql)
.bind(&task_id)
.fetch_all(&pool)
.await?;
Ok(())
}
⑥src/repositories/mod.rsファイル
repositoriesフォルダ内のmod.rsファイルには、同フォルダ内のプログラムファイルを
読み込む処理と、共通処理であるデータベース接続関数を記述します。
repositoriesフォルダ内のmod.rsファイルに、以下の内容を記述し、保存します。
// ファイル読み込み
pub mod taskrepository;
// クレート宣言
use sqlx::Error;
use std::env;
use sqlx::postgres::{PgPoolOptions, PgPool};
/// DB接続
pub async fn connect() -> Result<PgPool, Error> {
// DBパラメータセット
let db_host = env::var("DB_HOST").unwrap();
let db_port = env::var("DB_PORT").unwrap();
let db_user = env::var("DB_USER").unwrap();
let db_pass = env::var("DB_PASS").unwrap();
let db_target = env::var("DB_TARGET").unwrap();
let db_con_str = "postgres://".to_owned() + &db_user + ":" + &db_pass + "@" + &db_host + ":" + &db_port + "/" + &db_target;
// DB接続
let pool = PgPoolOptions::new()
.max_connections(5)
.connect(&db_con_str).await?;
Ok(pool)
}
⑦src/services/taskservice.rsファイル
taskservice.rsファイルには、タスクに関するテーブルアクセス以外の処理を記述します。
taskservice.rsファイルに、以下の内容を記述し、保存します。
// クレート宣言
use crate::repositories::taskrepository::*;
use sqlx::Error;
/// タスク取得
pub async fn srv_get_task(query: &str) -> Result<String, Error> {
// パラメータからJSON文字列取得
let param = format!("{}{}{}", r#"{ ""#, query.replace("=", r#"": ""#).replace("&", r#"", ""#), r#"" }"#);
// JSONにパース
let param_json = json::parse(¶m);
let param_json = match param_json {
Ok(data) => data,
Err(_error) => json::parse("{}").unwrap()
};
// タスクデータ
let task_datas;
// パラメータがあるかどうか
if param_json["taskid"].is_null() {
// なければ全タスク取得
task_datas = db_get_all_task().await.unwrap();
}else{
// あれば指定タスク取得
task_datas = db_get_task(param_json["taskid"].as_str().unwrap()).await.unwrap();
}
// レスポンス用JSON生成
let mut ret_json_str = String::from("[");
let task_iter = task_datas.iter();
for task_data in task_iter {
ret_json_str = format!("{}{}{}{}", ret_json_str, r#"{ "taskid": ""#, task_data.taskid, r#"", "#);
ret_json_str = format!("{}{}{}{}", ret_json_str, r#""userid": ""#, task_data.userid, r#"", "#);
ret_json_str = format!("{}{}{}{}", ret_json_str, r#""title": ""#, task_data.title, r#"", "#);
ret_json_str = format!("{}{}{}{}", ret_json_str, r#""description": ""#, task_data.description, r#"", "#);
ret_json_str = format!("{}{}{}{}", ret_json_str, r#""adddate": ""#, task_data.adddate, r#"", "#);
ret_json_str = format!("{}{}{}{}", ret_json_str, r#""moddate": ""#, task_data.moddate, r#"" },"#);
}
// 末尾の文字取得
let last_char = ret_json_str.chars().last().unwrap();
// データがない場合
if last_char != ",".chars().last().unwrap() {
ret_json_str = String::from("[]");
}else{
ret_json_str.pop();
ret_json_str = format!("{}{}", ret_json_str, "]");
}
// 結果を返す
Ok(ret_json_str)
}
/// タスク登録
pub async fn srv_add_task(request: String) -> Result<String, Error>{
// パラメータからJSON文字列取得
let param = format!("{}{}{}", r#"{ ""#, request.replace("=", r#"": ""#).replace("&", r#"", ""#), r#"" }"#);
// JSONにパース
let param_json = json::parse(¶m);
let param_json = match param_json {
Ok(data) => data,
Err(_error) => json::parse("{}").unwrap()
};
// Validationチェック
let mut valid_value = true;
if param_json["taskid"].is_null() {valid_value = false;}
if param_json["userid"].is_null() {valid_value = false;}
if param_json["title"].is_null() {valid_value = false;}
if param_json["description"].is_null() {valid_value = false;}
// チェック結果
if valid_value {
// データを登録
let result = db_add_task(
param_json["taskid"].as_str().unwrap(),
param_json["userid"].as_str().unwrap(),
param_json["title"].as_str().unwrap(),
param_json["description"].as_str().unwrap()).await;
// エラーチェック
let result = match result {
Ok(()) => String::from("OK"),
Err(error) => error.to_string()
};
// 結果を返す
return Ok(result);
}
// 異常を返す
return Ok(String::from("Validation Error"));
}
/// タスク変更
pub async fn srv_mod_task(request: String) -> Result<String, Error>{
// パラメータからJSON文字列取得
let param = format!("{}{}{}", r#"{ ""#, request.replace("=", r#"": ""#).replace("&", r#"", ""#), r#"" }"#);
// JSONにパース
let param_json = json::parse(¶m);
let param_json = match param_json {
Ok(data) => data,
Err(_error) => json::parse("{}").unwrap()
};
// Validationチェック
let mut valid_value = true;
if param_json["taskid"].is_null() {valid_value = false;}
if param_json["title"].is_null() {valid_value = false;}
if param_json["description"].is_null() {valid_value = false;}
// チェック結果
if valid_value {
// データを登録
let result = db_mod_task(
param_json["taskid"].as_str().unwrap(),
param_json["title"].as_str().unwrap(),
param_json["description"].as_str().unwrap()).await;
// エラーチェック
let result = match result {
Ok(()) => String::from("OK"),
Err(error) => error.to_string()
};
// 結果を返す
return Ok(result);
}
// 異常を返す
return Ok(String::from("Validation Error"));
}
/// タスク削除
pub async fn srv_del_task(request: String) -> Result<String, Error>{
// パラメータからJSON文字列取得
let param = format!("{}{}{}", r#"{ ""#, request.replace("=", r#"": ""#).replace("&", r#"", ""#), r#"" }"#);
// JSONにパース
let param_json = json::parse(¶m);
let param_json = match param_json {
Ok(data) => data,
Err(_error) => json::parse("{}").unwrap()
};
// Validationチェック
let mut valid_value = true;
if param_json["taskid"].is_null() {valid_value = false;}
// チェック結果
if valid_value {
// データを登録
let result = db_del_task(
param_json["taskid"].as_str().unwrap()).await;
// エラーチェック
let result = match result {
Ok(()) => String::from("OK"),
Err(error) => error.to_string()
};
// 結果を返す
return Ok(result);
}
// 異常を返す
return Ok(String::from("Validation Error"));
}
⑧src/services/mod.rsファイル
serviceフォルダ内のmod.rsファイルには、同フォルダ内のプログラムファイルを
読み込む処理を記述します。
serviceフォルダ内のmod.rsファイルに、以下の内容を記述し、保存します。
// ファイル読み込み
pub mod taskservice;
⑨src/controllers/taskcontroller.rsファイル
taskcontroller.rsファイルには、「/task」のURLにアクセスされた際に
どのようなアクションをするかについて記述します。
今回はREST APIという形式でWebAPIを作成します。
REST APIは、アクセス先が同じURLでも、HTTPメソッドの種類によって
行うアクションを変えるWebAPIです。
REST APIは、各HTTPメソッドがCRUDの各処理に対応しており、
GETが読み出し、POSTが更新、PUTが登録、DELETEが削除に対応します。
taskcontroller.rsファイルに、以下の内容を記述し、保存します。
// クレート宣言
use actix_web::{get, post, put, delete, HttpRequest, HttpResponse, Responder};
use crate::services::taskservice::*;
/// URL(/task)へのGetアクセス
#[get("/task")]
pub async fn ctl_get_task(request: HttpRequest) -> impl Responder {
// タスクデータ取得
let res_data = srv_get_task(request.query_string()).await.unwrap();
// 結果を返す
HttpResponse::Ok()
.content_type("application/json")
.body(res_data)
}
/// URL(/task)へのPostアクセス
#[post("/task")]
pub async fn ctl_post_task(req_body: String) -> impl Responder {
// タスクデータ変更
let res_data = srv_mod_task(req_body).await.unwrap();
// 結果を返す
HttpResponse::Ok()
.body(res_data)
}
/// URL(/task)へのPutアクセス
#[put("/task")]
pub async fn ctl_put_task(req_body: String) -> impl Responder {
// タスクデータ登録
let res_data = srv_add_task(req_body).await.unwrap();
// 結果を返す
HttpResponse::Ok()
.body(res_data)
}
/// URL(/task)へのDeleteアクセス
#[delete("/task")]
pub async fn ctl_delete_task(req_body: String) -> impl Responder {
// タスクデータ削除
let res_data = srv_del_task(req_body).await.unwrap();
// 結果を返す
HttpResponse::Ok()
.body(res_data)
}
⑩src/controllers/mod.rsファイル
controllersフォルダ内のmod.rsファイルには、同フォルダ内のプログラムファイルを
読み込む処理を記述します。
controllersフォルダ内のmod.rsファイルに、以下の内容を記述し、保存します。
// ファイル読み込み
pub mod taskcontroller;
動作確認
ここまで作成できたら、コマンドプロンプトを開き、以下のコマンドを実行します。
コマンドを実行することで、作成したWebAPIが起動します。
docker ps
※rust-rust-appとなっている行のCONTAINER IDを確認する
docker exec -it ↑で確認したID bash
cd back
cargo run
初回起動時は、各クレートがダウンロードされるため、少し時間がかかります。
「Running /tmp/target/debug/back
」と表示されると、起動は成功です。
途中でエラーが出た場合は、プログラムに入力間違い等がないか確認しましょう。
起動完了後、動作確認のため、もう一つコマンドプロンプトを起動し
以下のコマンドを実行します。
docker ps
※rust-rust-appとなっている行のCONTAINER IDを確認する
docker exec -it ↑で確認したID bash
curl http://localhost:9000/task -X PUT -d 'taskid=0000001&userid=A000000&title=test&description=tasktest'
上記は、PUTメソッドで/taskにアクセスすることで、データの登録を行うコマンドです。
上記実行後、Webブラウザを起動し、以下のURLにアクセスします。
http://localhost:9000/task
前述のcurlコマンドでは、引数で明示的に指定している為、PUTメソッドでのアクセスになりますが、
Webブラウザから直接URLを入力してアクセスした場合は、
GETメソッドでのアクセスとなり、データの読み出しが行われます。
以下のような内容が画面に表示されれば、登録ができています。
adddate、moddateは登録を行った日時が表示されます。
[{ "taskid": "0000001", "userid": "A000000", "title": "test", "description": "tasktest", "adddate": "2023/02/21 11:02:36", "moddate": "2023/02/21 11:02:36" }]
引き続き、コマンドプロンプトで以下のコマンドを実行します。
curl http://localhost:9000/task -X POST -d 'taskid=0000001&title=test2test2&description=tasktasktask'
上記は、POSTメソッドで/taskにアクセスすることで、データの更新を行うコマンドです。
上記実行後、Webブラウザを起動し、以下のURLにアクセスします。
http://localhost:9000/task
以下のような内容が画面に表示されれば、更新ができています。(赤字が更新箇所)
moddateは更新を行った日時が表示されます。
[{ "taskid": "0000001", "userid": "A000000", "title": "test2test2", "description": "tasktasktask", "adddate": "2023/02/21 11:02:36", "moddate": "2023/02/21 11:02:57" }
引き続き、コマンドプロンプトで以下のコマンドを実行します。
curl http://localhost:9000/task -X DELETE -d 'taskid=0000001'
上記は、DELETEメソッドで/taskにアクセスすることで、データの削除を行うコマンドです。
上記実行後、Webブラウザを起動し、以下のURLにアクセスします。
http://localhost:9000/task
以下のような内容が画面に表示されれば、削除ができています。
もともと登録されていたデータが削除され、1件も登録がない状態です。
[]
まとめ
以上で、基本的なデータ登録、更新、削除、読み出しを行うWebAPIができました。
次回は、このWebAPIを呼び出すフロントエンド(UI)を作成してみたいと思います。
次回に続く。
コメント