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

Cookpad Summer Internship 2021 Web API

Cookpad Summer Internship 2021 Web API

Cookpad Summer Internship 2021 10 Day Tech コース3日目講義パート
https://techlife.cookpad.com/entry/2021/09/06/130000

Takahiro Miyoshi

August 18, 2021
Tweet

More Decks by Takahiro Miyoshi

Other Decks in Programming

Transcript

  1. Image Area Image Area Image Area @sankichi92 (講師) 技術部 ユーザー・決済基盤

    グループ @osyoyu (TA) メディアプロダクト開 発部 マーケティングサー ビス開発グループ @s4ichi (TA) 技術部 クックパッドサービス 基盤グループ
  2. タイムテーブル • 10:00 講義: 要素技術の解説 • 12:00 ランチ休憩 • 13:00

    ハンズオン • 13:30 課題 • 17:30 5つのグループに分かれて成果発表 • 17:50 基礎課題の簡単な解説 • 18:00 終了
  3. Ruby の特徴 • オブジェクト指向スクリプト言語 ◦ すべてがオブジェクト ▪ プリミティブ型がない 42.class #=>

    Integer • 動的型付け ◦ Ruby 3.0 から静的型解析のための仕組みも ▪ Ruby 3 の静的解析ツール TypeProf の使い方 ▪ Ruby 3の静的解析機能のRBS、TypeProf、Steep、Sorbetの関係につ いてのノート • 非常に柔軟
  4. ローカル変数、リテラル、演算子式など # 変数宣言は不要 price = 42 tax = 4.2 #

    変数(やメソッド)はスネークケースが慣習 tax_included_price = price + tax #=> 46.2 name = "トマト" "おいしい#{name}" #=> "おいしいトマト" # nil と false 以外は真と評価される !!false #=> false !!nil #=> false !!"" #=> true !!0 #=> true # 文字列とは別にシンボル (Symbol) があり、連想配列のキーなど識別子として利用される :tomato.object_id == :tomato.object_id #=> true "tomato".object_id == "tomato".object_id #=> false
  5. メソッド定義、メソッド呼び出し def hello # 引数のないメソッド "Hello, world!" # return がない場合最後の式の値を返すので

    return は不要 end hello #=> "Hello, world!" hello() # カッコをつければメソッド呼び出しであることを明示できる(が、基本は省略する) # tax_rate はデフォルト値付きキーワード引数 def calculate_tax(price, tax_rate: 0.1) price * tax_rate end calculate_tax(100) #=> 10.0 calculate_tax 100 # 引数のある場合もカッコを省略できる(が、省略するかは場合による) calculate_tax 100, tax_rate: 0.08 #=> 8.0
  6. 制御構造 (if) price = 300 # if も式であり値を返す( if だけでなくすべては値を持つ)

    price_label = if price > 1000 :expensive elsif price > 100 :mid_range else :cheap end # 後置 if puts "not cheap" if price_label != :cheap
  7. 配列 (Array)、連想配列 (Hash) # 配列 categories = ["meat", "fish", "vegetables"]

    categories[1] #=> "fish" categories.first #=> "meat" categories.size #=> 3 # 文字列がキーの Hash item_to_price = { "onion" => 70, "carrot" => 80, "potato" => 50 } item_to_price["onion"] #=> 70 # シンボルがキーの Hash item_to_price = { onion: 70, carrot: 80, potato: 50 } item_to_price[:carrot] #=> 80 item_to_price["carrot"] #=> nil
  8. ブロック、イテレータ # %記法を使った配列 categories = %w[meat fish vegetables] #=> ["meat",

    "fish", "vegetables"] # do ... end または { ... } で囲まれたコードのかたまり(ブロック)をメソッドに渡せる categories.each do |category| puts category end categories.map { |c| c.upcase } #=> ["MEAT", "FISH", "VEGETABLES"] # for や while もあるがほとんど使わない 10.times do puts "Hello, world!" end
  9. クラス class Greeter def initialize(name) # コンストラクタ @name = name

    # インスタンス変数 end def say_hello "hello, #{@name}" end end class LoudGreeter < Greeter # 継承(単一継承のみ) def say_hello "#{super.upcase}!!!" end end greeter = Greeter.new("world") greeter.say_hello #=> "hello, world" LoudGreeter.new("world").say_hello #=> "HELLO, WORLD!!!"
  10. 定数・モジュール MASCOT_NAME = "mini-tomart" # 大文字ではじまる識別子は定数 # 定数 Greeter に

    Class クラスのインスタンスを代入 Greeter = Class.new # class Greeter; end と同じ(クラスもオブジェクト) module Minimart # モジュールを使って定数の名前空間を分けている( Mixinについては割愛) class Greeter def say_hello "Hello, #{MASCOT_NAME}" end end end greeter = Minimart::Greeter.new # :: 演算子を使ってアクセス greeter.say_hello #=> "Hello, mini-tomart!"
  11. ライブラリ (gem) • Ruby のサードパーティライブラリは RubyGems.org に gem として置かれている •

    gem install rails で gem をインストール • require "rails" で gem を利用できる ◦ (ただし、Rails ではアプリケーション初期化時に依存 gem を読み込んだ り、定数の自動読み込み仕組みがあったりしていて require を書かなくても 利用できてしまう)
  12. Bundler • gem の依存関係を管理するためのツール bundler.io • Gemfile に利用する gem を記述

    ◦ gem "rails", "~> 6.1.4" のようにバージョンを指定できる • bundle install で Gemfile の gem をインストール ◦ Gemfile.lock がなければ依存関係を解決して Gemfile.lock を作成 ◦ Gemfile.lock があればそこで指定されたバージョンをインストール • bundle exec command で Gemfile で指定された gem を利用する形で command (Ruby プログラム) を実行
  13. Rails の特徴 • フルスタックフレームワーク • MVC (Model-View-Controller) パターン • Rails

    の基本理念 ◦ 同じことを繰り返すな (Don't Repeat Yourself: DRY) ◦ 設定より規約 (Convention Over Configuration: CoC)
  14. Rails のメリット・デメリット • Rails の用意した道 (The Rails Way) に乗ることで非常にす ばやく

    Web アプリケーションを開発できる • The Rails Way で実現できないこともあり、そこから外れると 大変なことが多い
  15. minimart API における Rails • API のみでフロントエンドを持たず、最小限の機能しか利用 しない ◦ rails

    new minimart --api --minimal -d mysql • GraphQL Ruby (後述) を利用するので、MVC の View や Controller もほとんど使わない • Model に対応する Active Record の機能を主に利用
  16. Active Record • オブジェクト/リレーショナルマッピング (ORM) を行う • Active Record パターンが由来

    ◦ DB のテーブルをクラス、レコードをそのインスタンスに対 応させ、データアクセスのロジックをオブジェクトに持た せる • データの操作を手軽に行える • 一方で AR を継承したモデルが責務過多になりがち
  17. Active Record における「設定より規約」 # データベースへの接続( Rails アプリでは config/database.yml の設定が利用される) ActiveRecord::Base.establish_connection(

    adapter: 'mysql2', host: 'localhost', username: 'root', password: '', database: 'minimart_development', ) # User モデルに ActiveRecord::Base を継承させる class User < ActiveRecord::Base end # 規約により、クラス名から対応するテーブルが users になる User.table_name #=> "users" # 規約により、主キーは常に id User.primary_key #=> "id"
  18. CRUD: Create User.create(name: 'tomart') # INSERT INTO `users` (`name`, `created_at`,

    `updated_at`) VALUES ('tomart', '2021-08-18 10:00:00', '2021-08-18 10:00:00') user = User.new user.name = 'mini-tomart' user.save #=> true # INSERT INTO `users` (`name`, `created_at`, `updated_at`) VALUES ('mini-tomart', '2021-08-18 10:00:00', '2021-08-18 10:00:00')
  19. CRUD: Read User.all # SELECT `users`.* FROM `users` User.where('updated_at >

    ?', 1.day.ago).order(:updated_at) # SELECT `users`.* FROM `users` WHERE (updated_at > '2021-08-17 10:00:00') ORDER BY `users`.`updated_at` ASC User.find_by(name: 'tomart') # SELECT `users`.* FROM `users` WHERE `users`.`name` = 'tomart' LIMIT 1
  20. CRUD: Update user = User.find_by(name: 'tomart') user.update(name: 'mini-tomart') #=> true

    # UPDATE `users` SET `users`.`name` = 'mini-tomart', `users`.`updated_at` = '2021-08-18 10:00:00' WHERE `users`.`id` = 1 user.name #=> "mini-tomart" user.name = 'tomart' user.save #=> true # UPDATE `users` SET `users`.`name` = 'tomart', `users`.`updated_at` = '2021-08-18 10:00:00' WHERE `users`.`id` = 1
  21. 関連付け (Association) の定義 class User < ActiveRecord::Base # users テーブルは

    pickup_location_id という pickup_locations の外部キーを持ち # pickup_locations の主キーは id で対応するモデルは PickupLocation (規約) belongs_to :pickup_location end class PickupLocation < ActiveRecord::Base has_many :users end User PickupLocation n 1
  22. 関連付け (Association) の利用 user = User.find_by(name: 'tomart') pickup_location = PickupLocation.create(name:

    'WeWork みなとみらい') user.update(pickup_location_id: pickup_location.id) # User#pickup_location というメソッドが追加される user.pickup_location.name #=> "WeWork みなとみらい" # SELECT `pickup_locations`.* FROM `pickup_locations` WHERE `pickup_locations`.`id` = 1 LIMIT 1 # PickupLocation#users というメソッドが追加される pickup_location.users.first.name #=> "tomart" # SELECT `users`.* FROM `users` WHERE `users`.`pickup_location_id` = 1
  23. DB スキーマ管理のための DSL (1/2) create_table :pickup_locations do |t| # 主キーとして

    id カラムを暗黙的に作成する t.string :name, null: false t.timestamps # created_at, updated_at というカラムを作成する end # CREATE TABLE `pickup_locations` ( # `id` bigint NOT NULL AUTO_INCREMENT PRIMARY KEY, # `name` varchar(255) NOT NULL, # `created_at` datetime NOT NULL, # `updated_at` datetime NOT NULL)
  24. DB スキーマ管理のための DSL (2/2) create_table :users do |t| t.belongs_to :pickup_location

    # pickup_location_id を作成してインデックスを貼る t.string :name, null: false t.timestamps t.index :name, unique: true # name にユニーク制約をつける end # CREATE TABLE `users` ( # `id` bigint NOT NULL AUTO_INCREMENT PRIMARY KEY, # `pickup_location_id` bigint, # `name` varchar(255) NOT NULL, # `created_at` datetime NOT NULL, # `updated_at` datetime NOT NULL) # CREATE INDEX `index_users_on_pickup_location_id` ON `users` (`pickup_location_id`) # CREATE UNIQUE INDEX `index_users_on_name` ON `users` (`name`)
  25. Ridgepole # DSL をもとに DB スキーマの変更を宣言的に行う gem # ridgepole コマンドを実行することで変更を適用できる

    # https://github.com/ridgepole/ridgepole create_table :pickup_locations do |t| t.string :name, null: false + t.string :address, null: false t.timestamps end # ALTER TABLE `pickup_locations` ADD `address` varchar(255) NOT NULL AFTER `name`
  26. アンケート GraphQL API サーバーについて • 開発経験がない #=> 20 • Ruby

    以外での開発経験がある #=> 0 • Ruby での開発経験がある #=> 0
  27. GraphQL Ruby • GraphQL の Ruby 実装 (gem) ◦ GraphQL

    のクエリ(文字列)を入力にデータ(Hash)を出 力するまでのもろもろをいい感じにやってくれる • サーバーの機能はもたない ◦ Rails との連携がサポートされている ▪ ハンズオンで実践
  28. # 入力 (GraphQL query) query { pickupLocations { name }

    } // 出力 (JSON) { "data": { "pickupLocations": [ { "name": "WeWork みなとみらい" }, { "name": "恵比寿ガーデンプレイスタワー" } ] } } GraphQL の入出力
  29. GraphQL Ruby を用いた場合 class MinimartSchema < GraphQL::Schema # GraphQL Ruby

    より Graphql::Schema を継承 # ここを起点に実装 # ... end result = MinimartSchema.execute(<<~GRAPHQL) # ヒアドキュメント query { pickupLocations { name } } GRAPHQL result['data']['pickupLocations'][0]['name'] #=> "WeWork みなとみらい" puts result.to_json # {"data":{"pickupLocations":[{"name":"WeWork みなとみらい"},{"name":"恵比寿ガーデンプレイス タワー"}]}}
  30. 必要な実装 • スキーマの定義 ◦ GraphQL の型を Ruby のクラスで定義(コードファース ト) •

    リゾルバ (resolver) の実装 ◦ 各フィールドに対し何を返すかを決めるメソッドを実装 クエリのパースやバリデーション、結果の整形等は上記をもとに GraphQL Ruby がいい感じにやってくれる
  31. 以降の例で実現するスキーマ type Query { # すべての受け取り場所を返す pickupLocations: [PickupLocation!]! } #

    Active Record の説明で定義した PickupLocation モデルに対応 # (データは DB の pickup_locations テーブルにある) type PickupLocation { id: ID! name: String! }
  32. GraphQL Ruby による型定義 # GraphQL::Schema::Object を継承したクラスで GraphQL の型を定義する module Types

    class PickupLocationType < GraphQL::Schema::Object # filed クラスメソッドで定義する型のもつフィールドを定義する # 第一引数がフィールド名 # 第二引数が返り値の型( Ruby の型も GraphQL の型に置き換えられる) field :id, ID, null: false field :name, String, null: false end end # クラス名から GraphQL の型名が決まる(規約) Types::PickupLocationType.graphql_name #=> "PickupLocation"
  33. module Types class QueryType < GraphQL::Schema::Object # 第一引数はスネークケースが慣習(キャメルケースに置き換えられる) # 第二引数に配列を渡すと

    GraphQL のリスト型と解釈される(直感的だが 不思議) field :pickup_locations, [Types::PickupLocationType], null: false end end class MinimartSchema < GraphQL::Schema # root となる Query 型に対応するクラスを指定 query Types::QueryType end GraphQL Ruby によるスキーマ定義
  34. リゾルバの実装 (1/2) module Types class QueryType < GraphQL::Schema::Object field :pickup_locations,

    [Types::PickupLocationType], null: false # デフォルトでフィールド名と同名のメソッドがリゾルバになる # リゾルバでは返り値の GraphQL の型に対応する Ruby オブジェクトを返す # ここでは PickupLocation のインスタンスの Array(-like) を返している def pickup_locations PickupLocation.all end end end
  35. リゾルバの実装 (2/2) module Types class PickupLocationType < GraphQL::Schema::Object field :id,

    ID, null: false field :name, String, null: false # 自身の型に対応するアプリケーションのオブジェクトに object でアクセスできる def id object.id end # フィールド名のメソッドがない場合は object の同名メソッドを呼ぶので name は省略 end end
  36. クエリの実行 # ここまでのコードで以下が実現できる result = MinimartSchema.execute(<<~GRAPHQL) query { pickupLocations {

    name } } GRAPHQL result['data']['pickupLocations'].first['name'] #=> "WeWork みなとみらい" puts result.to_json # {"data":{"pickupLocations":[{"name":"WeWork みなとみらい"},{"name":"恵比寿 ガーデンプレイスタワー "}]}}
  37. ハンズオン・課題で実践しながら確認 • Rails との連携 • Context • Mutation • 引数

    • Input Objects • Validation • 認可 (Authorization) • エラーハンドリング などなど適宜ドキュメントを参照しつつ
  38. Remote Procedure Call (RPC) • ネットワーク上の別のマシンの手続きを呼び出す手法 ◦ 分散システムのための技術 • Web

    よりずっと歴史が長い ◦ 遠隔手続き呼出し - Wikipedia によると1976年まで遡る
  39. gRPC の特徴 • HTTP/2 上で動作 ◦ HTTP は隠蔽されていて利用時は気にしなくてよい • 様々な言語・プラットフォームで利用可能

    • Protocol Buffers をデフォルトで使用 ◦ インターフェース定義言語 (IDL) ▪ GraphQL のスキーマ定義 (schema.graphql) のようなもの ◦ データのシリアライズ ▪ GraphQL のデータのシリアライズフォーマットは基本的に JSON • クライアントライブラリの自動生成
  40. gRPC と GraphQL との比較 • 両者ともネットワークを介した API のための技術 • GraphQL

    が優れている点 ◦ クエリ言語による柔軟なデータ取得 • gRPC が優れている点 ◦ HTTP/2 と Protocol Buffers による効率的な通信 ◦ サーバーの実装が比較的シンプル
  41. minimart API における gRPC • gRPC API を叩いて注文完了時の決済を行う • minifinancier

    が決済機能を提供 ◦ クックパッドの決済基盤 Financier が由来 ▪ https://techlife.cookpad.com/entry/2019/12/17/113612 ◦ Node.js & TypeScript 製 ▪ Ruby 以外の言語かつ Web フロントエンド講義で使用 ◦ 実際に決済を行うわけではなく実装は空 ▪ 本来は Stripe など決済代行の API を叩く
  42. gRPC の実装手順 1. Protocol Buffers でインターフェースを定義 2. 1 をもとに gRPC

    のコードを生成 3. サーバー側の rpc を実装 4. 生成されたコードでクライアントから rpc 呼び出し
  43. minifinancier の提供する RPC • ユーザーへの請求を行う Charge という rpc を提供 •

    パラメータは以下の2つ ◦ user_id: 請求対象のユーザー ID ◦ amount: 請求金額(JPY) • 返り値はPayment 型のメッセージ ◦ 上記のパラメータに加えて請求 ID と請求時刻をフィール ドにもつ
  44. Protocol Buffers によるインターフェース定義 // minifinancier.proto syntax = "proto3"; // v3

    のシンタックスの使用 package minifinancier; // 名前空間の分割( Ruby ではモジュールに対応) import "google/protobuf/timestamp.proto"; // 別ファイルやライブラリからメッセージ定義をインポート可能 service PaymentGateway { // rpc をサービスという単位で定義 rpc Charge(ChargeRequest) returns (Payment); } message ChargeRequest { uint64 user_id = 1; // フィールドごとにユニークな番号を割り当てる。バイナリエンコーディングで利用 uint32 amount = 2; } message Payment { string id = 1; uint64 user_id = 2; uint32 amount = 3; google.protobuf.Timestamp create_time = 4; // ライブラリのものや独自のメッセージ型も使用可能 }
  45. protoc によるコードの生成 • protocol buffer compiler (protoc) により .proto ファイル

    に定義したメッセージを扱うコードを生成できる • gRPC では add-on を使ってサービス定義から rpc のコード も生成 ◦ Ruby 用ラッパー (gem) : grpc-tools ◦ TypeScript 用ラッパー (npm): grpc_tools_node_protoc_ts • (ハンズオンのリポジトリを使ってデモ)
  46. サーバーの実装 (Node.js & TypeScript) import { sendUnaryData, Server, ServerCredentials, ServerUnaryCall

    } from "@grpc/grpc-js"; import { ChargeRequest, Payment } from "./minifinancier_pb"; // メッセージ定義から自動生成されたコード import { PaymentGatewayService } from "./minifinancier_grpc_pb"; // サービス定義から自動生成されたコード import { Timestamp } from "google-protobuf/google/protobuf/timestamp_pb"; function charge(call: ServerUnaryCall<ChargeRequest, Payment>, callback: sendUnaryData<Payment>) { const response = new Payment() .setId("payment-42") // 説明のため決め打ち .setUserId(call.request.getUserId()) .setAmount(call.request.getAmount()) .setCreateTime(Timestamp.fromDate(new Date())); callback(null, response); // コールバックの第2引数に rpc の返り値を渡す(第 1引数に値を渡すのはエラーの場合) } const server = new Server(); server.addService(PaymentGatewayService, { charge }); // サービスと対応する rpc charge の実装をサーバーに追加 server.bindAsync("0.0.0.0:50051", ServerCredentials.createInsecure(), () => { server.start(); // 50051 ポートで gRPC サーバーを起動 });
  47. クライアントの実装 (Ruby) require 'minifinancier_services_pb' # 自動生成されたコードのロード # 自動生成されたコードから gRPC Stub

    を作成 service = Minifinancier::PaymentGateway::Stub.new( 'localhost:50051', :this_channel_is_insecure, ) # gRPC Stub のメソッドを呼ぶと minifinancier の rpc が呼ばれる payment = service.charge( Minifinancier::ChargeRequest.new(user_id: 1, amount: 100), ) payment.id #=> "payment-42"
  48. 公式ドキュメントを読もう • Ruby ◦ https://docs.ruby-lang.org/ja/3.0.0/doc/ ◦ https://rubyapi.org/ • Ruby on

    Rails ◦ https://railsguides.jp/ ◦ https://api.rubyonrails.org/ • GraphQL Ruby ◦ https://graphql-ruby.org/guides • gRPC / Protocol Bufflers ◦ https://grpc.io/docs/ ◦ https://developers.google.com/protocol-buffers/docs/proto3