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

Cookpad Summer Internship 2021 Web API

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.

Cookpad Summer Internship 2021 Web API

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

Avatar for Takahiro Miyoshi

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