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.toml
にrocket_sync_db_pools
を追加します。
rocket_sync_db_pools = { version = "0.1.0-rc.1",features = ["diesel_mysql_pool"] }
今回はfeatureにdiesel_mysql_poolを指定しましたがSQLiteとPostgreSQLを指定することも可能です。
その場合は以下のadapterのリストから適切なものを選択してください。
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を指定します。
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.sqlにdrop 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です!