Share
Embed
Transcript
63 slides
-
免責事項 1 時間しかないので、すべてを完璧に説明し切ることはできません。 文法を復習したりはしません。適宜キーワードをいうので、手元で調べてもらえると助 かります。 ただ応用的で他の言語にはない文法がある場合には解説します。 Rust でも Web アプリケーションが書ける(しかも結構楽に)というのを知ってもらう
ことと、 どういったライブラリがあるのか、エコシステムを知ってもらうことが目的です。 説明の便宜上、 unwrap を多用します。 3 -
作るもののゴール health というエンドポイントに GET リクエストを投げると、HTTP ステータスコードが 200 OK でかつ body
に OK という文字列を含むレスポンスを返すエンドポイントを用 意します。 ❯ curl localhost:8080/health OK 10 -
Rust と Web Rust にはいくつか HTTP サーバーを作るためのライブラリが存在します。 現在最も利用例が多いのは actix-web というクレートです。
HTTP サーバーを本番環境で使用できるレベルで一通り機能が揃っているので、今回は これを利用します。 https://actix.rs/ 11 -
Cargo.toml に actix-web への依存を追加する [dependencies] actix-web = "3.3.2" あるいは、 cargo-edit
というツールを使い、下記を実行すると追加できます。 $ cargo install cargo-edit $ cargo add actix-web 13 -
実際にヘルスチェックエンドポイントを書いてみる 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 -
サーバーの起動とエンドポイントの定義 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 -
async / .await Rust では、非同期処理は async と await というキーワードを使って行う。 意味合いは
JavaScript や C# などで登場する同名のキーワードと同じ。 19 -
非同期ランタイム Rust の async / .await はあくまでただのシンタックスシュガーになっている。 コンパイラは、裏では Future に変換して処理する。
そのため、 Future のスケジューリングや実行を行う基盤が必要になる。 tokio , async-std などが有力。 エコシステムの関係で tokio を選んでおくのが今は無難。 20 -
impl Trait Trait が実装された型を返す、という意味になる。 このケースだと Responder トレイトが実装された型なら返せる、という意味になる。 返す型が複雑になりすぎるケースで、型を簡略化するために使用する。 async fn
hc() -> impl Responder { ... 21 -
マクロ 似たような処理をまとめるもの。 関数とは少し違い、プログラムのメタ的な解析をしてより強力に共通化できる。 C/C++ のマクロとは異なり衛生的なマクロになっている。 Rust のマクロには 2 種類ある。 宣言的マクロ:
! が名前の後ろにあるもの。DSL を作るのに使われる。 手続きマクロ: 関数とかを解析してコードを自動生成できる機能をもつ。 22 -
マクロ actix-web のエンドポイントの定義は手続きマクロを用いて行うことができる。 エンドポイントが GET メソッドであることを付与する エンドポイントのパスが /health であることを付与する コンパイル時に実装が展開されて、補完されるイメージ。
#[get("/health")] async fn hc() -> impl Responder { ... 23 -
マクロ 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 -
マクロ 非同期ランタイムのメイン関数であることを示す下記のコードも… #[actix_web::main] async fn main() -> std::io::Result<()> { HttpServer::new(||
App::new().service(hc)) .bind("127.0.0.1:8080")? .run() .await } 25 -
マクロ 実は裏側では下記のように展開されていることがわかる。 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 -
クロージャー 他の言語でいうところのラムダ式とか高階関数とかそういう感じのやつ。 Fn , FnMut , FnOnce といった高階関数トレイトを引数とするとクロージャーとなる。 || は引数をとくに使用しないことを示す。
... HttpServer::new(|| App::new().service(hc)) ... 28 -
作るもののゴール 下記の JSON を使った HTTP リクエストを送ると、sqlite にタスクを登録できるように します。 $ curl
-X POST -H "Content-Type: application/json" \ -d '{"description":" タスク1"}' localhost:8080/todo 31 -
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 -
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 -
接続の確立とコネクションプールの生成 r2de_sqlite というクレートを使って ConnectionManager を用意する。 r2d2 というクレートを使って、コネクションプールを生成する。 ... let manager
= SqliteConnectionManager::file("test.db"); let pool = Pool::new(manager).unwrap(); ... 36 -
コネクションプールを actix-web にセットする App のもつ data メソッドを利用してコネクションプールを他の箇所でも利用できる ようにする。 後々 Data<T>
という構造体を使って取り出す。 move キーワードは、クロージャ内で使用した外部の値の所有権をクロージャー内に移 すという意味合いをもつ。 ... HttpServer::new(move || { App::new() .data(pool.clone()) .service(hc) }) ... 37 -
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<Utc>, } 38 -
JSON 化できる仕組み #[derive(Serialize)] がキーです。これが serde というクレートの機能を呼び出し ています。 #[] ではじまるものをアトリビュートと呼びます。 親の構造体で
Serialize を継承させた場合、内部に持つフィールドの構造体やデータ 型もすべて Serialize を実装している必要があります。 39 -
New Type パターン 型システム上区別される名前をつける。 コンパイルタイムでは TaskId と Uuid は完全に区別される。 そういった意味で型エイリアスとは別物。
DDD でいう値クラスなどを作ることができる。 TaskId がいい例。 41 -
Todo を登録するエンドポイントを作りたい /todo というエンドポイントに対して下記の JSON を POST すると、Todo が中に作 成されるようにしたい。
{ "description": "Rust ハンズオンに出席する" } 44 -
HTTP リクエストの JSON ボディをデシリアライズする 下記構造体を用意し、HTTP リクエストの JSON ボディのデシリアライズに使用する。 id や
datetime は後ほど、リクエストを受け取ったタイミングで生成して埋める。 #[derive(Deserialize)] struct RegisterTodo { description: String, } 45 -
POST エンドポイントを作って JSON を受け取れるようにする Json<RegisterTodo> から構造体を取り出すには .0 と書く。 これはタプルを取り出す記法で、タプルの 0
番目を取り出すという意味。 // 関数を書く #[post("/todo")] async fn register_todo( req: Json<RegisterTodo>, db: Data<Pool<SqliteConnectionManager>>, ) -> impl Responder { println!("{}", req.0.description); HttpResponse::Ok() } 46 -
エンドポイントを作ったら登録する 登録後、リクエストを送ってみると、送った description の内容が出力されるはず。 // main 関数内: App に登録する HttpServer::new(move
|| { App::new() .data(pool.clone()) .service(hc) .service(register_todo) }) 47 -
sqlite に登録するための下準備を開始する sqlite は UUID 型や datetime 型が存在しない(らしい)。 それらはすべて文字列で入れる必要がある。 bool
もないので、integer で入れ直す必要がある。 変換するための構造体を用意する。 struct SqliteTodo { id: String, description: String, done: u8, datetime: String, } 48 -
From トレイト データ変換を行き来するための特別な From<T> というトレイトが存在する。 コンパイラから見た意義についてはこの記事が詳しい: https://qiita.com/hadashiA/items/d0c34a4ba74564337d2f Todo と SqliteTodo
を行き来することができるようになる。 後ほど実例を紹介する。 impl From<SqliteTodo> 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 -
トレイト 共通する振る舞いを定義するために使用する。 トレイト (trait) = 束 (たば ≠ Lattice)。メソッドが束として定義されているイメージ。 他言語でいう
interface みたいなものだが、それより若干機能が多い。 50 -
POST エンドポイント内で sqlite に登録をかけたい #[post("/todo")] async fn register_todo( req: Json<RegisterTodo>,
db: Data<Pool<SqliteConnectionManager>>, ) -> 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 -
// ... 続き let mut stmt = conn .prepare("select id,
description, done, datetime from todo where id = ?") .unwrap(); let results: Vec<Todo> = 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 -
POST エンドポイント内で sqlite に登録をかけたい Data<Pool<SqliteConnectionManager>> : 少し前に設定した actix-web の App
に登 録されているコネクションプールを持っている。 下記でコネクションを取り出している。 let conn = db.get().unwrap(); 54 -
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 -
POST エンドポイント内で sqlite に登録をかけたい Prepared Statement を発行している。 let mut stmt
= conn .prepare("select id, description, done, datetime from todo where id = ?") .unwrap(); 56 -
POST エンドポイント内で sqlite に登録をかけたい 発行した UUID でテーブルを検索し、合致するレコードを取り出す。 最終的にクライアントに JSON で登録したレコードを返したいので、
SqliteTodo から Todo に変換をかけている。 変換をかける部分で Todo::from を呼んでいる。これが先ほど実装した From トレイ トを裏で呼んでいる。 57 -
POST エンドポイント内で sqlite に登録をかけたい let results: Vec<Todo> = 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 -
この節のまとめ 今回は SQLite に対する操作なので、お手軽な r2d2 系のクレートを使用してデータベー ス接続をしてみた。 Rust でよく使用される New
Type というワークアラウンドを学んだ。 データ構造をシリアライズ/デシリアライズできる serde について学んだ。 From<T> トレイトを実装しておくと変換処理を抽象化して呼び出しできることを紹介 した。 60 -
紹介できなかったもの rusoto: プロダクトでは AWS と組み合わせて使用することが多いと思いますが、AWS SDK です。tokio ベース。 tide: async-std
をベースとした HTTP サーバーフレームワーク。 62 -
紹介できなかったもの 他の RDS への接続系クレート diesel: ORM。MySQL、Postgres など幅広く対応している。 sqlx: Go の
sqlx と同じ。ORM ではないので軽量だと思う。MySQL、Postgres な ど幅広く対応している。 63