Rustで簡単なCRUDをやってみる(WebAPI編1)

プログラミング
この記事は約34分で読めます。

こんにちは!大垣です!
前回に引き続き、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(&param);
    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(&param);
    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(&param);
    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(&param);
    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)を作成してみたいと思います。

次回に続く。

コメント

タイトルとURLをコピーしました