Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

Yuki Toyoda
May 15, 2021
700

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

2021/04/15に開催した社内向けハンズオンの資料です。

Yuki Toyoda

May 15, 2021
Tweet

Transcript

  1. 作るもののゴール health というエンドポイントに GET リクエストを投げると、HTTP ステータスコードが 200 OK でかつ body

    に OK という文字列を含むレスポンスを返すエンドポイントを用 意します。 ❯ curl localhost:8080/health OK 10
  2. Rust と Web Rust にはいくつか HTTP サーバーを作るためのライブラリが存在します。 現在最も利用例が多いのは actix-web というクレートです。

    HTTP サーバーを本番環境で使用できるレベルで一通り機能が揃っているので、今回は これを利用します。 https://actix.rs/ 11
  3. Cargo.toml に actix-web への依存を追加する [dependencies] actix-web = "3.3.2" あるいは、 cargo-edit

    というツールを使い、下記を実行すると追加できます。 $ cargo install cargo-edit $ cargo add actix-web 13
  4. 実際にヘルスチェックエンドポイントを書いてみる 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
  5. サーバーの起動とエンドポイントの定義 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
  6. 非同期ランタイム Rust の async / .await はあくまでただのシンタックスシュガーになっている。 コンパイラは、裏では Future に変換して処理する。

    そのため、 Future のスケジューリングや実行を行う基盤が必要になる。 tokio , async-std などが有力。 エコシステムの関係で tokio を選んでおくのが今は無難。 20
  7. マクロ 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
  8. 作るもののゴール 下記の JSON を使った HTTP リクエストを送ると、sqlite にタスクを登録できるように します。 $ curl

    -X POST -H "Content-Type: application/json" \ -d '{"description":" タスク1"}' localhost:8080/todo 31
  9. 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
  10. 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
  11. コネクションプールを actix-web にセットする App のもつ data メソッドを利用してコネクションプールを他の箇所でも利用できる ようにする。 後々 Data<T>

    という構造体を使って取り出す。 move キーワードは、クロージャ内で使用した外部の値の所有権をクロージャー内に移 すという意味合いをもつ。 ... HttpServer::new(move || { App::new() .data(pool.clone()) .service(hc) }) ... 37
  12. 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
  13. JSON 化できる仕組み #[derive(Serialize)] がキーです。これが serde というクレートの機能を呼び出し ています。 #[] ではじまるものをアトリビュートと呼びます。 親の構造体で

    Serialize を継承させた場合、内部に持つフィールドの構造体やデータ 型もすべて Serialize を実装している必要があります。 39
  14. HTTP リクエストの JSON ボディをデシリアライズする 下記構造体を用意し、HTTP リクエストの JSON ボディのデシリアライズに使用する。 id や

    datetime は後ほど、リクエストを受け取ったタイミングで生成して埋める。 #[derive(Deserialize)] struct RegisterTodo { description: String, } 45
  15. 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
  16. sqlite に登録するための下準備を開始する sqlite は UUID 型や datetime 型が存在しない(らしい)。 それらはすべて文字列で入れる必要がある。 bool

    もないので、integer で入れ直す必要がある。 変換するための構造体を用意する。 struct SqliteTodo { id: String, description: String, done: u8, datetime: String, } 48
  17. 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
  18. 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
  19. // ... 続き 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
  20. POST エンドポイント内で sqlite に登録をかけたい Data<Pool<SqliteConnectionManager>> : 少し前に設定した actix-web の App

    に登 録されているコネクションプールを持っている。 下記でコネクションを取り出している。 let conn = db.get().unwrap(); 54
  21. 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
  22. POST エンドポイント内で sqlite に登録をかけたい Prepared Statement を発行している。 let mut stmt

    = conn .prepare("select id, description, done, datetime from todo where id = ?") .unwrap(); 56
  23. POST エンドポイント内で sqlite に登録をかけたい 発行した UUID でテーブルを検索し、合致するレコードを取り出す。 最終的にクライアントに JSON で登録したレコードを返したいので、

    SqliteTodo から Todo に変換をかけている。 変換をかける部分で Todo::from を呼んでいる。これが先ほど実装した From トレイ トを裏で呼んでいる。 57
  24. 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
  25. この節のまとめ 今回は SQLite に対する操作なので、お手軽な r2d2 系のクレートを使用してデータベー ス接続をしてみた。 Rust でよく使用される New

    Type というワークアラウンドを学んだ。 データ構造をシリアライズ/デシリアライズできる serde について学んだ。 From<T> トレイトを実装しておくと変換処理を抽象化して呼び出しできることを紹介 した。 60
  26. 紹介できなかったもの 他の RDS への接続系クレート diesel: ORM。MySQL、Postgres など幅広く対応している。 sqlx: Go の

    sqlx と同じ。ORM ではないので軽量だと思う。MySQL、Postgres な ど幅広く対応している。 63