Ubieのリモートワーク(テレワーク, WFH)を活用して、子育てが充実した話

みなさん、リモートワーク、テレワーク、WFH生活どうでしょうか?

この生活がどれだけ続くか分かりませんが、案外会社が回ることがわかったことですし、新型コロナウイルス問題収束したあかつきにはリモートでの仕事をすることができるような日々が来るかもしれませんね。

そんなご家庭にこんな働き方もありまっせ!ということで、決まった曜日にリモートをすることにより、共働きの家庭の未就学児に習い事をさせ、子育てを充実させてみたので、それについて書いてみます。

前提

Ubieの定義

Ubieには2020年3月現在、UbieとUbie AI Consulting(UAC)の2つの組織が存在しています。

f:id:shiraji:20200314195252p:plain

このエントリーでは私が所属している開発を主に担当しているDevチームの話になります。UbieのDevチームはコアタイムなしのフルフレックス制度を採用しています。

UACの詳細はきっと UAC代表 がどこかでしてくれると思います。楽しみにしていてください。

平日の子供の習い事

共働きで保育園に子供を通わせてるご家庭ならわかってくれるはず!と思うのですが、平日子供に習い事をさせるのはほぼほぼ無理です。平日の習い事は15時には帰宅する幼稚園に通っている子供向けに時間が設定されており、15:30~16:00くらいからスタートし、18:00までには終わる時間になっています。6時間勤務の時短を使っているご家庭は16:00~17:00くらいに退社することが多いはずです。その時間に会社から子供を迎えに行き、習い事のところまで届けるとしてもかなり厳しいです。私たち夫婦はフルタイムの共働きで、私がUbieに入社するまでは平日の習い事なんて考える余地がないくらい無縁なものでした。

リモート利用状況

Ubieには新型コロナウイルス問題以前から週2日のリモートワーク制度が存在しています。新型コロナウイルス問題以前の私のだいたいなリモート利用状況はこんな感じです。

  • 月曜日 基本出社。木、金に出勤すべき予定が入っていれば、リモート
  • 火曜日 リモート
  • 水曜日 会社の出社推奨日であるため、出社
  • 木曜日 予定が無ければリモート
  • 金曜日 木曜日がリモートできなければ、リモート

1年間このルールに則って出社したりリモートしたりしていました。少なくとも週1は必ずリモートでした。

それと半年経過して書いたエントリーにもありますが、出社時間は10:00-17:30にして、子供の送り迎えをし、タスク消費量が足らないので、その後は家で仕事をしています。

shiraji.hatenablog.com

水曜日

水曜日だけ特殊で、出社推奨日となっています。基本この日はリモートせずに対面でみんな集まろう!と決まっています。その水曜日はスプリントのレビュー、全社でOKRの進捗確認、全員でやる採用定例などなどを実施しています。

月、木、金

月曜日をリモートしてしまうと水、木、金と三日連続で満員電車に乗らないといけない状況に陥るので、基本的に木、金のどちらかをリモートするようにしていました。あと月曜に出社すると週が始まった感があってシャキーン!🤩としてその週のパフォーマンスが若干よくなってる印象があります。。。(多分気のせい)

火曜日

さて本題の火曜日のお話です。

習い事に通わせる経緯

そもそもの発端は4歳児の運動会の時でした。私の息子は他の子たちと比べ、明らかに運動が出来ない子という感じで各競技をこなしていました。別に苦手なら苦手で良いのですが、息子は運動することがどちらかと言うとかなり好きな方であり、息子自身も何か歯痒いそうな感じが見て取れました。納得が出来ず妻と2人で振り返りをしました。そこで気づいたのが

  • 私と妻は共に保育園や学生時代は早く家に帰ってくることができる子供で、帰ってきたら外で遊ぶ生活をしていた
  • そのため、子供とは親が何もせずとも普通に運動ができるものだと言う認識だった
  • 息子は家に帰ってくるのが19:30過ぎ。平日運動する機会が他の子供たちと比べ少なく、運動する機会を提供できていない

完全に私たち親の問題だと気づきました。そこで運動ができる習い事をさせてあげようと探し始めました。ただ4歳児ともなると習い事をする子供が多く、特に保育園児が通う事ができる土日の習い事は半年待ち(ただし半年で入れるとは言ってない)状態で、空いていたのが、火曜日の15:30~17:30までやっている運動教室でした。

当時まだUbieに入社して数ヶ月でリモートもあまり積極的に活用していませんでした。しかし、この習い事に通わせるため、数ヶ月週2リモート+フレックス生活を試し、自身のパフォーマンスを落とさずに仕事ができることを確認してから、息子をその習い事に通わせることにしました。

初日にその習い事のお試し体験をさせてもらいました。あの時の息子の笑顔や楽しそうな大きな声は1年経った今でも鮮明に覚えています。本当に心から運動することを楽しんでいる息子を見て、ものすごく嬉しかったし、同時になんでもっと早く気づいてやれなかったのか?と後悔もしました。何がなんでもこの生活を回していくぞと決めた日でもありました。

スケジュール

ざっくりとした火曜日のスケジュールはこんな感じです。

f:id:shiraji:20200314030058p:plain
火曜日のタイムテーブル

夜は21時くらいまでは仕事してますが、子供の習い事のときの自分のパフォーマンスが悪かったら、もう少し遅くまで仕事してますし、そのスプリントで進捗良ければ、20時頃には切り上げたりしています。(冷静に考えて奥さん仕事し過ぎでは?🤔)

火曜日午後の会議

順調にリモートしつつ、習い事に行かせる生活を送っていたのですが、7月頃に大きな問題が発生しました。その時まで実は私と大半がリモートの業務委託の人たちだけのチームに所属していたため、同期的なコミュニケーションを取らずにリモートで仕事をすることができていました。しかし、7月頃にオフィスでスクラムをバリバリこなしているチームへ移籍となり、同期的なコミュニケーションが必要になりました。スクラムの各種セレモニーにも参加する必要があります。特にこのチームに移籍することの問題だったのが火曜日の午後に実施していたリファインメントでした。リファインメントは重要なスクラムセレモニーの一つで一回出ることが出来ないと次以降のスプリントのキャッチアップすることが厳しくなります。このタイミングで子供の習い事を辞めさせることを検討すべきかもしれませんが、あの笑顔を見た後にこの習い事をやめさせることは私の中に選択肢としてありませんでした。そこで、チームにリファインメントを午前中に変更してもらえないか?と話したところ、こんなプライベートな理由だったのですが、快くリファインメントの時間を変更してくれました。

Ubieの本当に素晴らしいところは、チームメンバーのパフォーマンスを最大限引き出せる環境をみんなが真剣に考えて作ってくれる事です。この件に関して本当に感謝する事しか出来ません。ありがとうございます。この判断をしてくれたことにより、パフォーマンスを落とさず、習い事に通わせつつ、スクラムの一員になることが出来ました。

その後また別のチームに参加することになったのですが、火曜日の午後は基本会議を入れないように配慮してもらっています。時間がどうしても合わず、今までに2回この時間に会議が入ったのですが、教室からリモートで参加をさせてもらいました。子供や周りの声が入り、聞こえづらい時があったのでノイズキャンセリングのヘッドフォンが必要だなーという感じでした。私が頻繁に発言するような会議がもしその時間に組まれた場合は子供たちの声を拾ってしまうことと、周りのママさんたちに迷惑になりそうなので、教室の外に行った方が良いかもしれないなーと考えています。

結果

Ubieやチームのサポートのおかげで、保育園の送迎など子育てを中心にした仕事のスケジュールを組むことができるようになっています。

4歳児の時はとにかく何事もなく終わらせることしか頭になかったようですが、5歳児の運動会のダンスでは一番難しい技を出すグループに自ら手を上げて参加し、本番で成功させていました。走り方もすごく綺麗なフォームでしっかり走れていました。祖父母も明らかによくなったと喜んでいました。本人もやれることが増えて自信が顔に出ていました。保育園の先生からいろんなことを自ら積極的にやれるようになったと聞いています。

卒園式では、園の毎年恒例である式の最後に、「園児が保護者のうち1人を呼び、感謝され、いろんな人に写真撮られつつ一緒に退室していく」という保護者の晴れ舞台を作るための時間がありました。普通その場で呼ばれるのは子育てに多くの時間を割いている「ママ」です。今年もクラスの大半が「ママ」を選択しました。しかしうちの息子は練習の時から「パパ」一択だったそうで、他の子に何を言われようが、先生に何を言われようが、ママ・パパに何を言われようが「パパ」にすると言っていました。息子は私が子育てに参加してることをしっかり認識してくれてることが本当に嬉しかったです。

念のため明記しておきますが、妻も世間一般のママと同じかそれ以上にしっかりと子育てに参加してます。私と同様に息子に愛情を注いでいる素晴らしいママです。

今後

卒園式と書いてある通り、4月から息子は小学生になります。また生活がガラッと変わりそうですが、Ubieは柔軟に働くことができる環境なので、問題が発生したら都度チームに相談して解決していこうと思います。

まとめ

リモートワーク、テレワーク、WFHも良いけど、フルフレックスのUbieならうまく時間を合わせられたら、子育て充実させられるよ!

さて、そのUbieですが、現在絶賛採用募集中です。Webエンジニア、QAエンジニア、SRE、MLエンジニア、データエンジニア全ポジション募集中です。どのポジションも事業成長スピードと比較して全然人が足らないので、興味のある方はぜひ

www.figma.com

ubie.life

また、このエントリーでは詳細説明しておりませんが、UACと言う組織が現在立ち上がりのフェーズに入っています。こちらも全然人が足らない!マーケティングやセールスが武器だ!と言える人ぜひ検討してください!!!

speakerdeck.com

IntelliJプラグインでAnnotate機能を作った

はじめに

これは Ubie Advent Calendar 2019 - Qiita の21日目の記事です。

想定読者

(開発方法セクションより前)

(開発方法セクション以降)

本題

まずはこれを見て欲しい。

https://raw.githubusercontent.com/shiraji/find-pull-request/master/website/images/list-pr.gif

以前から欲しいなーって思っていたのだけど、プライベートの時間の捻出が出来ず、主に眠気に勝てないのが原因で、なかなか腰が上がらなかったんですが、息子くんがすんなり寝てくれて2時間くらい時間が出来たので、さくっと作ってみた。その後Twitterに成果報告して寝た。

翌朝起きたら中々の反応がもらえてたので、ちょっとやる気が出て、そのまま勢いで作り切りました。余談ですが、自分の場合、こんな機能作った!ってツイートした場合、反応あるとすごくモチベーションが上がるので、欲しいな!って思ったらぜひいいねしてもらえると!

これ単体でプラグインとして出しても良いのですが、切り出すメリットがあまり見えなかったため、Find Pull Request プラグインのv1.7.0の機能として出しています。Repositoryはこちら

この機能により、Find pull requestプラグインは3つの機能を持つことになりました。

  1. 右クリックでpull requestページに飛ぶ
  2. 右クリックでpull requestページのURLをコピーする
  3. pull requestの一覧をエディタに記載する

まだまだ他にも欲しい機能があるので、ちょくちょく作っていこうと思います。他にも欲しい機能あればぜひissueにあげてください。ちなみにGitHub, GitHub Enterprise, GitLab, Bitbucketで動作します。他にも欲しいサービスあれば、issueテンプレート埋めていただければ対応します。

開発方法

せっかく作ったので、調べた知見を置いておきます。

ここから想定読者は 「IntelliJプラグインをがっつり開発したことがある・Annotate機能を作りたい人」とします。つまり基本的なプラグイン開発の説明は省略します。

参考ソース

普段プラグイン開発するなら他の似たようなプラグインソースコード読め。で終わるのですが、アイコンつけるのはいくつかありましたが、Annotate機能を使ったプラグインは誰も作ったことがないのではないか?と言うくらい見つけることが出来ませんでした。

その為、みなさんお馴染みのintellij-communityのrepoから仕様を把握することにしました。

github.com

その中でも唯一Annotate機能を提供している、 AnnotateToggleAction を見て開発を行ないました。

https://github.com/JetBrains/intellij-community/blob/master/platform/vcs-impl/src/com/intellij/openapi/vcs/actions/AnnotateToggleAction.java

Find pull requestプラグインは開発当時2019.1をサポートしているので、masterブランチではなく、2019.1のソースを見て開発していました。後述するけど、これが後に問題になります。

Annotateを出すには

色々読んだ結果、Annotateを出すには editor.gutter.registerTextAnnotation() を呼び出すことで実装が可能のようです。

  /**
   * Adds a provider for drawing custom text annotations in the editor gutter, with the
   * possibility to execute an action when the annotation is clicked.
   *
   * @param provider the provider instance.
   * @param action the action to execute when the annotation is clicked.
   */
  void registerTextAnnotation(@NotNull TextAnnotationGutterProvider provider, @NotNull EditorGutterAction action);

引数は二つ、一つ目の TextAnnotationGutterProvider はどんなテキストを出すのか?もう一つの EditorGutterAction はそのテキストに対してクリックした時どんなアクションを起こすのか?です。クリックなどのアクションが必要ない場合、二つめの引数は省略可です。

TextAnnotationGutterProvider

TextAnnotationGutterProviderで一番重要なメソッドはこの getLineText メソッドです。

/**
   * Returns the text which should be drawn for the line with the specified number in the specified editor.
   *
   * @param line   the line for which the text is requested.
   * @param editor the editor in which the text will be drawn.
   * @return the text to draw, or null if no text should be drawn.
   */
  @Nullable
  String getLineText(int line, Editor editor);

このメソッドは引数に行番号と表示するEditorが渡されます。戻り値にはその行に表示するStringです。

簡単なメソッドなのですが、一つ問題があります。それはこのメソッドが呼び出されるタイミングはAnnotateがレンダーされ得るたびと言うことです。

その何が問題なのかを説明する前に、Annotateの機能についておさらいします。AnnotateとはEditorの左横の部分を右クリックし、Annotateを選択したら、その行の最後のコミット(正確には少し違う)を表示します。

f:id:shiraji:20191215165849p:plain
Annotate

ここまではよく知られています。問題はエディタでの表示と言うことはAnnotate表示中も開発者がコードの更新をすることが可能です。つまりコード編集してもAnnotateを閉じず、正しい行に正しい情報を表示する必要があるということです。

f:id:shiraji:20191215170639g:plain
コードの変更・改行・行削除などにも対応する必要がある

registerTextAnnotation するのはアクションがcheckになった時のみです。そのため、registerTextAnnotation された時と getLineText が呼び出された時の表示されているエディタの状態は異なる可能性があることになります。

これを踏まえて、 getLineText の実装をします。最初に問題になるのが、 line の意味です。the line for which the text is requested. と記述されていますが、説明が圧倒的に足りていません。というか、説明通りに受け取ってはいけません。この lineAnnotate が表示された時の行番号に当たります。今まさにEditorで表示されている行番号ではありません。受け取った line をそのまま利用するのは危険です。 line が今表示されているエディターの行番号としてどこに当たるのかを確認し、その確認した行番号に対して情報を返さないといけません。(ここを読むような訓練されたプラグイン開発者なら「あーよくあるやつね」となるいつものやつです。)

line が今表示されているエディターの行番号のどこに当たるのか」は UpToDateLineNumberProviderImpl を利用して取得します。

        val upToDateLineNumberProvider = UpToDateLineNumberProviderImpl(editor?.document, editor?.project)
        val currentLine = upToDateLineNumberProvider.getLineNumber(line)

UpToDateLineNumberProviderImpl#getLineNumber にはドキュメントはありませんが、0未満の時には現在表示されているエディタにはその行は存在していないことを表しています。そのため今回は、0未満の値だった場合、表示されることはないため、空文字即returnにしてあります。

ここが呼び出される回数は思った以上に頻度が高いです。しかも各行呼び出されます。このプラグインではgitのハッシュ値からpull requestの番号を取得しますが、都度その処理を行うことは危険です。そのため、ハッシュ値とPR番号のマッピングregisterTextAnnotation する前に計算しておき、このメソッド内ではそのマッピングから表示する文字列を生成する簡単な処理に留めてあります。

EditorGutterAction

EditorGutterAction重要なのがdoAnnotateメソッドです。

/**
   * Processes the click on the specified line.
   *
   * @param lineNum the number of line in the document the annotation for which was clicked.
   */
  void doAction(int lineNum);

このメソッドのlineNumは現在表示されている行番号になります。 UpToDateLineNumberProviderImpl は利用する必要はありません。Editorは受け取れないことに注意してください。

TextAnnotationGutterProvider と同じようなデータやロジックを使います。そのため、今回の機能では、一つのクラスで二つとも実装することにしています。

FileAnnotation

AnnotateのようにFileAnnotationに依存するような情報を表示する場合、注意しなくてはならないのが、IntelliJのエディタ以外の部分からもファイルの変更が可能になる点です。何を言っているのかわからないと思います。例えばですが、ファイルの変更をして、コンソール上でコミットをするようなケースを考えてください。エディタの変更だけではなく、IntelliJ外部からのFileの変更(FileAnnotationの変更)などを検知し最新の情報を表示する必要があります。ちなみにAnnotateを開いたまま、改行し、コンソールでコミットしてみてください。Annotateが自動で閉じます。今回の機能もFileAnnotationに依存しているため、その変更を検知する必要があります。あまり深く仕様を考えるのは大変であるため、Annotateと同じようにFileAnnotationの変更が発生した場合、Annotateを閉じるようにします。

FileAnnotationの変更を検知するメソッドは二つあります。

  /**
   * Notify that annotations should be closed
   */
  public synchronized final void close() {
    myIsClosed = true;
    if (myCloser != null) {
      myCloser.run();

      myCloser = null;
      myReloader = null;
    }
  }

  /**
   * Notify that annotation information has changed, and should be updated.
   * If `this` is visible, hide it and show new one instead.
   * If `this` is not visible, do nothing.
   *
   * @param newFileAnnotation annotations to be shown or `null` to load annotations again
   */
  @CalledInAwt
  public synchronized final void reload(@Nullable FileAnnotation newFileAnnotation) {
    if (myReloader != null) myReloader.consume(newFileAnnotation);
  }

  /**
   * @see #close()
   */
  public synchronized final void setCloser(@NotNull Runnable closer) {
    if (myIsClosed) return;
    myCloser = closer;
  }

  /**
   * @see #reload(FileAnnotation)
   */
  public synchronized final void setReloader(@Nullable Consumer<? super FileAnnotation> reloader) {
    if (myIsClosed) return;
    myReloader = reloader;
  }

上記のようにコンソールでコミットする場合、FileAnnotateのcloseが呼び出されるため、setCloserメソッド内でAnnotateを閉じる処理を記述します。

        fileAnnotation.setCloser {
            UIUtil.invokeLaterIfNeeded {
                if (!project.isDisposed) editor.gutter.closeAllAnnotations()
            }
        }

同じようにsetReloaderメソッドを実装しようとしたのですが、思いつく限りの行動をしても、残念ながらreloadメソッドが呼び出されることがありませんでした。そのため、setReloaderの実装することができませんでした。AnnotationToggleActionから引数のFileAnnotationが最新のFileAnnotationのようですが、ここは謎です。。。誰か教えて。

2019.1 vs 2019.2

さてここまで実装して、よっしゃリリースだ!って思ったのですが、 closeAllAnnotations メソッドが気になっていました。このメソッド呼び出されるとAnnotateの部分に表示されている情報全てを閉じます。つまり、Annotateを表示し、List pull requestを表示し、 close annotationを選択するとAnnotateとList pull requestが両方とも閉じてしまします。片方だけ閉じることが出来ません。

f:id:shiraji:20191220231636g:plain

2019.1の場合、個々のAnnotateを閉じる方法が提供されておらず、さらに設定されているTextAnnotationの取得が行えません。残念だなーと思っていたのですが、ふとした拍子にこんなissueを見つけてしまいました。

Annotate action behave wrong if other annotations added https://youtrack.jetbrains.com/issue/IDEA-209722

このissueによると、192.2300から個々のAnnotateを閉じることができるようになったみたいです。つまり2019.1では出来ないけど、2019.2では出来ると。2019.1のソースコードばかり追っていたため、最新のコードでは出来る事を出来ないと諦めてしまっていました。

古いバージョンに引っ張られて、しょぼい機能を提供するのはなんだかなーって思ったので、2019.2での開発に切り替えることにしました。

ちなみにこのissueにはこんなことが書かれています。

We've run into that in the students' project, but it also can be actual for 3rd-party plugins developers.

要約すると「学生のプロジェクトやってる時に発覚した。3rdパーティのプラグイン開発者にも発生する可能性あるよ。」とのことです。3rdパーティのプラグイン開発者でAnnotate作って公開まで持っていったの、ひょっとすると相当少ないようです。どうりで似たようなプラグインが見つからない訳ですね・・・

2019.2対応

2019.2(ここでは192.5728以上とします)では新たに以下の二つのメソッドが提供されました。

  @NotNull
  List<TextAnnotationGutterProvider> getTextAnnotations();

  void closeTextAnnotations(@NotNull Collection<? extends TextAnnotationGutterProvider> annotations);

getTextAnnotations はそのEditorに設定されている全てのTextAnnotationsを取得し、 closeTextAnnotations は引数に渡された複数のTextAnnotationを閉じるメソッドです。

これを使えばOK!ということでさくっと実装してみました。上記の問題が解消されると思ったのですが、Exceptionが出ます。

java.lang.Throwable: Synchronous execution on EDT: /usr/bin/git -c ...
    at com.intellij.openapi.diagnostic.Logger.error(Logger.java:145)
    at com.intellij.execution.process.OSProcessHandler.checkEdtAndReadAction(OSProcessHandler.java:117)
    at com.intellij.execution.process.OSProcessHandler.waitFor(OSProcessHandler.java:62)
    at git4idea.commands.GitTextHandler.waitForProcess(GitTextHandler.java:145)
    at git4idea.commands.GitHandler.runInCurrentThread(GitHandler.java:383)
    at git4idea.commands.GitImplBase.doRun(GitImplBase.java:172)
    at git4idea.commands.GitImplBase.run(GitImplBase.java:148)
    at git4idea.commands.GitImplBase.runCommand(GitImplBase.java:46)
    at git4idea.commands.GitImpl.runCommand(GitImpl.java:41)

これは何かというと、UIスレッド時にgitのような外部コマンド叩くとエラーになるということです。IntelliJ 2019.2からこのような仕様に変わったようです。

Find Pull Requestプラグインは各所でgitコマンドを叩いているため、これの対応が必要になります。

外部コマンドを叩く場合、一番簡単な対処方法はバックグラウンドスレッドでの処理にしてしまうことです。バックグラウンドスレッドでの実行方法はいくつかあるのですが、簡単な方法として、Task.Backgroundableを使ってみます。

        object : Task.Backgroundable(project, "Listing Pull Request...") {
            override fun run(indicator: ProgressIndicator) {
                // 外部コマンドを叩くなどの処理+その後続処理
            }
        }.queue()

queue()の呼び出しを忘れがちになるので、注意してください。

さぁ対応終わったぞ!って思ったのですが、今度は別のエラーが出ます。

java.lang.Throwable: Read access is allowed from event dispatch thread or inside read-action only (see com.intellij.openapi.application.Application.runReadAction())
    at com.intellij.openapi.diagnostic.Logger.error(Logger.java:162)
    at com.intellij.openapi.application.impl.ApplicationImpl.assertReadAccessAllowed(ApplicationImpl.java:1027)
    at com.intellij.openapi.editor.impl.EditorImpl.assertReadAccess(EditorImpl.java:3254)
    at com.intellij.openapi.editor.impl.EditorImpl.getSettings(EditorImpl.java:920)

今度は何かっていうと、上でやったバックグラウンドスレッドはEditorへのRead権限がないため、あかんよってことです。上のバックグラウンド処理は必須であるため、read権限あるスレッドでEditorへのアクセスをするように修正します。

以下の公式ドキュメントにスレッド周りの話が書いてありますので、興味ある人は読んで下さい。 www.jetbrains.org

ドキュメントによると今回の場合は、invokeLaterを呼び出せば対処可能になります。つまりコードとしてはこんな感じに。

        object : Task.Backgroundable(project, "Listing Pull Request...") {
            override fun run(indicator: ProgressIndicator) {
                // 外部コマンドを叩くなどの処理+その後続処理
                ApplicationManager.getApplication().invokeLater {
                    // バックグラウンドスレッド内でeditorへアクセスするなどread権限が必要な処理を実行
                }
            }
        }.queue()

ようやく全てのコードがエラーなしで2019.2で動くようになりました。ほっと一安心。

さぁリリースです。

はいリリース後にユーザが多い、Android Studioの最新版3.5系(2019.1ベース)のサポートを勢い余って切ったことが発覚しました。最近Android開発してなかったし、完全に忘れてました。。。

複数バージョン対応

かと言って、2019.1のサポートに戻すと、問題解消されている2019.2のユーザにも問題ある挙動を強制する事になってしまいます。そこで、複数のバージョンのプラグインをリリースすることにしました。

  • 1.7.0 -> 2019.2以降のユーザ
  • 1.7.0-2019.1 -> 2019.1.xのユーザ(Android Studio含む)

対処方法は簡単で、plugin.xmlの idea-version のuntil-buildを設定し <idea-version since-build="191" until-build="192.5727"/>プラグインのバージョンを新しく 1.7.0-2019.1 として、リリースするだけです。(until-buildなどはplugin.xmlじゃなくて、gradle-intellij-pluginで設定可能になっているはずです。未検証) そうするとこんな感じで、IntelliJのバージョンによって、インストールできるバージョンが変わるようになります。

f:id:shiraji:20191221002340p:plain
複数バージョンがホストされる

まとめ

先日のUbieアドベントカレンダーLukas (@cvguy84) | Twitter さんのIntelliJプラグインの記事では

qiita.com

抽象的で考えるとIntelliJプラグインの開発は簡単です。

どこがやー!って思うのですが、

IntelliJプラグインは誰でも作ることができます。 Documentationを慎重に読んで、実装するしかないです。でもそれがOSS開発の一つの楽しみであると思います。

本当にこれです。ドキュメント(という名のソースコード)を読んで、うまく動いた時、すっごく楽しいし、それが人に使われると思うとやめられませんなーってなります。

ちなみに年内に出したいと思ったので、コピペの嵐でやり過ごしました。そのため、2019/12/21現在のrepositoryのコードは参考にしない方が良いです。。。時間とってがっつりフルスクラッチします。誰にも何も言われず、費用対効果ガン無視で思いっきり俺の考えた最強のコードがかけるのもOSSの醍醐味だと思います。

Spring+GraphQLからKtor+GraphQLに移行する

はじめに

これは Ubie Advent Calendar 2019 - Qiita の7日目の記事です。

6日目は かみな/Kaito Minatoya@Ubie (@kamina_zzz) | Twitter による 地球上の Kubernetes ユーザーは絶対使うべきツールたちを紹介するよ - Qiita でした。

想定読者

  • Spring + Kotlin + GraphQLを使っていて、Ktorに移行したくなっちゃった人 (需要🤔)

本題

UbieでAdvent CalendarをKtor縛りで書くぞ!となりました。じゃあ自分は何を書こうかなーって考えてたらGraphQLに決まってるじゃん?と振られたので、UbieではKtor+GraphQLは使ってないのですが、もしSpringのアプリケーションである https://github.com/ubie-inc/kotlin-graphql-sample をKtorに移し替えるとしたら何をするのかを調査しつつ、記事にすることにしました。

この記事では一旦動くところまでを目指します。

生かすコード、諦めるコード

フレームワークを載せ替えるとしてもそこまでガッツリ書き直す訳ではなく、生かすことが出来るコードが多々あります。今回は出来うる限りコードを生かす方向にしました。そのため、graphql-java-toolsを同様に採用します。

github.com

これを採用するため、domainquery resolver, resolver はSpringのアノテーションを消すだけで再利用出来ます。

import com.coxautodev.graphql.tools.GraphQLResolver
- import org.springframework.stereotype.Component

- @Component
class DiseaseResolver(...) : GraphQLResolver<Disease> {

jdbc も同様に修正するのですが、Springのjdbcを使っているため、思った以上にガッツリ修正が入ります。この辺りはKtorで利用するDBアクセスライブラリ次第になります。ここではあまり関係ないので、省略します。

スキーマファイルもSpringの時同様にresources直下に置きます。

再利用できるコードもありますが、完全に捨てるコードもあります。

例えば、JdbcConfigKotlinGraphQLSampleApplication これらはSpringのための設定なので再利用は出来ません。ただこのようなコードだけなのでほとんどのコードが再利用することが可能となります。

新規実装

IntelliJのウィザードで作成されるここから、新規の実装をしていきます。

fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)

@Suppress("unused") // Referenced in application.conf
@kotlin.jvm.JvmOverloads
fun Application.module(testing: Boolean = false) {
}

最初にRoutingの定義です。 GraphQLなので /graphql にコードを書いていきます。GraphQLは GETとPOSTのサポートをしているので、一応、両方とものAPIを書きます。

fun Application.module(testing: Boolean = false) {
    routing {
        get("/graphql") {
            TODO()
        }

        post("/graphql") {
            TODO()
        }
    }
}

最初にリクエストをパースします。リクエストはこんなJSONです。

{
    "query":"{\n  drug(id:\"id\") {\n    name\n  }\n}",
    "variables":null,
    "operationName":null
}

これをこのままdata classにします。

data class GraphQLRequest(
    val query: String = "",
    val operationName: String? = "",
    val variables: Map<String, Any>? = mapOf()
)

Ktorでデフォルトで使える、 ObjectMapper を使ってパース処理を書くと。

fun Application.module(testing: Boolean = false) {
    routing {
        get("/graphql") {
            val stringRequest = call.receive<String>()
            val mapper = ObjectMapper()
            val request = mapper.readValue(stringRequest, GraphQLRequest::class.java)
        }

        post("/graphql") {
            val stringRequest = call.receive<String>()
            val mapper = ObjectMapper()
            val request = mapper.readValue(stringRequest, GraphQLRequest::class.java)
        }
    }
}

何度も同じ処理を二箇所に書くのは辛いので、 GraphQLController を作成します。

class GraphQLController {
    suspend fun handleGraphQL(call: ApplicationCall) {
        val stringRequest = call.receive<String>()
        val mapper = ObjectMapper()
        val request = mapper.readValue(stringRequest, GraphQLRequest::class.java)
        TODO()
    }
}

これを使ってコード量減らします。

fun Application.module(testing: Boolean = false) {
    // Controller
    val graphQLController = GraphQLController()

    routing {
        get("/graphql") {
            graphQLController.handleGraphQL(call)
        }

        post("/graphql") {
            graphQLController.handleGraphQL(call)
        }
    }
}

さて次は実際に graphql-java を利用してコードです。

    val filePath = "kotlingraphqlsample.graphqls"
    val graphQLSchema = SchemaParserBuilder()
        .file(filePath)
        .resolvers(
            listOf(diseaseResolver, 
                DiseaseQueryResolver(diseaseService),
                // 他のResolverもここに記述する
                ))
        .build()
        .makeExecutableSchema()
    val graphQL = GraphQL.newGraphQL(graphQLSchema).build()

この graphQL インスタンスをController内で使い、それぞれのResolverにアクセスしていきます。graphQL インスタンスはDI使うなり、コンストラクタに差し込むなり、Controller内でインスタンス化するなり、好きなようにControllerに差し込みます。

    suspend fun handleGraphQL(call: ApplicationCall) {
        val stringRequest = call.receive<String>()
        val mapper = ObjectMapper()
        val request = mapper.readValue(stringRequest, GraphQLRequest::class.java)
        val context = ConcurrentHashMap<String, Any>()
        // graphQLは上でインスタンス化したもの
        val result = graphQL.execute(
            ExecutionInput.newExecutionInput()
                .query(request.query)
                .operationName(request.operationName)
                .variables(request.variables ?: emptyMap())
                .context(context)
        )
        TODO()
    }

最後にresultをJSONにして、返します。

    suspend fun handleGraphQL(call: ApplicationCall) {
        val stringRequest = call.receive<String>()
        val mapper = ObjectMapper()
        val request = mapper.readValue(stringRequest, GraphQLRequest::class.java)
        val context = ConcurrentHashMap<String, Any>()
        val result = graphQL.execute(
            ExecutionInput.newExecutionInput()
                .query(request.query)
                .operationName(request.operationName)
                .variables(request.variables ?: emptyMap())
                .context(context)
        )
        val json = mapper.writeValueAsString(result.toSpecification())
        call.respondText { json }
    }

これでAPIは完成。APIアクセスを簡単にするために、GraphiQLも導入します。 graphiql/packages/examples/graphiql-cdn at master · graphql/graphiql · GitHub この辺りのサンプルコードをそのまま拝借し、index.htmlとしてプロジェクト直下に置きます。

fun Application.module(testing: Boolean = false) {
    // Controller
    val graphQLController = GraphQLController()

    routing {
        static("/graphiql") {
            // GraphiQL
            default("index.html")
        }

        get("/graphql") {
            graphQLController.handleGraphQL(call)
        }

        post("/graphql") {
            graphQLController.handleGraphQL(call)
        }
    }
}

Runボタン押して、実際にGraphiQL http://localhost:8080/graphiql にアクセスしてみます。

f:id:shiraji:20191207224439p:plain

見えるようになりました。yay!

終わりに

いかがでしたでしょうか?(一回やってみたかった)

やってみて思ったんですが、Springですでに動いているのであれば、今のところKtorに移す必要ないなーと言うのが感想です。

今回はこんなレポジトリのコードを参考にしました。

あまりアクティブな開発がされていないので、今後どうなるのかわからないですが、もうちょっとKtorのGraphQL周りのプラグインが出来たら良いなーと。

自分で作れってことか 🙃

カナダから来たUbieのインターン生(Hiroくん)はすごかった

最初にこの記事はどの企業も個人も批判する目的はありません。もし批判を感じる文章があればご指摘して頂けると幸いです。


Hiroくんインターンを予定通り8月中旬まで無事に終わらせたので、彼がこの期間何をやったのか?などを書こうと思います。

f:id:shiraji:20190825222847p:plain
アイキャッチ画像のための無意味なスクショ

出会い

最初の彼との出会いは、Twitter上で、自分の経歴とほぼ同じ経歴を持っている学生がいるなー珍しいなーと思ったけど、関わりないだろうなーとスルーしたのを覚えています。

その数日後、彼のツイートは炎上していました。

朝ごはん食べてたら前見つけた子が炎上してるじゃん!あららーもっと先にフォローして、声かけちゃえば良かったかなー。と朝ごはん食べながら眺めてました。

最初、インターン先探しているということに対して、声かけるのはなしだよなーって思ったんですが、子供の朝ごはんを少しこぼしたので片付けをしていて、人ってミスするし、そこでダメって決めつけるのおかしい。優秀な子がこれでチャンス無くすのおかしい。弊社のバリューの一つが「launch x launch」(ロンロン)*1なのにグジグジ考えて行動起こさないのおかしい。と思い、声かけることにしました。

f:id:shiraji:20190826000401p:plain
声かけると決めた時の分報

声かけたあと、東京に来るとのことだったので、その時に会いましょうと言う約束をしました。予定を聞いて、最後の方ってことだったので、まぁ彼のキャリア的にも弊社はまだ魅力的には見えないだろうから優先度下げられたかーとちょっと残念な気分でした。

会ってみて

5月頭に彼は日本に一時帰国していて、会社に来てもらいました。 最初会ったとき、顔と体のギャップがすごい!って思ったのですが、話してみると本当に素晴らしい好青年でした。 素直に話は聞けるし、しっかりと意見出来ていたし、キャリアもしっかり考えていたし、事業への質問も的を得たものでした。 会う前に炎上してしまった経緯も説明してもらっていたのですが、本当に不可抗力だったのだろうなとそこでようやく納得できました。

唯一問題というと彼の今後の専門としたい分野がUXなどであること。当時彼の経験があるAndroidアプリやUnityのポジションは弊社にはありませんでした。 あるのはUXなにそれうまいの?CUIで良くね?と言っちゃう自分しかいないチームでのサーバサイドKotlinでした。 そこで、「すごくHiroくんは優秀だと感じたし、ぜひ働いて欲しいけど、今弊社に来るとHiroくんのキャリア的に遠回りになるかもしれない。本気でおすすめ出来ない。でも同じ界隈にいるだろうし、引き続き仲良くしてね!」と笑顔で送り出すことにしました。

彼が帰った後「すごく良さそうだったけど、今後の彼の専門や今のスキル的にポジションがほぼない。なので今回はなかったことで!」という評価を会社のメンバーに伝えておきました。

数日後

その数日後、TwitterのDMでこんなメッセージが来ました。

「色々週末考えたのですが、Kotinサーバーサイドとして入らせて頂くこと可能でしょうか?」

正直、まじか!空気読めない子か!ってフいたのを覚えてます。(そして今気づいたけど、typoしてるw) でも入りたい!と言ってきたのに、なにもせずにごめんなさいってするのはおかしいので、入社試験をしてもらいました。 一週間くらいかけて貰えばいいかなーって思ったところ、1,2回のラリーはあったものの、2日で作り切り、コードもしっかり読めていて、想像以上でした。 こちら側からお断りする理由もないし、彼のキャリア的に美味しいのか?と罪悪感を感じつつ、入社してもらうことにしました。

ちなみに入社後に彼が書いた下でも紹介している記事を読んで、すごく真剣に話を聞いてもらえたのがわかったし、めっちゃ嬉しかったのを覚えています。

入社前に

今のUbieはまだまだ小さな会社であり、事業を危険に晒すこと=即会社が潰れるということが十分あり得ます。そのため、彼の受け入れ条件としては公開して良い情報・ダメな情報を明確にすることをみんなに約束しました。と言っても、会った時にその話はしっかりすでにしていて、理解していたし、なんなら話す前から理解していたので、全く不安はありませんでした。

入社

一番の問題は彼が他の弊社メンバーに受け入れられず、ぎこちない関係になってしまうかも?という点でした。短い期間しかいないため、早めに打ち解けてもらいたかったです。そこでwelcomeランチがあったのですが、紹介一言目に「あの炎上した子です!」と全体に情報公開しました。彼はめっちゃ焦ってましたが、Ubieメンバーの器のデカさや彼が誠実に説明してるところを見て、みんな安心して受け入れてくれていました。あの早いタイミングでアイスブレイクしっかり出来たのは良かったなーと思います。彼いろんな会社のイベント参加してたし、入社日からUbieの一員になってくれました。

タスク

サーバサイドKotlinのポジションではあったのですが、せっかく来てもらう訳だし、色々吸収してもらおうと考えました。彼には複数のサービスの担当をしてもらい結果的に

  • サーバサイド(Kotlin/Python/Rails)
  • フロント(JS/TypeScript)
  • 実装設計
  • 単独での開発
  • スクラムでのチーム開発
  • デザイン

といろんなことをしてもらいました。全く経験がなかったのに、土日で勉強してきて、フロントの実装も素晴らしい速度でやっていたのは本当に驚きでした。

もちろん彼にもまだまだ未熟な部分はあって、そこは週1の1on1で話していたし、色々彼なりに頑張って改善していました。

結果今の彼のレジュメはぐちゃぐちゃになっただろうと思いますが、master resumeには残して、提出するresumeから外せばいいだけだし、好きにしたら良いと思う(責任放棄)

留学生のインターン

余談ですが、インターンインターンと言ってますが、留学生のインターンって日本で言う、学生の業務委託であり、使えなければクビ切られる覚悟を持っている人が多いです。彼もその考えを持っていました。

もし留学生をインターンとして受け入れる会社があるなら、学ばせるというより、キャリアを積ませる+プロの業務委託的な扱いをしてあげて欲しいです。なので、今回も学ばせると言うより、会社への貢献のためのタスクを選んでやってもらいました。

このあたり読んでもらえると良いのではないかなーと思います。

hiro-ca.hatenablog.com

なぜこの記事を書いたか?

インターン生や新卒などなど色んな人を見てきましたが、こんなブログを書くのは初めてで、今後もあんまり書かないだろうと考えています。しかし優秀な子が不本意なデジタルタトゥーにかわいそうな思いはして欲しくないので、彼はそんな人じゃないということをこの記事で明言しておきます。

彼はフルタイムで働いているため会社の様々な情報にリーチできる人材でしたが、インターン期間中一度も情報公開に関して指導する必要はありませんでしたし、信用できる人間です。

最後に

Hiroくん、お疲れ様でした。色々自分至らぬところがあったと思います。申し訳ないです。でも一緒に働けてすごく楽しかったし、Hiroくんが将来めっちゃ活躍してるところを早くみたいです。そして「わしが育てた」と言わせて下さい。Good luck!!!

*1:弊社の三つのバリューのうちの一つ。100の議論より1の実行に価値がある。

Kotlin Fest 2019に参加してきました

Kotlin Festの感想ブログを読んでいて、やっぱり自分も書きたくなっちゃった。だから書く。(2年連続)

去年の記事

shiraji.hatenablog.com

CfP

今年はCfPだったので、2本書きました。

  • GraphQL周り(通過した話)
  • IntelliJ周り(DroidKaigiでなかなか反応良かった話の改良版)

セッションの内容に新規性・独自性があること Kotlin独自の知見を得られること ― 例えば内容をJavaに置き換えても同じ知見が得られるようなセッションは採用されにくいです。

これらの要件が本当に難しくて、Javaに置き換えられない内容ってCoroutineやKotlinでしか使えないライブラリしかないんでは?と悩みまくりました。それに加えて新規性!?とかなり苦しんだCfPでした。

通過連絡

7/10に通過連絡が来ました。

全然時間なかった!とここで文句書くつもりだったけど、メール確認して7/10に連絡来ていた事実を知り、1.5ヶ月もあったんなら余裕だったんじゃん・・・と今反省しています。いや、夏休みがあったから実質1ヶ月だしやっぱり時間なかった!

スライド準備

CfPの時にアウトラインが決まっていたこともあり、サクサク進むかなーって思ってました。 IntelliJの話がしたくてしたくて、その影響もあり、あんまりモチベーションが上がらず、進捗は良くなかったです。 7月から話す内容を書き出し、2週間前にKeynoteの1枚目を作り出し、1週間前にようやく完成しました。

かなり遅くて焦ったのですが、今までとは別の方法でスライド作成し、その方法が自分には結構フィットしました。これは自分にとってかなり大きな収穫かなーと考えています。

前日

去年同様、ぼっち飯回避ツイートをしました。

kouさん、ikkunさんが反応してくれたので、前日の時点でぼっち飯回避できたの本当に良かった。

当日に追加で呼びかけたらいっせいさんが反応してくれました。これで準備万端になりました!

当日

会場に入ったのは10:40くらいでした(これがあとで叱られる原因に)。もうすごい盛り上がっていて、会場入って、おおお!とすごくワクワクしたのを覚えています。控え室に荷物を置かせてもらい、キーノートを聴きました。 Svetlana Isakovaさんに紹介してもらった、Inline classやimmutableListの話が非常に良く、早速どこに導入するべ?って検討していました。それとSvetlana Isakovaさんのスライドにまさか自分が出てくるとは思ってなかったのですごく嬉しかったです。

最近正直OSSで貢献できてないので、何かしなきゃ!と思いました。

ランチ

前日のツイートで集まった三人の方落ち合おうとしていたら、八木さんがランチ相手を探していたので、八木さんを引っ張っていき、去年と同じところでランチしました。

バックグランドがみんなバラバラだったのですが、去年同様結構盛り上がりました。 話の内容はKotlinの話はもちろん、Androidの話であったり、サーバサイドの話であったり、業務の話であったり、多岐に渡りました。 たった1時間しかなかったのが残念ですが、やっぱり初めての人とご飯行くの楽しいし、知ってる人がいても楽しい!という気づきがありました。 これは来年もKotlin Festがあるならぜひやりたい!

当日(午後)

午後のトークは自分の登壇が15:30からあると言うことであんまり聴いてないのですが、以下の二つを聴講しました。

  • Kotlin コルーチンを 理解しよう 2019
  • 公募によるLT大会

八木さんのトークはコルーチンの知識を最新版にアップデートしようと思いましたが、すんごくうまくスライド作っていて、発表内容も濃く、コードが読みやすく、途中で自分のスライドや発表内容直したくなりました。ちょっと自信を無くして、控え室に行った覚えがあります。

LTはどれも良かったです。途中どうしてもトイレにいきたくなってしまい、漏らして人生終わらすならここで行くしかないと、「静的解析ツール detekt で任意の条件で警告させる」の話途中で抜けてしまいました。lintの話は大好きだったのでめっちゃ悔しかったです。急いでトイレ行った後に戻ってきたら会場が爆笑していて、漏らさなくて良かったけど、トイレはトーク前に行こう!と言う小学生並の知見を得ました。動画に映ってる気がするので、まじで申し訳ないです。すいません。LTはめちゃくちゃよくて、どれも5分じゃなくてもっと長い時間で聴きたい!と感じました。

登壇

スライド

speakerdeck.com

GraphQLの話をする時、どこから話せば良いんだろう?と悩み、結局出来上がったのがKotlinの話をせずに1/3基本的なGraphQLの話をすると言う選択でした。 最初にGraphQLの経験を確認してみたのですが、思った以上に少なくて、この選択が間違ってなかったー!良かったーと話ながら思っていました。 GraphQLのデメリットの話は時間的に削りました。LTでGraphQLのN+1問題が辛いって話が出ていたので、そこで紹介してもらえたし、結果オーライかなーと。

今回意識したのがspeakerdeckではあんまり見えないのですが、スライド間のトランジションです。 GraphQLはデータの流れ、クエリの流れなどを理解することが重要だなと思っており、Magic Moveを駆使してそれを表現してみました。 聴いてくれた方があれがすごく良かったと反応をくれたのが本当に嬉しかったです。

余談ですが、Magic Moveを使う場合、閉じ括弧が消える問題があるのですが、これを解消するために、調べていたら結局Jakeさんに到達して、自分はようやく彼の5年前に追いついたのか・・・愕然としていました。(解消方法は下のツイートのスレッドに記述されていますが、透過率100%の文字を括弧の後ろに入れました。)

オープニングでたろうさんが盛り上げましょうみたいな話をしてくれたので、話を聴いてくれた人たちが結構トーク中に反応してくれて嬉しかったです。 スライド作った当時本当にコードしかなくて、これ絶対寝るわ。って思ったので、どうにかして、興味を失わず、理解してもらう方法を実施していたので、話ていて気持ち良かったです。聴いてくれた人たち本当にありがとうございました!

登壇後

Ask The Speakersで30分の休憩全部使って質問対応しました。 みんなかなり面白い質問をしてくれて自分まだまだGraphQLのこと知らないなーって思いました。

最初APIの開発辛いよねーって大げさな話を出したのですが、まさにあれば起こってるんです!ぜひ導入してみたい!と言う話を何人かにしてもらえたのがやっぱりこう言うのあるんだ。とびっくりしつつ、めっちゃ嬉しかったです。

ブース

ブースですが、朝ブースにいる知っている人たちを見つけて、挨拶をして、あとで来ます!と宣言して、Ask The Speakers後に向かいました。 しかし、自分のAsk The Speakersが終わる時間がブースの撤収開始時間らしく、ほぼほぼ回れませんでした。。。 家族がブースでもらえるノベルティが大好きで、Twitterを眺めていて、あれ良さそう!と自分の帰りをワクワクして待っていたようなのですが、ステッカーだけもらって帰って来た自分に対し、なぜもっと早く会場に行かなかったのか?とガチ目な説教をしてきました。次回以降しっかり早めに行きます。。。

各社のノベルティ、すごく工夫されていて -> 持って帰る -> 家族が喜ぶ -> その会社のプロダクトを使う と言う良い流れが私の家では出来ています。

懇親会

懇親会ですが、結構いろんな方と話せてすごく楽しかったです。 Svetlana Isakovaさんに登壇直前の時間にあとで話しかけるね!と宣言しており、immutableListの話を聴けて満足しました。

また初めての方が結構な数話しかけてくれたのが本当に嬉しかったです。自分が知ってる人に対しては話しかけることができるのですが、初めての方は紹介してもらうなりしないと遭遇出来ないです。 でも食事を盛っている時とか、「今話しかけても良いですか?」とか言ってもらえて本当に嬉しかったです。 ご飯食いたいけど、それ以上に話したいってのがあるので、どんどん話しかけてもらえるとありがたいなーと。 お子さんを連れて参加していたポールさんとそのお子さんとお話も出来たのも良かったです。自分もいつか子供連れて勉強会とか参加したいなーと。

まとめ

来年もあったら絶対参加したい!絶対また登壇したい!おー!!!


一人で馴染みの店で打ち上げ。めっちゃ美味かったー。

Yet another emoji support をリリースしました。

Yet another emoji supportという絵文字の入力をサポートするIntelliJプラグインをリリースしました。

f:id:shiraji:20190607004731g:plain
イメージとしてはこんな感じ

f:id:shiraji:20190607004809p:plain
Commitダイアログでも利用できます。

github.com

経緯

元々Emoji supportプラグインを公開していました。しかし、いくつかのissueで絵文字をコミットダイアログ以外のところでも挿入したいと言う要望を受けることが多くなりました。cmd+ctrl+spaceで入力できると何度も伝えていたのですが、なぜかみんなあんまり納得してくれませんでした。リリースから3年経過し、公式のサポートもまだないため、需要があるかさっぱりわからないですが、一旦作ってみるかということで作りました。

特徴

特徴としてはどの言語でもコメントでの入力をサポートしています。また自分の気づいたベースの言語に関してはStringの中での入力ができるようになっています。(GroovyとScalaが初期リリースでサポートされていないのは完全に失念していたためです。次のバージョンで対応予定です)

細かい点としてはKotlinのようにString内でコードを書ける場合、コードを書くことが可能な箇所ではCompletionが出ないようになっています。

val foo1 = "${:<caret>}" // この場合は出ない
val foo2 = ":<caret>${}" // この場合は出る

graphql-spring-boot-starterのExceptionハンドリングがめっちゃ便利になってた

Kotlinサーバサイド開発者のみなさん、graphql-javaでの開発の調子はどうでしょうか?

去年末にリリースされたバージョンのgraphql-spring-boot-starterのExceptionハンドリングがめっちゃ便利で感動したので、ブログに残しておきます。

想定読者

  • graphql-spring-boot-starter使って、GraphQLやってる人/これからやる人

はじめに

graphql-spring-boot-starterのバージョンが5.0.4の時、Resolver内で発生したExceptionは特に何も指定しない場合、全て Internal Server Error(s) while executing query と言うmessageを返していました。 例えば、AuthException のような自前で作成したExceptionが出た場合、レスポンスとしては違ったものを返したくなりますが、問答無用でInternal Server Error扱いです。(ちなみに最新版でも互換性を残すため、同じ挙動です)

    @Suppress("unused")
    fun diseases(icd: String): List<Disease> {
        if (icd.isEmpty()) throw IllegalArgumentException("icd must not be empty")
        return service.getDiseases(icd)
    }

f:id:shiraji:20190424022910p:plain
何だろうとInternal Server Error!

そこで、カスタマイズしたエラーを返すようにしたいのですが、探せど探せど良い方法が見つかりません。issueにこのドキュメント通りに書けばいけるぜ!と書かれているので、URLをクリックすると

        \          SORRY            /
         \                         /
          \    This page does     /
           ]   not exist yet.    [    ,'|
           ]                     [   /  |
           ]___               ___[ ,'   |
           ]  ]\             /[  [ |:   |
           ]  ] \           / [  [ |:   |
           ]  ]  ]         [  [  [ |:   |
           ]  ]  ]__     __[  [  [ |:   |
           ]  ]  ] ]\ _ /[ [  [  [ |:   |
           ]  ]  ] ] (#) [ [  [  [ :===='
           ]  ]  ]_].nHn.[_[  [  [
           ]  ]  ]  HHHHH. [  [  [
           ]  ] /   `HH("N  \ [  [
           ]__]/     HHH  "  \[__[
           ]         NNN         [
           ]         N/"         [
           ]         N H         [
          /          N            \
         /           q,            \
        /                           \

こんなページに飛ばされ、かなりイライラします。*1

でまぁ、色々調べていくと、Authエラーなどで全処理中断させたいであれば、 GraphQLException を実装すれば、一般的なランタイム時のExceptionとなり、エラーを返してくれると書いてあるので、試してみます。

    @Suppress("unused")
    fun diseases(icd: String): List<Disease> {
        if (icd.isEmpty()) throw GraphQLException("icd must not be empty")
        return service.getDiseases(icd)
    }

結果

f:id:shiraji:20190424023144p:plain
返してくれなかったよ・・・涙

このあたりで心折れます。

Spring BootでのExceptionハンドリング

話変わって、Spring Bootには結構便利なExceptionハンドラがあります。 以下のような感じで、各種Controller内で発生したExceptionに対して、一箇所でうまくシュッとやってくれます 😤

@RestControllerAdvice(basePackageClasses = [ExceptionHandlerAdvice::class])
class MyExceptionHandlerAdvice {

    @ExceptionHandler(TimeoutException::class)
    @ResponseStatus(HttpStatus.REQUEST_TIMEOUT)
    fun handle(exception: TimeoutException): ExceptionResource {
        log.error("caught an exception", exception)
        return ExceptionResource("タイムアウト")
    }

    @ExceptionHandler(WebExchangeBindException::class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    fun handle(exception: TimeoutException): ExceptionResource {
        log.error("caught an exception", exception)
        return ExceptionResource("なんか起こった")
    }

}

まぁあんまり詳しく書いても話が逸れるので、こんな感じのものがあるのだなー程度で良いです。

で、graphql-javaでもこんな感じで処理したい!と思う訳です。

時間が解決してくれた(=中の人達が頑張ってくれた)

それから数ヶ月が経ち、graphql-spring-boot-starterがメキメキ更新されていました。

現在の最新のバージョン(5.7.3)を使い、graphql.servlet.exception-handlers-enabledをtrueにすると以下のコードでもうまくハンドリングしてくれるようになります。*2

    @Suppress("unused")
    fun diseases(icd: String): List<Disease> {
        if (icd.isEmpty()) throw IllegalArgumentException("icd must not be empty")
        return service.getDiseases(icd)
    }

f:id:shiraji:20190424030032p:plain
IllegalArgumentExceptionですらうまくメッセージを返すように

ものすごく良くなりました。しかし一点typeがException名になってしまいます。 フロント側で IllegalArgumentException を処理してもらうで良いのですが、ちょっとダサい。GraphQLExceptionをthrowしたとしても、同じようにtypeが表示されてしまいます。

Spring BootのExceptionハンドリングのように特定のExceptionに対して、カスタマイズしたGraphQLErrorを表示したくなります。

そこで登場したのが、exception-handlers-enabledが導入されたタイミングでgraphql-spring-boot-starterが @ExceptionHandler によるExceptionのカスタマイズ機能です。実装方法としてはこんな感じのBeanやComponentをクラスを作るだけ!

@Component
class GraphQLExceptionHandler {
    @ExceptionHandler(IllegalArgumentException::class)
    fun handleSomeException(e: Throwable): GraphQLError {
        return GenericGraphQLError("Foo! ${e.message}")
    }
}

それで同じようにExceptionを発生させると

f:id:shiraji:20190424031759p:plain
メッセージやtypeを表示しないなどのカスタマイズが出来た!

これにより、エラーメッセージのカスタマイズやtypeなどを表示しないなどなどエラーハンドリングが容易に出来るようになりました。

実際のコード

実際にUbieで公開している https://github.com/ubie-inc/kotlin-graphql-sample に、動くコードをコミットしてあります。上記には記述してありませんが、graphql-java-toolsのバージョン上げも必要になりますので、ご注意してください。

github.com

*1:https://graphql-java.readthedocs.io/en/latest/https://www.graphql-java.com/documentation にリダイレクト機能なし+ドキュメント削除して移動したことが原因です

*2:5.3で導入され、5.4.1でバグfixされています。