Slide 1

Slide 1 text

Rust ハンズオン第 4 回 Web バックエンド実装編 1

Slide 2

Slide 2 text

今日のゴール Rust による Web バックエンド開発を体感する。 actix-web の基本的な使い方を理解する。 アプリケーションを作る際に便利なクレートを知ってもらう。 2

Slide 3

Slide 3 text

免責事項 1 時間しかないので、すべてを完璧に説明し切ることはできません。 文法を復習したりはしません。適宜キーワードをいうので、手元で調べてもらえると助 かります。 ただ応用的で他の言語にはない文法がある場合には解説します。 Rust でも Web アプリケーションが書ける(しかも結構楽に)というのを知ってもらう ことと、 どういったライブラリがあるのか、エコシステムを知ってもらうことが目的です。 説明の便宜上、 unwrap を多用します。 3

Slide 4

Slide 4 text

目次 今回作るアプリケーションの仕様について ヘルスチェックできるエンドポイントを作る データベースと接続して、タスクの保存や取得を行えるようにする 4

Slide 5

Slide 5 text

今回作るアプリケーションの仕様について 5

Slide 6

Slide 6 text

仕様 よくある Todo アプリのバックエンドを作成します。 POST するとデータベースに新しいタスクを作成できるものとします。 6

Slide 7

Slide 7 text

ソースコード https://github.com/yuk1ty/rust-webapp-tutorial 7

Slide 8

Slide 8 text

事前準備 Rust のインストール SQLite のインストール: brew install sqlite3 8

Slide 9

Slide 9 text

ヘルスチェックできるエンドポイントを作る 9

Slide 10

Slide 10 text

作るもののゴール health というエンドポイントに GET リクエストを投げると、HTTP ステータスコードが 200 OK でかつ body に OK という文字列を含むレスポンスを返すエンドポイントを用 意します。 ❯ curl localhost:8080/health OK 10

Slide 11

Slide 11 text

Rust と Web Rust にはいくつか HTTP サーバーを作るためのライブラリが存在します。 現在最も利用例が多いのは actix-web というクレートです。 HTTP サーバーを本番環境で使用できるレベルで一通り機能が揃っているので、今回は これを利用します。 https://actix.rs/ 11

Slide 12

Slide 12 text

新しいプロジェクトを用意する cargo new webapp-handson 12

Slide 13

Slide 13 text

Cargo.toml に actix-web への依存を追加する [dependencies] actix-web = "3.3.2" あるいは、 cargo-edit というツールを使い、下記を実行すると追加できます。 $ cargo install cargo-edit $ cargo add actix-web 13

Slide 14

Slide 14 text

ヘルスチェックするエンドポイントを書く 14

Slide 15

Slide 15 text

コードはこちら https://github.com/yuk1ty/rust-webapp-tutorial/tree/master/health-check 15

Slide 16

Slide 16 text

実際にヘルスチェックエンドポイントを書いてみる use actix_web::{get, App, HttpResponse, HttpServer, Responder}; #[get("/health")] async fn hc() -> impl Responder { HttpResponse::Ok().body("OK") } #[actix_web::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| App::new().service(hc)) .bind("127.0.0.1:8080")? .run() .await } 16

Slide 17

Slide 17 text

ん? かなり見慣れない感じのコードになっているような…… 1 つ 1 つ解説していきます。 17

Slide 18

Slide 18 text

サーバーの起動とエンドポイントの定義 HttpServer という構造体が HTTP サーバーの大本。 new するタイミングで投げ込まれる App によって、エンドポイントを登録している。 bind によって、どのアドレス・ポートで開くかを決定している。 ? は Result 型がエラーだったときにエラーの伝播を自動でしてくれる。 run で起動する。 async fn main() -> std::io::Result<()> { HttpServer::new(|| App::new().service(hc)) .bind("127.0.0.1:8080")? .run() .await } 18

Slide 19

Slide 19 text

async / .await Rust では、非同期処理は async と await というキーワードを使って行う。 意味合いは JavaScript や C# などで登場する同名のキーワードと同じ。 19

Slide 20

Slide 20 text

非同期ランタイム Rust の async / .await はあくまでただのシンタックスシュガーになっている。 コンパイラは、裏では Future に変換して処理する。 そのため、 Future のスケジューリングや実行を行う基盤が必要になる。 tokio , async-std などが有力。 エコシステムの関係で tokio を選んでおくのが今は無難。 20

Slide 21

Slide 21 text

impl Trait Trait が実装された型を返す、という意味になる。 このケースだと Responder トレイトが実装された型なら返せる、という意味になる。 返す型が複雑になりすぎるケースで、型を簡略化するために使用する。 async fn hc() -> impl Responder { ... 21

Slide 22

Slide 22 text

マクロ 似たような処理をまとめるもの。 関数とは少し違い、プログラムのメタ的な解析をしてより強力に共通化できる。 C/C++ のマクロとは異なり衛生的なマクロになっている。 Rust のマクロには 2 種類ある。 宣言的マクロ: ! が名前の後ろにあるもの。DSL を作るのに使われる。 手続きマクロ: 関数とかを解析してコードを自動生成できる機能をもつ。 22

Slide 23

Slide 23 text

マクロ actix-web のエンドポイントの定義は手続きマクロを用いて行うことができる。 エンドポイントが GET メソッドであることを付与する エンドポイントのパスが /health であることを付与する コンパイル時に実装が展開されて、補完されるイメージ。 #[get("/health")] async fn hc() -> impl Responder { ... 23

Slide 24

Slide 24 text

マクロ cargo expand というツールを使用すると、マクロの展開結果を見ることができる。 これを使って先ほどのマクロを展開してみた結果は下記のようになっている。 #[allow(non_camel_case_types, missing_docs)] pub struct hc; impl actix_web::dev::HttpServiceFactory for hc { fn register(self, __config: &mut actix_web::dev::AppService) { async fn hc() -> impl Responder { HttpResponse::Ok().body("OK") } let __resource = actix_web::Resource::new("/health") .name("hc") .guard(actix_web::guard::Get()) .to(hc); actix_web::dev::HttpServiceFactory::register(__resource, __config) } } 24

Slide 25

Slide 25 text

マクロ 非同期ランタイムのメイン関数であることを示す下記のコードも… #[actix_web::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| App::new().service(hc)) .bind("127.0.0.1:8080")? .run() .await } 25

Slide 26

Slide 26 text

マクロ 実は裏側では下記のように展開されていることがわかる。 fn main() -> std::io::Result<()> { actix_web::rt::System::new("main").block_on(async move { { HttpServer::new(|| App::new().service(hc)) .bind("127.0.0.1:8080")? .run() .await } }) } 26

Slide 27

Slide 27 text

DSL が厳しすぎる…? 気持ちはわかる。 27

Slide 28

Slide 28 text

クロージャー 他の言語でいうところのラムダ式とか高階関数とかそういう感じのやつ。 Fn , FnMut , FnOnce といった高階関数トレイトを引数とするとクロージャーとなる。 || は引数をとくに使用しないことを示す。 ... HttpServer::new(|| App::new().service(hc)) ... 28

Slide 29

Slide 29 text

この節のまとめ actix-web を使ったエンドポイントの設定方法を学んだ。 いくつかの Rust の応用的な文法について学んだ。 async/await impl Trait マクロ クロージャー 29

Slide 30

Slide 30 text

データベースに接続して、タスクの保存や取得を行えるよ うにする 30

Slide 31

Slide 31 text

作るもののゴール 下記の JSON を使った HTTP リクエストを送ると、sqlite にタスクを登録できるように します。 $ curl -X POST -H "Content-Type: application/json" \ -d '{"description":" タスク1"}' localhost:8080/todo 31

Slide 32

Slide 32 text

コードはこちら 一部チュートリアルでは解説しなかったものも含まれます。 https://github.com/yuk1ty/rust-webapp-tutorial/tree/master/second-todo-list 32

Slide 33

Slide 33 text

Cargo.toml に必要なクレートの設定を追加する [dependencies] actix-web = "3.3.2" serde = { version = "1.0.125", features = ["derive"] } chrono = { version = "0.4.19", features = ["serde"] } uuid = { version = "0.8.2", features = ["serde", "v4"] } r2d2 = "0.8.9" r2d2_sqlite = "0.18.0" rusqlite = "0.25.0" 33

Slide 34

Slide 34 text

データベースへの接続をする 今回は sqlite を使ってデータベースへの接続を行ってみます。 接続を確立できるように、いくつか設定をメイン関数に用意してみましょう。 うまくいくとエラーは出ることなく起動できるはず。 34

Slide 35

Slide 35 text

use r2d2::Pool; use r2d2_sqlite::SqliteConnectionManager; #[actix_web::main] async fn main() -> std::io::Result<()> { let manager = SqliteConnectionManager::file("test.db"); let pool = Pool::new(manager).unwrap(); HttpServer::new(move || { App::new() .data(pool.clone()) .service(hc) }) .bind("127.0.0.1:8080")? .run() .await } 35

Slide 36

Slide 36 text

接続の確立とコネクションプールの生成 r2de_sqlite というクレートを使って ConnectionManager を用意する。 r2d2 というクレートを使って、コネクションプールを生成する。 ... let manager = SqliteConnectionManager::file("test.db"); let pool = Pool::new(manager).unwrap(); ... 36

Slide 37

Slide 37 text

コネクションプールを actix-web にセットする App のもつ data メソッドを利用してコネクションプールを他の箇所でも利用できる ようにする。 後々 Data という構造体を使って取り出す。 move キーワードは、クロージャ内で使用した外部の値の所有権をクロージャー内に移 すという意味合いをもつ。 ... HttpServer::new(move || { App::new() .data(pool.clone()) .service(hc) }) ... 37

Slide 38

Slide 38 text

Todo 構造体を用意する 先ほどのコードに Todo 構造体を追加してみましょう。 TaskId は後述しますが New Type パターンと呼ばれる手法を使用しています。 use serde::Serialize; use uuid::Uuid; #[derive(Serialize)] struct TaskId(Uuid); #[derive(Serialize)] struct Todo { id: TaskId, description: String, done: bool, datetime: DateTime, } 38

Slide 39

Slide 39 text

JSON 化できる仕組み #[derive(Serialize)] がキーです。これが serde というクレートの機能を呼び出し ています。 #[] ではじまるものをアトリビュートと呼びます。 親の構造体で Serialize を継承させた場合、内部に持つフィールドの構造体やデータ 型もすべて Serialize を実装している必要があります。 39

Slide 40

Slide 40 text

Todo をリストで返せる構造体を実装する Todo 構造体をリストで返せる構造体をさらに用意します。 後述しますが New Type パターンというものを使用して定義します。 #[derive(Serialize)] struct TodoList(Vec); 40

Slide 41

Slide 41 text

New Type パターン 型システム上区別される名前をつける。 コンパイルタイムでは TaskId と Uuid は完全に区別される。 そういった意味で型エイリアスとは別物。 DDD でいう値クラスなどを作ることができる。 TaskId がいい例。 41

Slide 42

Slide 42 text

serde serde はデータ構造をシリアライズ/デシリアライズできるクレート。 JSON だけでなく、他のいろんな構造にシリアライズ/デシリアライズできる。 よく使う。 42

Slide 43

Slide 43 text

その他の便利クレート uuid : UUID を生成できる。 chrono : DateTime 型がそれ。Rust の日付操作のデファクト。 43

Slide 44

Slide 44 text

Todo を登録するエンドポイントを作りたい /todo というエンドポイントに対して下記の JSON を POST すると、Todo が中に作 成されるようにしたい。 { "description": "Rust ハンズオンに出席する" } 44

Slide 45

Slide 45 text

HTTP リクエストの JSON ボディをデシリアライズする 下記構造体を用意し、HTTP リクエストの JSON ボディのデシリアライズに使用する。 id や datetime は後ほど、リクエストを受け取ったタイミングで生成して埋める。 #[derive(Deserialize)] struct RegisterTodo { description: String, } 45

Slide 46

Slide 46 text

POST エンドポイントを作って JSON を受け取れるようにする Json から構造体を取り出すには .0 と書く。 これはタプルを取り出す記法で、タプルの 0 番目を取り出すという意味。 // 関数を書く #[post("/todo")] async fn register_todo( req: Json, db: Data>, ) -> impl Responder { println!("{}", req.0.description); HttpResponse::Ok() } 46

Slide 47

Slide 47 text

エンドポイントを作ったら登録する 登録後、リクエストを送ってみると、送った description の内容が出力されるはず。 // main 関数内: App に登録する HttpServer::new(move || { App::new() .data(pool.clone()) .service(hc) .service(register_todo) }) 47

Slide 48

Slide 48 text

sqlite に登録するための下準備を開始する sqlite は UUID 型や datetime 型が存在しない(らしい)。 それらはすべて文字列で入れる必要がある。 bool もないので、integer で入れ直す必要がある。 変換するための構造体を用意する。 struct SqliteTodo { id: String, description: String, done: u8, datetime: String, } 48

Slide 49

Slide 49 text

From トレイト データ変換を行き来するための特別な From というトレイトが存在する。 コンパイラから見た意義についてはこの記事が詳しい: https://qiita.com/hadashiA/items/d0c34a4ba74564337d2f Todo と SqliteTodo を行き来することができるようになる。 後ほど実例を紹介する。 impl From for Todo { fn from(st: SqliteTodo) -> Self { Todo { id: Uuid::parse_str(st.id.as_str()).unwrap(), description: st.description, done: matches!(st.done, 1), datetime: Utc .from_local_datetime( &NaiveDateTime::parse_from_str(st.datetime.as_str(), "%Y-%m-%dT%H:%M:%S") .unwrap(), ) .unwrap(), } } } 49

Slide 50

Slide 50 text

トレイト 共通する振る舞いを定義するために使用する。 トレイト (trait) = 束 (たば ≠ Lattice)。メソッドが束として定義されているイメージ。 他言語でいう interface みたいなものだが、それより若干機能が多い。 50

Slide 51

Slide 51 text

matches! マクロ done: matches!(st.done, 1) 左に判定したい対象、右に true にしたい値を入れる。 右の値に左の値が一致すれば true が返る。 51

Slide 52

Slide 52 text

POST エンドポイント内で sqlite に登録をかけたい #[post("/todo")] async fn register_todo( req: Json, db: Data>, ) -> impl Responder { let id = Uuid::new_v4(); let todo = SqliteTodo { id: id.to_string(), description: req.0.description, done: 0, datetime: Utc::now().format("%Y-%m-%dT%H:%M:%S").to_string(), }; let conn = db.get().unwrap(); conn.execute( "insert into todo (id, description, done, datetime) values(?1, ?2, ?3, ?4)", params![todo.id, todo.description, todo.done, todo.datetime], ) .unwrap(); // ... 続く 52

Slide 53

Slide 53 text

// ... 続き let mut stmt = conn .prepare("select id, description, done, datetime from todo where id = ?") .unwrap(); let results: Vec = stmt .query_map(params![id.to_string()], |row| { Ok(SqliteTodo { id: row.get_unwrap(0), description: row.get_unwrap(1), done: row.get_unwrap(2), datetime: row.get_unwrap(3), }) }) .unwrap() .into_iter() .map(|r| Todo::from(r.unwrap())) .collect(); HttpResponse::Ok().json(TodoList(results)) } 53

Slide 54

Slide 54 text

POST エンドポイント内で sqlite に登録をかけたい Data> : 少し前に設定した actix-web の App に登 録されているコネクションプールを持っている。 下記でコネクションを取り出している。 let conn = db.get().unwrap(); 54

Slide 55

Slide 55 text

POST エンドポイント内で sqlite に登録をかけたい insert クエリを発行している。 conn.execute( "insert into todo (id, description, done, datetime) values(?1, ?2, ?3, ?4)", params![todo.id, todo.description, todo.done, todo.datetime], ) .unwrap(); 55

Slide 56

Slide 56 text

POST エンドポイント内で sqlite に登録をかけたい Prepared Statement を発行している。 let mut stmt = conn .prepare("select id, description, done, datetime from todo where id = ?") .unwrap(); 56

Slide 57

Slide 57 text

POST エンドポイント内で sqlite に登録をかけたい 発行した UUID でテーブルを検索し、合致するレコードを取り出す。 最終的にクライアントに JSON で登録したレコードを返したいので、 SqliteTodo から Todo に変換をかけている。 変換をかける部分で Todo::from を呼んでいる。これが先ほど実装した From トレイ トを裏で呼んでいる。 57

Slide 58

Slide 58 text

POST エンドポイント内で sqlite に登録をかけたい let results: Vec = stmt .query_map(params![id.to_string()], |row| { Ok(SqliteTodo { id: row.get_unwrap(0), description: row.get_unwrap(1), done: row.get_unwrap(2), datetime: row.get_unwrap(3), }) }) .unwrap() .into_iter() .map(|r| Todo::from(r.unwrap())) .collect(); 58

Slide 59

Slide 59 text

動かして確認してみる POST リクエストを送る。 SQLite のクライアントを起動して、レコードが入ったことを確かめる。 59

Slide 60

Slide 60 text

この節のまとめ 今回は SQLite に対する操作なので、お手軽な r2d2 系のクレートを使用してデータベー ス接続をしてみた。 Rust でよく使用される New Type というワークアラウンドを学んだ。 データ構造をシリアライズ/デシリアライズできる serde について学んだ。 From トレイトを実装しておくと変換処理を抽象化して呼び出しできることを紹介 した。 60

Slide 61

Slide 61 text

フルバージョンのチュートリアルに記載したもの RDS からすべてのタスクを取り出して返すエンドポイントを作ってあります。 ロギングについても解説しています。 https://github.com/yuk1ty/rust-webapp-tutorial 61

Slide 62

Slide 62 text

紹介できなかったもの rusoto: プロダクトでは AWS と組み合わせて使用することが多いと思いますが、AWS SDK です。tokio ベース。 tide: async-std をベースとした HTTP サーバーフレームワーク。 62

Slide 63

Slide 63 text

紹介できなかったもの 他の RDS への接続系クレート diesel: ORM。MySQL、Postgres など幅広く対応している。 sqlx: Go の sqlx と同じ。ORM ではないので軽量だと思う。MySQL、Postgres な ど幅広く対応している。 63