GraphQL Schema Design @ Scale (Marc-André Giroux)メモ
最近は GraphQL やっていきな感じです。ただスキーマ設計が色々と悩ましく、設計の参考にするためにGraphQL Schema Design @ Scale (Marc-André Giroux) を観たのでかなり雑にメモ。
※ 英語のプレゼン動画をさくっと観ただけなので、正しく理解できていない所もあるかもしれません。
なぜ GraphQL を使うのか
GraphQL スキーマデザインについての誤解
- 複雑なアプリケーションにおいて、GraphQL はそのまま DB のインターフェースになることはめったにない
- GraphQL スキーマは既存の REST API のリソースにマッチする必要はない
- UI と 1:1 でマッピングされる必要はない
GraphQL の真の力
良い GraphQL スキーマをつくるための 2 つのポイント
- アプリケーションのドメインのエキスパートになり、知識を身につける
- GraphQL 特有の設計のエキスパートになる
そしてこれらを同じ人間が担当することはめったにない。
GitHub のアプローチは
Guiding Principles
データよりも振る舞いやユースケースを重視した設計
type PullRequest { title: String! description: String! ciStatus: CIStatus! reviews: [Review!]! }
クライアントがこの PR がマージ OK かどうか知りたい場合、以下のようなチェックが必要になるはず。
fun mergeable?(pr) pr.ciStatus == CI_STATUSES.GREEN && pr.reviews.all? { |r| r.approved }
でもクライアントはただマージできるのかどうか知りたいのでスキーマ的には以下のような形で十分。
type PullRequest { title: String! description: String! ciStatus: CIStatus! reviews: [Review!]! isMergeable: Boolean! }
マージ OK の条件が今後変わっても、それはサーバ側で吸収することでクライアントはただマージできるかどうか見ていればよい。
スマートで汎用的なフィールドよりも高度に最適化されたフィールド
type Query { user(id: ID, login: String): User }
nullable なid
かlogin
を受け取れるような設計より、それぞれのアプローチに最適化されたクエリを定義して型の恩恵を受けたほうがよい。
type Query { userById(id: ID!): User userByLogin(login: String!): User }
Tips
SDL のチェック
$ bin/dump-graphql-schema > config/schema.public.graphql $ git add config/schema.public.graphql
みんなスキーマをどんどん更新していくが、GitHub はgraphql-ruby
を使っているのでスキーマにどのような変更があったのかぱっと見てわかりにくい。上記のようなアプローチによって
スキーマ設計ミスった時や breaking changes を入れたい場合
@deprecated
を使うoldField (@deprecatedつき)
,newField
をメンテする- GitHub ではすべての@deprecated をあつめて breaking changes ページに公開してメンテしている
deprecated( start_date: ..., reason: ..., superseded_by: .... ) type Repository { databaseId: ID! @deprecated(reason: ...) }
RailsのマイグレーションでPostgreSQLのCREATE INDEX CONCURRENTLYする
最近、PostgreSQL を使用している Rails プロジェクトでCREATE INDEX CONCURRENTLY
する機会があったのでメモ。
CREATE INDEX CONCURRENTLY とは
通常のインデックス作成処理では、処理が完了するまで対象テーブルへの書き込みはロックされる(読み取りはロックされない)。
そしてテーブルのレコードが大量にある場合、インデックス作成に数時間かかることも考えられるので、その間ずっと書き込みがロックされるのはツラい。
PostgreSQL は CREATE INDEX
でCONCURRENTLY
というオプションをサポートしており、書き込みのロックを行わずにインデックスを作成することが可能。
ただし、これはサービスのダウンタイムが回避できる代わりに、通常よりも処理に時間がかかり、負荷も上がることが考えられる。
またここでは CREATE INDEX CONCURRENTLY
が実際どのようにワークするのかといったことには言及しない (知らん)ので、そういった所に興味のある方はこちらのような記事を読むのが良いと思います。
Rails のマイグレーションで実行する
こんな感じで指定できる:
class AddIndexToUsersActive < ActiveRecord::Migration disable_ddl_transaction! def change add_index :users, :active, algorithm: :concurrently end end
disable_ddl_transaction!
はCREATE INDEX CONCURRENTLY
したい場合は必須。すべてのマイグレーションはデフォルトでトランザクション下で実行されるようになっているので、それを無効にするためのもの。
またこれは指定したファイルでのみ適用され、他のマイグレーションは各自のトランザクションで実行される。
そして PostgreSQL のCREATE INDEX CONCURRENTLY
に該当するのが algorithm: :concurrently
の部分。これを指定しておくことで今回期待するインデックス処理が実行されるようになる。
まとめ
- Rails ではマイグレーションで
disable_ddl_transaction!
とalgorithm: :concurrently
を指定するだけで容易に PostgreSQL のCREATE INDEX CONCURRENTLY
を実行することができる - ダウンタイムを避けることができる代わりに通常よりも処理に時間がかかる、負荷が上がるなどのデメリットもあると思うのでメリット、デメリットをちゃんと考慮して使ったほうが良いかも
参照
プロセス置換 (Process Substitution)について
bash や zsh にはプロセス置換 (Process Substitution)という機能があるのだが、あまり使うことがなく頻繁に忘れるのでメモ。
プロセス置換 (Process Substitution)とは
コマンドの結果をファイルのように扱うことができる機能。以下のように書く。
<(list)
- リストの結果を入力ファイルに置き換える
>(list)
- リストの結果を出力ファイルに置き換える
これはリストの結果をもう一方のプロセスに送るために/dev/fd/<n>
のファイルディスクリプタを使っているっぽい。
使用例
例えば comm
というコマンドがある。これは comm file1.txt file2.txt
のようにしてファイル 1 と 2 を良い感じに比較したりできるが、プロセス置換を使えばファイルを用意せず使用することができる。
$ comm <(ls -l) <(ls -la) total 0 -rw-r--r--@ 1 username staff 0 Jun 6 17:29 file1.txt drwxr-xr-x 3 username staff 96 May 18 2019 tmp total 16 drwx------+ 6 username staff 192 Jun 6 17:29 . drwxr-xr-x+ 132 username staff 4224 Jun 6 16:25 .. -rw-r--r--@ 1 username staff 0 Jun 6 17:29 .secret.txt -rw-r--r--@ 1 username staff 0 Jun 6 17:29 file1.txt drwxr-xr-x 3 username staff 96 May 18 2019 tmp
またディレクトリ間の差分を見たい場合など、diff
コマンドをよく使うがこれも以下のようにプロセス置換を使うことができてよい。
$ diff <(ls $first_directory) <(ls $second_directory)
他にも例えば、k8s のドキュメントでも bash や zsh の自動補完の項で echo 'source <(kubectl completion bash)' >>~/.bash_profile
のようにプロセス置換を使っていたり。
まとめ
ファイルしか受け付けないようなコマンドでもプロセス置換を使えば一時ファイルを作成する必要がないので便利。>(list)
の方は使い道あまり思いつかなかった w
参照
英語で1on1ミーティングができるようになるまでにやったこと
今、自分の所属する会社では外国籍のエンジニアも働いていて、彼らとは普段英語でコミュニケーションを取っています。
具体的には Slack 上でのチャットや彼らが参加するミーティング、1on1 などを英語で行っていて、全く流暢ではないですが、現状問題なくコミュニケーションは取れていると思います。
初めて海外からエンジニアをチームに迎え入れたのが約 1 年半前。当初から分かっていたことではあったけど、特にリスニングとスピーキングが大きな課題となり、ミスコミュニケーションによって開発に支障をきたすようなケースもありました。
ただ色々と試行錯誤を繰り返し、今日なんとか 30 分~1 時間程度の 1on1 ミーティングぐらいであれば英語でもこなせるようになりました。
今回はこれまでどのようにして自分が英語を勉強してきたかをざっくり書いてみたいと思います。「TOEIC900 点です」みたいな定量的な実績も残していないし、誰かの参考になるかは分からないけど「英語、意外となんとかなるよ」的なメッセージを少しでも伝えられたらと思います。
はじめに
対象読者と当初の自分の英語力は以下のような感じ。
対象読者
- 仕事で英語が必要な人、もしくはこれから積極的に使っていきたい人
- これはあくまでエンジニア同士の会話のように、ある程度コンテキストが共有されている前提でのコミュニケーションを想定している (必要最低限の英単語や言い回しさえ知っていれば、それなりに会話ができるような状況)
- 海外ドラマや海外映画のようなレベルの英会話を達成するには今回書いているような勉強量では不十分だと思います、すみません
当初の英語力
- 留学経験とか一切なし
- TOEIC スコアは多分 550 ぐらい
- 受験したのは 5 年以上前、院試で必要だっただけ
- 英語のドキュメントは必要であれば読んでた
- Git のコミットメッセージやコードコメント、テストの Description 程度は英語で書いてた
- 英語を話すこと自体に特に抵抗はなかった
- 恥ずかしくて話せないみたいなメンタリティではなかった
準備編
ここからは海外から同僚がジョインするまでの数ヶ月 (Visa の発行とかあるので)でどのような英語対策を行ったかという話。
文法の復習
「日本人は英語を話すことに抵抗がある、間違ってもいいからまずは話せ」みたいな意見をよく聞く一方で、「文法を捨てて英会話に特化して勉強していると、いずれ成長が鈍化する」みたいな記事をどこかで見た気がして、まずは中高 6 年で習ったであろう文法をざっくり復習してみることにした。
選んだ本はこちら。
この本は英文法について「ネイティブがどのような感覚でそれを使っているか」みたいな視点で解説されているということで選んだのだが、当たりだった。受験で高得点を取るためではなく、純粋に英語を話せるようになるための本だった。
例文も This is a pen. みたいな何の役にも立たなそうなものではなく、もっと日常で出てきそうなものばかりで大変参考になった。
ただしこの本、辞書みたいに分厚くて何度も繰り返し読むのは非常にツラいので、2 週間ぐらいかけて読みながら各文法の要点を Evernote にまとめていき、エビングハウスの忘却曲線に沿った周期でそのノートを何度もチェックするようにした。
英語上達完全マップとの出会い
次に色んな人の英語勉強法を検索する中で、英語上達完全マップというサイトを発見した。
自分も今まで受験向けの英語教育しか受けてこず、実際に英語を使う (特にスピーキング)経験が圧倒的に不足しているという所に課題感を持っていたので、ここで書かれていた内容は腑に落ちた。
自分はこちらに書かれている、音読パッケージと瞬間英作文を重点的にやっていこうと決めた。文法に関しては、1 億人の英文法ですでにちょっと復習していたので、それ以上は特に何もしていない。
音読パッケージ
まずは音読パッケージの本を 1 冊買って、それを何度も繰り返すことにした。
選んだ本はこちら。
上記の完全マップの人が書いた本だったので、まあ間違いないだろうという感じで選んだ。学習方法は完全マップや本の中で説明されているので、素直にそれに従った。
毎朝、会社に行く前の 2~30 分で 1 セクション進めるようにして、それを 5 周ぐらいは続けたと思う。ただ実際はそんなストイックなものではなく、だるい日や少し寝坊した日は本能に従って休んだりも結構した。
そのうち丸暗記に近い感じになって飽きてきたので、別のシリーズも買って、同じように進めた。今でも継続している。
瞬間英作文
音読パッケージと合わせて瞬間英作文のトレーニングもスタートした。
選んだ本はこちら。
こちらも受験英語的な例文ではなく、より生の会話に近いような例文が多い印象だったので選んだ。進め方はこれまた上述の完全マップを完全に参考にした。
家ではすでに音読パッケージの学習をしていたので、気分を変えたいなと思い、こちらは通勤時、駅まで歩く間とかジョギングしながらとか、動きながら進めるようにした。周囲に人がいない時はちょっと声に出して発音の練習をしたりもした。
これは多分日本人に一番足りていないスキルで、ストレスなく英語が出てくるようになるまでかなり苦労したので 10 周ぐらいした。今は忘れてきたなという頃にたまに復習する程度で、以下の本あたりを検討中。
実践編
音読パッケージと瞬間英作文を 1 周終えたぐらいの時期、Visa を無事取得した同僚がジョインして、実践形式での英語コミュニケーションがスタートした。
日々の Slack の開発チャンネルや GitHub でのレビューなどがすべて英語と化し、開発環境の構築を英語でサポートするなど、「英語話せないとガチでヤバい」という状況が整った。
正直、これが一番最高なトレーニングであることは間違いないので、オンライン英会話を始めるとか英語のミートアップに参加するとか、何かしら実践できる環境を見つけることが上達への近道だと思う。
読み書きのためのツール
英語を読み書きする機会が増えてからは、以下のツールにはとてもお世話になっている。
-
- 精度も日々向上しているし、もはや「メインエディタかよ」ってぐらい使っている
- 自分はいつも英文は Google 翻訳上で書いていて、自分の書いた英文の日本語訳が意味不明じゃなければコピーアンドペーストみたいな感じで、バリデーション用途で使用している
- Slack のようなチャットでのコミュニケーションだと、さすがにそんな悠長なことやってられないので時と場合による
発音
実際に英語で話す中でたまに発音が思ったように通じないことがあったため、発音も改めて勉強してみることにした。
発音の勉強にはこちらのサイトを参考にした。
タイトルにもある通り、3 時間でざっと学習できるのでオススメ。これは 3 周ぐらいやって、あとはたまに自分の発音が通じなかった時とかに復習している。
あと、これも日本語の弊害で例えば genre という単語は日本語で「ジャンル」と読みがちだが、実際は「ジョンラ」みたいな発音だったり、既存の日本語知識が邪魔をしてくるケースもあるので、新しく英単語を覚える時は必ず発音やアクセントを確認したほうが良いと思う。
よく英語ペラペラの日本人が英単語の部分だけ英語ネイティブっぽく発音しているのをイキってるだけだと思っていたけど、そういう言語間の発音の違いを吸収するのが大変だっただけなのかな?なんて思ったりした。
英語の Podcast を聴く
音読パッケージと瞬間英作文をそれぞれ 2~3 周ぐらい終え、ちょっと実践的な英語にも慣れてきたタイミングで英語の Podcast をたまに聴くようになった。
自分はソフトウェアエンジニアなので、テック系の Podcast の中で面白そうなもの、そして中でも興味のあるトピックの回を何度も繰り返し聴いたりした。日本語で聴いてもよくわからないようなものは絶対に英語でも理解できないので、自分が使い慣れた技術について語っているようなものがオススメ。
元々よく聞いていたRebuild.fmの英語回やThe Changelog, GCP の Podcast などを好んで聴いた。
一言一句完璧に聴き取るのは基本的に不可能だし、聴き取れない部分は何度聴いても聴き取れなかったりするので、文字起こしがある (もしくは購入できる)ものを選ぶのがオススメ。聴き取れなかった箇所を確認しながら聴くのが良いと思う。
会話から学んだ英単語や言い回しをストックする
実際に日常で飛び交った英単語を都度調べて単語帳にストックするようにした。自分はWeblioを使っていて「同僚が使っていて自分が知らなかった単語を調べる -> 単語帳に登録 -> 夜寝る前とか暇な時に見返す」みたいにして覚えている。
これは学校の単語テストの前に頑張って暗記した時のような雑な記憶ではなく、実際に単語が使用されたシチュエーションとともにエピソード記憶として残りやすいので結構定着した。
仕事でエンジニアリングについての議論をする程度であれば、例えば loose coupling とか technical debt みたいな技術的な単語は一度使ったら結構忘れないものだし、It makes sense! みたいなよく使う言い回しとかも何度か使っていれば英語として自然に出てくるようになった。
If I were a bird, ... みたいなメルヘンチックな言い回しとか業務内でめったに使わないだろうし、シンプルで便利な言い回しだけ覚えておけばコミュニケーションは十分取れると思う。
Twitter で著名な海外のエンジニアのツイートをチェックする
Twitter のリスト機能を使って有名な OSS のコミッターなど、著名な海外のエンジニアの方を追加しまくって、英語の技術情報だけが流れてくるタイムラインを用意した。
1 投稿あたりの文字数が少ないので、洋書を読むとかよりも格段にハードルが低く、フランクなネイティブらしい言い回しなども目にすることができるので良かった。
どちらかというと英語の勉強というよりは情報収集のために見ている感じ。英語のリーディングに関しては普段ドキュメントなどを読む機会も多いので、あまり力は入れていない。
英語でブログを書いてみる
すでにコードレビューやチャットで英語のライティングの機会はあったが、よりアウトプットの場を増やすべく英語のテックブログも始めた。わりと海外で使われていそうなdev.toを利用した。
ただ超大作みたいな記事は心が折れて書けないので、簡単な技術メモ程度の記事をたまに書くようにしている。
まとめ
色々と書いてきたけど、結局英語上達するには以下のことに尽きるかなという気がしました。
- 日々の習慣の中に英語学習を組み込んで、地道に反復練習を継続することが重要
- モチベーションを継続するために「英語ができないとやばい」環境に身を置いたり、英語力を向上する目的 (アメリカ人の恋人がほしいとか)を明確化するのが重要
- 実際に英語を使うことが最も重要
「いや英語使う環境なんてねーよ」というエンジニアの方、Twitterとかでご連絡いただければ自分の会社のことも含め、色々とお話できるのでお気軽にどうぞ。
この記事が何かしらの参考になれば幸いです。
参照
factory_botでactive_model_serializers用のPORO (Plain-Old Ruby Object)のテストデータを作成する
最近、DB の存在しない Rails プロジェクト下で API を作る機会がありました。
外部から fetch してきたデータから PORO (Plain-Old Ruby Object)を作って、 active_model_serializersでひたすらシリアライズしまくるみたいな感じです。 特にテストまわりで若干の工夫が必要だったので、その時のメモです。
環境
- (なぜか) Rails 6.0.0.beta3
- API モード
- ActiveRecord 使わない
- Rspec
- active_model_serializers 0.10.9
- factory_bot 5.0.2
現時点でまだ本リリースされていない Rails 6 を無駄に使っている。
構成
- app/models
- PORO を定義していく
- app/serializers
- シリアライザを定義していく
- app/controllers
- エンドポイントを定義していく
モデル
サンプルとして URL とサイズの情報を持つImage
モデルを定義してシリアライズするケースを考えてみる。
こちらに書かれているように active_model_serializers では PORO 用に ActiveModelSerializers::Model
を定義してくれているので、シンプルなものであればこれを使って簡単に実装できる。
よりファンキーな実装が求められる状況であっても、 こちらの仕様に沿った自前モデルさえ作れれば問題なくワークすると思う。
# app/models/image.rb class Image < ActiveModelSerializers::Model attributes :url, :size end # app/models/image/size.rb class Image::Size < ActiveModelSerializers::Model attributes :width, :height end
シリアライザ
# app/serializers/image_serializer.rb class ImageSerializer < ActiveModel::Serializer attributes :url, :type has_one :size end # app/serializers/image/size_serializer.rb class Image::SizeSerializer < ActiveModel::Serializer attributes :width, :height end
has_one
みたいにリレーションを定義しておくと、include オプションとかが良い感じに使える。
例えば、 render json: image, include: '*'
とするとsize
が含まれた JSON が返るし、 render json: image, include: ''
とすると size
が含まれない JSON が返る。
コントローラ
コントローラではインスタンスを作って、そいつをrender
メソッドに渡せば active_model_serializers が最適なシリアライザを見つけてシリアライズ -> JSON を返却してくれる。
実際のプロジェクトでは外部からデータを取ってくる実装 (下の fetch_data_somehow
の部分) が一番ツラかったのだが、本題ではないので割愛。
# app/controllers/v1/images_controller.rb module V1 class ImagesController < ApplicationController def show render json: image, include: params[:include] end private def image @image ||= Image.new(image_attrs) end def image_attrs @image_attrs ||= fetch_data_somehow end end end
テストまわり
ActiveRecord
のモデルのテストとかだと factory_bot
を使って
FactoryBot.define do factory :user do name { Faker::Name.name } end end
> user = FactoryBot.create(:user)
みたいにさくっとテストデータを用意できると思う。
ただ今回作成した PORO の場合、create
メソッドを呼んでも保存する場所がない。また、ActiveModelSerializers::Model
はActiveModel
をベースにしているのでnew
のタイミングで attributes
を渡してやらないと attributes = {}
がアサインされてしまって、そのままだと意図通りにシリアライズされない。
# spec/factories/images.rb FactoryBot.define do factory :image do url { Faker::Internet.url } size { build(:image_size) } end end # spec/factories/image/sizes.rb FactoryBot.define do factory :image_size, class: 'Image::Size' do width { rand(100..500) } height { rand(100..500) } end end
> image = FactoryBot.create(:image) => NoMethodError: undefined method `save!`... > image = FactoryBot.build(:image) > image.attributes => {} > image.to_json => "{}"
factory_bot はイニシャライザをオーバライドできる initialize_withや create
をスキップできる skip_createを提供してくれているので、これらをありがたく使わせてもらい解決できた。
# spec/factories/images.rb FactoryBot.define do factory :image do skip_create initalize_with { new(attributes) } url { Faker::Internet.url } size { build(:image_size) } end end # spec/factories/image/sizes.rb FactoryBot.define do factory :image_size, class: 'Image::Size' do skip_create initalize_with { new(attributes) } width { rand(100..500) } height { rand(100..500) } end end
> image = FactoryBot.create(:image) => #<Image:...> `build`と同じ結果になり、エラーは発生しない > image = FactoryBot.build(:image) > image.attributes => { "url" => ..., "size" => ... } > image.to_json => "{\"url\":...,\"size\":...}"
毎回initialize_with
やskip_create
を書くのはだるいので、最終的には以下のような DSL を用意してそいつを使うことにした。
# config/initializers/factory_bot.rb if defined?(FactoryBot) module FactoryBot module Syntax module Default class DSL # Custom DSL for ActiveModelSerializers::Model # Original: https://github.com/thoughtbot/factory_bot/blob/v5.0.2/lib/factory_bot/syntax/default.rb#L15-L26 def serializers_model_factory(name, options = {}, &block) factory = Factory.new(name, options) proxy = FactoryBot::DefinitionProxy.new(factory.definition) if block_given? proxy.instance_eval do skip_create initialize_with { new(attributes) } instance_eval(&block) end end FactoryBot.register_factory(factory) proxy.child_factories.each do |(child_name, child_options, child_block)| parent_factory = child_options.delete(:parent) || name serializers_model_factory(child_name, child_options.merge(parent: parent_factory), &child_block) end end end end end end end
# spec/factories/images.rb FactoryBot.define do serializers_model_factory :image do url { Faker::Internet.url } size { build(:image_size) } end end
あとは以下のようにテストで使える。
# spec/serializers/image_serializer_spec.rb require 'rails_helper' RSpec.describe ImageSerializer, type: :serializer do let(:resource) { ActiveModelSerializers::SerializableResource.new(model, options) } let(:model) { build(:image) } let(:options) { { include: '*' } } describe '#url' do subject { resource.serializable_hash[:url] } it { is_expected.to eq model.url } end ... end
まとめ
PORO かわいい。