Rust + Rocket (0.5.0-rc.1) + rocket_sync_db_pools + DIESEL + MySQLでAPIサーバーを作る

タイトル通りの言語、ライブラリ、フレームワークAPIサーバーを作成する話です。

似たような記事はたくさんあるんですがrocket_sync_db_poolsを使ったやり方があんまりなくて困ったので書き残しておきます。

ちなみに基本的にはrocket_sync_db_poolsのexampleと(DIESEL)https://diesel.rs/のGetting Startedを進めて繋ぎ合わせただけです。

ただサンプルがPostgreSQLだったりmain.rsじゃなくてlib.rsだったり個人的には読み替えが大変でした。

作り方

local環境のversionは以下の通りです。

  • rustc: 1.60.0 (7737e0b5c 2022-04-04)
  • cargo: 1.60.0 (d1fd9fe2c 2022-03-01)

まずはRocketのリファレンスのGetting Started は済んでいる前提で進めます。

また、MySQLもどこかの環境に使えるものがある状態とします。

rocket_sync_db_poolsを追加

2022/05/05時点でRocketのlatest versionはv0.4.10 です。 また、v0.5.0-rc.1がPre-releaseされていて、Rocketのリファレンスでは初めて訪問するとv0.5のリファレンスが表示されます。

v0.4 まではrocket_contrib::databasesというlibraryがdatabase周りの設定に使われていたのですが、こちらがdeprecatedになります。

代わりにrocket_sync_db_poolsを使います。

Rocket/CHANGELOG.md at v0.5.0-rc.1 · SergioBenitez/Rocket · GitHub

なのでCargo.tomlrocket_sync_db_poolsを追加します。

rocket_sync_db_pools = { version = "0.1.0-rc.1",features = ["diesel_mysql_pool"] }

今回はfeatureにdiesel_mysql_poolを指定しましたがSQLitePostgreSQLを指定することも可能です。

その場合は以下のadapterのリストから適切なものを選択してください。

api.rocket.rs

DBのURLを設定

次にdbのurlをアプリケーションに設定します。

Rocket.tomlを作成し以下のように記載します。

[default.databases]
mysql = { url = "mysql://user1:root@127.0.0.1/testdb" }

urlの記法は

{dbの種類}://{dbに接続する際のusername}:{usernameに対応するpassword}@{dbのhost}/{利用するdatabaseの名前}

です。

  • mysqlを利用していて
  • アプリケーションがmysqlに接続するときのユーザー名が user1
  • user1のpasswordが root
  • dbのhostが 127.0.0.1
  • アプリケーションが利用するdatabaseの名前が testdb

の場合 mysql://user1:root@127.0.0.1/testdb になります。

main.rsを編集

src/main.rsを以下のように編集します。

#[macro_use] extern crate rocket;

use rocket_sync_db_pools::{database, diesel};

#[database("mysql")]
struct LogsDbConn(diesel::MysqlConnection);

#[launch]
fn rocket() -> _ {
    rocket::build()
        .mount("/", routes![posts])
        .attach(LogsDbConn::fairing())
}

databaseとdieselをscope内に入れてStructを作成、rocket::build()にattachしたらOKです。

この状態でcargo runを実行してサーバーが立ち上がればアプリケーションとMySQLの接続は完了です。

DIESELを追加

次にDIESELを設定していきます。

ちなみにDIESELはRust用のORMです。

Diesel is a Safe, Extensible ORM and Query Builder for Rust

Cargo.tomlに追加します。

[dependencies]
...
...
diesel = { version = "1.4.4", features = ["mysql"] }

mysqlを使うのでfeaturesはmysqlを指定します。

次にdiesel cliをinstallします。

cargo install diesel_cli

Rocket.tomlに書いたdbのURLを DATABASE_URL というkey名で環境変数に設定します。

export DATABASE_URL=mysql://user1:root@127.0.0.1/testdb

setupコマンドを実行します。

diesel setup

おそらくcargo runでサーバーが起動できている場合はsetupも問題なくいけます。

次にmigrationファイルを用意します。ここではGetting Started通りpostsテーブルを作ります。

diesel migration generate create_posts

migrations/{日付}_create_posts/ 以下にup.sqlとdown.sqlがそれぞれ作成されるので、up.sqlにcreate tableのSQLを、down.sqldrop tabelのSQLを書きます。

-- up.sql
CREATE TABLE posts (
  id integer AUTO_INCREMENT PRIMARY KEY,
  title varchar(255) NOT NULL,
  body text NOT NULL,
  published bool NOT NULL DEFAULT false
)
-- down.sql
DROP TABLE posts

migrationを実行し、posts テーブルが作成されたことを確認します。

diesel migration run

次にmodels.rsを作成します。

ここにはPost struct (structはjavascriptで言うところのObject的なやつ)を定義します。

use rocket::serde::Serialize;

#[derive(Queryable, Serialize)]
#[serde(crate = "rocket::serde")]

pub struct Post {
    pub id: i32,
    pub title: String,
    pub body: String,
    pub published: bool,
}

Queryable はSQL内でPost structを読めるようにしてくれる便利なやつ、だそうです。まだあんまりわかってない。

SerializeはPost structをAPIのresponseの型として利用するために使っています。

次はmain.rsを書いていきます。

まずdieselをextern crate (global環境にlibrary importする的なやつ)としてscopeに入れます。

そうなるとrocket_sync_db_pools::dieselと名前がかち合うのでrocket_sync_db_poolsもextern crateとしてscopeに入れました。

これはglobalを汚染してしまうので本来良くないと思います。

asを使って別の名前をつければ全部scope内に入れなくても良いはず。

#[macro_use] extern crate rocket_sync_db_pools;
#[macro_use] extern crate diesel;

次に先ほど作成したmodel.rsとmigration実行時に自動作成されたschema.rsをmain.rs内で利用するためにmodで指定します;

mod schema;
mod models;

filterメソッドなどを利用するためにdiesel::prelude::*を、Post structを利用するためにmodels::Postをuseでscope内に入れます。

use diesel::prelude::*;
use models::Post;

最後にendpointにアクセスが来たときの関数を定義します

use rocket::serde::json::Json;

#[get("/posts")]
async fn index_posts(conn: LogsDbConn) -> Json<Vec<Post>> {
    use schema::posts::dsl::*;

    conn.run(|c| {
        let result = posts.filter(published.eq(true))
            .limit(5)
            .load::<Post>(c)
            .expect("Error Loading");
        Json(result);
    }).await
}

最終的なmain.rsは以下のようになります。

#[macro_use] extern crate rocket;
#[macro_use] extern crate rocket_sync_db_pools;
#[macro_use] extern crate diesel;
use rocket::serde::json::Json;

mod schema;
mod models;

use diesel::prelude::*;
use self::models::Post;

#[database("mysql")]
struct LogsDbConn(diesel::MysqlConnection);

#[launch]
fn rocket() -> _ {
    rocket::build()
        .mount("/", routes![posts])
        .attach(LogsDbConn::fairing())
}

#[get("/posts")]
async fn index_posts(conn: LogsDbConn) -> Json<Vec<Post>> {
    use schema::posts::dsl::*;

    conn.run(|c| {
        let result = posts.filter(published.eq(true))
            .limit(5)
            .load::<Post>(c)
            .expect("Error Loading");
        Json(result);
    }).await
}

dbからのresponseをresultに入れてJsonでserializeしてresponseとして返しています。

あとはdbにデータを挿入。

insert into posts(title, body, published) values
("ワイワイ", "ワイワイしています。", false),
("ガヤガヤ", "ガヤガヤしています。", true),
("オヤオヤ", "オヤオヤしています。", true)
;

curlで確認。

$ curl localhost:8000/posts
[{"id":2,"title":"ガヤガヤ","body":"ガヤガヤしています。","published":true},{"id":3,"title":"オヤオヤ","body":"オヤオヤしています。","published":true}]

ちゃんとpublishedがtrueのものだけ返却されてればOKです!