🚀

Rust Rocket,Dieselを使用した簡単なGET,POST

2022/01/07に公開

前提

今回は自分の作成していたクリーンアーキテクチャで長くつまずいてしまったので、早く形にできるように勉強用の素材として新たに作成したものになります。
使用したwebフレームワークが「Rocket」、ormが「Diesel」を使用しました。
そして今回行ったのはサーバーを立ち上げてGET,POSTができるレベルになります。ドキュメント見つつなので作成期間は10時間ほどだったと思います。

Rocket,Dieselの導入

Cargo.toml

作成したプロジェクトのCargo.tomlの[dependencies]に以下の記述をします。
その後$ cargo buildコマンドでインストールしてくれます。
ややこしいと思いますがdiesel = { version = "1.4.8", features = ["mysql", "r2d2"]}部分ではバージョンとfeatures = []で使用許可を出します。

  • 今回はmysqlを使用しましたが、使用するDBの指定 -> Dieselでは現状"postgres","mysql","sqlite"の三種のみで扱えるようです。
  • diesel内からr2d2というクレートの使用許可 -> これがないと後に記述するDBコネクションの部分でエラーが出ました。
Cargo.toml
rocket = "0.4.10"
rocket_codegen = "0.4.10"
rocket_contrib = "0.4.10"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"

diesel = { version = "1.4.8", features = ["mysql", "r2d2"]}

/src/bin/main.rs

まずはmain.rsのコードになります。

main.rs
#![feature(plugin)]
#![feature(proc_macro_hygiene, decl_macro)]

extern crate rocket;
#[macro_use] extern crate rocket_codegen;
#[macro_use] extern crate rocket_contrib;
extern crate serde_derive;

use rocket_contrib::json::{Json, JsonValue};

use rust_rocket::db;
use rust_rocket::user::{User, NewUser};


#[get("/")]
fn index(connection: db::Connection) -> Json<JsonValue> {
    Json(json!(User::index(&connection)))
}

#[get("/hello")]
fn hello() -> String {
    "hello, world!!".to_string()
}

#[get("/hello/<name>")]
fn name(name: String) -> String {
   format!("Hello, {}", name)
}

#[post("/todo", format = "json", data = "<user>")]
fn new_user(user: Json<NewUser>) -> String {
    let insert_user = user.into_inner();
    format!("{:?}", insert_user)
}


#[post("/users", format = "json", data = "<user>")]
fn post(user: Json<NewUser>, connection: db::Connection) -> Json<JsonValue> {
    let insert_user = user.into_inner();
    Json(json!(User::create(insert_user, &connection)))
}

fn main() {
    rocket::ignite()
        .manage(db::connect())
        .mount("/api", 
        routes![
                index,
                hello,
                name,
                new_user,
                post
                ])
        .launch();
}

main関数

基本的にはRocketに管理させるようにするので、まずmain関数では、

  • ignite() -> Rocket.tomlの情報をもとにアプリケーションを立ち上げます。今回は使用していないので、Rocketで用意されているデフォルトが使用されます。
  • manage() -> 後に記述しますがここでDBとの接続、コネクションの状態を管理します。
  • mount() -> コードにある"/api"がベースになります。http://localhost:8000/api + 「任意のルート」となります。route![]で呼び出すハンドラを指定します。
  • launch() -> アプリケーションサーバーを起動し、マウントされたルートとキャッチャーへのリクエストのリッスンとディスパッチを開始します。

ルーティング設定

main.rs
#[get("/")]
fn index(connection: db::Connection) -> Json<JsonValue> {
    Json(json!(User::index(&connection)))
}

#[get("/hello")]
fn hello() -> String {
    "hello, world!!".to_string()
}

#[get("/hello/<name>")]
fn name(name: String) -> String {
   format!("Hello, {}", name)
}

#[post("/users", format = "json", data = "<user>")]
fn post(user: Json<NewUser>, connection: db::Connection) -> Json<JsonValue> {
    let insert_user = user.into_inner();
    Json(json!(User::create(insert_user, &connection)))
}

ルーティングの設定は、#[method("url")]で行います。method部分にGETさせたければ「get」をPOSTさせたければ「post」を記述します。
url部分は

  • hello()を説明するとhttp://localhost:8000/api/helloにアクセスすると"「hello, world!!」とStringを返す"という処理になるのですが、/apiのあとの/helloのように設定することができます。

  • name()ではhttp://localhost:8000/api/hello/test_userとアクセスすると、"Hello, test_user"と返ってきます。これはidなどパラメーターを受け取る際などのように指定した値を引数としても扱えます。記述は/<xxxx>とするようです。

  • post()に関しては自分が少しつまずいたので
    #[post("/users", format = "json", data = "<user>")]と記述しているのは、
    url部分は除くと、format = "json"はリクエストのbodyをjson指定で値を受け取る状態になります。まだ試していませんがpostform等でも受け取れるはずです。data = "<user>"は送られてきたdataが引数のuserだと関連付けさせます。

/src/db.rs

次にdb.rs

db.rs
use diesel::{r2d2::{Pool, ConnectionManager, PooledConnection}, mysql::MysqlConnection};

use std::ops::Deref;
use rocket::http::Status;
use rocket::request::{self, FromRequest};
use rocket::{Request, State, Outcome};

pub type MysqlPool = Pool<ConnectionManager<MysqlConnection>>;

static DATABASE_URL: &str = "mysql://[user]:[password]@localhost/[db name]";


pub fn connect() -> MysqlPool {
    let manager = ConnectionManager::<MysqlConnection>::new(DATABASE_URL);
    Pool::new(manager).expect("Failed to create pool")
}


pub struct Connection(pub PooledConnection<ConnectionManager<MysqlConnection>>);

impl<'a, 'r> FromRequest<'a, 'r> for Connection {
    type Error = ();

    fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
        let pool = request.guard::<State<MysqlPool>>()?;
        match pool.get() {
            Ok(conn) => Outcome::Success(Connection(conn)),
            Err(_) => Outcome::Failure((Status::ServiceUnavailable, ()))
        }
    }
}

impl Deref for Connection {
    type Target = MysqlConnection;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

まず、pub type MysqlPool = Pool<ConnectionManager<MysqlConnection>>;部分で、Dieselで使用するためのmysqlの接続、汎用接続プールの型を指定します。
ドキュメントでは.envを使用して、DBのURLを選択できるようにしていますが、今回は使わず、直に変数に入力しています。

pub fn connect() -> MysqlPool {
    let manager = ConnectionManager::<MysqlConnection>::new(DATABASE_URL);
    Pool::new(manager).expect("Failed to create pool")
}

この部分は、main()内のmanege()部分で呼ばれた箇所になります。
ここでDB接続に接続しその状態をRocketに返しています。

impl<'a, 'r> FromRequest<'a, 'r> for Connection {
    type Error = ();

    fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
        let pool = request.guard::<State<MysqlPool>>()?;
        match pool.get() {
            Ok(conn) => Outcome::Success(Connection(conn)),
            Err(_) => Outcome::Failure((Status::ServiceUnavailable, ()))
        }
    }
}

impl Deref for Connection {
    type Target = MysqlConnection;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

ここの部分ではfn post(user: Json<NewUser>, connection: db::Connection)
の部分の引数connectionに関しての記述になります。
【FromRequestトレイトについてのドキュメント抜粋】
リクエストから値を取得するためにリクエストガードによって実装されるトレイト。
と記述されていますが、これは引数として扱えるように確認するトレイトではないかと思います。
引数userは実際に受け取る値でマクロ内で記述していますがconnectionに関してはこちらで用意しているものなので、この実装がないと引数として扱ってもらえずエラーを吐いてしまいす。

/src/user.rs

そしてuser.rsへの記述になります

user.rs
use diesel;
use diesel::prelude::*;
use diesel::MysqlConnection;
use serde_derive::Deserialize;
use serde_derive::Serialize;

use crate::schema::users;
use crate::schema::users as users_schema;

#[derive(Debug, Queryable, Serialize, Deserialize)]
pub struct User {
	pub id: u64,
	pub display_name: String,
}

#[derive(Debug, Insertable, Deserialize)]
#[table_name = "users"]
pub struct NewUser {
	pub display_name: String
}


impl User {
	pub fn index(connection: &MysqlConnection) -> Vec<User> {
		users_schema::dsl::users
		.load::<User>(connection)
		.expect("Error loading users")
	}

	pub fn create(insert_user: NewUser, connection: &MysqlConnection) -> Vec<User> {
		diesel::insert_into(users_schema::dsl::users)
			.values(&insert_user)
			.execute(connection)
			.expect("Error inserting user");

			users_schema::dsl::users
			.load::<User>(connection)
			.expect("Error loading users")
	}
}

主に構造体の宣言をしています。なぜUserとNewUserの二つあるのかですが、Userは取得用でNewUserは作成用です。分けている理由ですが、NewUser構造体はDBへのInsert用で、idはプライマリキーとしているので管理は毎回データが送られてくるものに指定するわけにはいきません。かと言って、idがフィールドにあると、値が入っていないとビルド出来ません。User構造体は取得、扱う際に必要なので分けています。

impl User {
	pub fn index(connection: &MysqlConnection) -> Vec<User> {
		users_schema::dsl::users
		.load::<User>(connection)
		.expect("Error loading users")
	}

	pub fn create(insert_user: NewUser, connection: &MysqlConnection) -> Vec<User> {
		diesel::insert_into(users_schema::dsl::users)
			.values(&insert_user)
			.execute(connection)
			.expect("Error inserting user");

			users_schema::dsl::users
			.load::<User>(connection)
			.expect("Error loading users")
	}
}

それではメソッドの部分へといきましょう。
上記二つはmain.rsの箇所でもお話した、ルーティングで呼ばれた後の処理になります。

  • index()
    -> こちらはusersテーブルから入っている値を全件取得しています。users_schema::dsl::usersの箇所で取得するテーブルの指定をしています。users_schemaに関する記述はdieselの設定時になるのでこの後まとめています。
    load<User>(connection)でconnectionを引数にしてdbに接続->loadで全件取得->データを<User>構造体に入れています。
  • create()
    -> こちらはデータの作成して、その後作成されてあるデータを取得しています。diesel::insert_intoで作成の指示->引数にテーブルの指定->valueで値のinsert_userの参照を行なっています。

Dieselについて

今回採用したDieselは他と比べてstarsの数が多かったのが理由になります。
そしてマイグレーション機能もあります。自分でsqlファイルを書かないといけませんが、、、

Diesel CLIのインストール

RustでDB管理をサポートするDiesel CLIをインストールします。
今回はMySQLとの依存関係のみインストールするので、
–no-default-features –features mysql
のオプションを追加しています。

$ cargo install diesel_cli --no-default-features --features mysql

マイグレーション

各用意されたコマンドを打ってテーブルの作成を行っていきます。
.envを用意している場合はドキュメント通りでいけると思います。自分はあえて作らないようにしたのでdatabase_url=XXXXXXを引数として扱いました。

$ diesel setup --database_url=XXXXXXX

./migrationsフォルダが作成されます。
その後migration内のsqlファイルに記述します。

up.sql
CREATE TABLE users
(
	id SERIAL PRIMARY KEY,
	display_name varchar(255) NOT NULL
);
down.sql
DROP TABLE users;

そしてマイグレーションを実行します。

$ diesel migration run --database_url=XXXXXXX

そうすると下記にあるファイルが作成されます

schema.rsについて

まとめると言っていたschema.rsですが、上記のコマンドを打つとschema.rsがdiesel.tomlに記述した箇所に自動で作成されます。

schema.rs
table! {
    users (id) {
        id -> Unsigned<Bigint>,
        display_name -> Varchar,
    }
}
diesel.toml
[print_schema]
file = "src/schema.rs"

がそれぞれの内容です。

まとめ

以上が作成した簡単なGET,POSTを行うものを作成しました。githubに載っているExampleではエラーなども出るようなので、ドキュメント、github、issue、他の方のコード、Qiita、stack overflowなど見るものも多くあり予想よりも多く時間がかかってしまいましたね。
標準ライブラリ、フレームワークともに開発段階で今後整備されていくんだと思います。

Discussion