graphql-java で 雑に pagination 実装したい

実装した graphql-java で雑に pagination 実装できるようにした - 宇宙行きたい


なんかサクッと返してくれるのないかなぁと探したんだけどなかった。 SimpleListConnection は全部のリスト渡さないといけないし。。。ということで雑に書いたんだけど、これをそのまま使いたいってよりもこういうことしたいんだけど自分で実装しないでもなんか汎用的なのあるんでしょ!?俺が見つけられてないだけで!!! 教えてください!!!的な気持ちで書いてみた。(出てこなかったらこれもうちょっと弄って使うけど)

    var count = hogeMapper.count();
    var hogeConnection = new SimpleConnection<Hoge>(count, (offset, limit) -> {
      return hogeMapper.getHoges(offset, limit);
    }).get(env);

こんな感じでページネーションしたいリストの合計サイズと offset, limit を元に list 返す関数渡すと graphql.relay.Connectionインスタンス返してくれるやつ

import static graphql.Assert.assertNotNull;

import com.google.common.base.Objects;
import com.google.common.io.BaseEncoding;
import graphql.relay.Connection;
import graphql.relay.ConnectionCursor;
import graphql.relay.DefaultConnection;
import graphql.relay.DefaultEdge;
import graphql.relay.DefaultPageInfo;
import graphql.relay.Edge;
import graphql.schema.DataFetchingEnvironment;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.BiFunction;

public class SimpleConnection<T> {

  final SimpleConnectionCursor sizeCursor;
  final BiFunction<Integer, Integer, List<T> > func;

  public SimpleConnection(Integer size, BiFunction<Integer, Integer, List<T> > func) {
    this.sizeCursor = new SimpleConnectionCursor(assertNotNull(size));
    this.func = assertNotNull(func);
  }

  public Connection<T> get(DataFetchingEnvironment environment){
    Integer firstInteger = environment.getArgument("first");
    if(firstInteger == null){
      firstInteger = 20;
    }
    var limitCursor = new SimpleConnectionCursor(firstInteger);
    var offsetCursor = new SimpleConnectionCursor(
      (String) environment.getArgument("after"));
    if(offsetCursor.getOffset() != 0){
      offsetCursor = new SimpleConnectionCursor(offsetCursor.getOffset() + 1);
    }

    List<T> list = func.apply(offsetCursor.getOffset(), limitCursor.getOffset());
    if(list.isEmpty()){
      return emptyConnection();
    }

    List<Edge<T>> edges = new ArrayList<>();
    int index = offsetCursor.getOffset();
    for(T o : list){
      edges.add(new DefaultEdge<>(o, new SimpleConnectionCursor(index++)));
    }

    var firstEdge = edges.get(0);
    var lastEdge = edges.get(edges.size() - 1);
    var pageInfo = new DefaultPageInfo(
      firstEdge.getCursor(),
      lastEdge.getCursor(),
      0 < ((SimpleConnectionCursor)firstEdge.getCursor()).getOffset(),
      ((SimpleConnectionCursor)lastEdge.getCursor()).getOffset() < sizeCursor.getOffset() -1
    );
    return new DefaultConnection<>(
      edges,
      pageInfo
    );
  }

  Connection<T> emptyConnection() {
    return new DefaultConnection<>(Collections.emptyList(), new DefaultPageInfo(null, null, false, false));
  }

  static class SimpleConnectionCursor implements ConnectionCursor{
    static final String PREFIX = "DUMMY";
    final int offset;

    SimpleConnectionCursor(int offset) {
      this.offset = offset;
    }

    SimpleConnectionCursor(String cursor) {
      this.offset = convertToOffset(cursor);
    }

    /**
     * @return an opaque string that represents this cursor.
     */
    @Override
    public String getValue() {
      return convertToCursorString(offset);
    }

    public int getOffset(){
      return offset;
    }

    private int convertToOffset(String cursorString) {
      if (cursorString == null) {
        return 0;
      }
      try{
        var string = new String(BaseEncoding.base64().decode(cursorString), StandardCharsets.UTF_8);
        return Integer.parseInt(string.substring(PREFIX.length()));
      }catch (IllegalArgumentException ignored){
      }
      return 0;
    }

    private String convertToCursorString(int offset) {
      return BaseEncoding.base64().encode((PREFIX + Integer.toString(offset)).getBytes(StandardCharsets.UTF_8));
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (o == null || getClass() != o.getClass()) {
        return false;
      }
      SimpleConnectionCursor that = (SimpleConnectionCursor) o;
      return offset == that.offset;
    }

    @Override
    public int hashCode() {
      return Objects.hashCode(offset);
    }
  }
}

ドラム式乾燥機に2日放置しててもシワにならないシャツ

最近服を買う時に一番大事にしているポイントは乾燥機に放置してもシワにならないことです。次点で猫の毛が目立たない。

と言うことで今年買ってマジでシワにならなくて感動したのがGUのこのシャツ。マジでシワにならなすぎてちょっと縫い目がチクチク感じることもあるけど慣れればOK。生地感は何というか普通のシャツとかと違って未来素材感ある。テカテカツルツルな感じ、でもシワにならないの幸せ。今年の夏はこのシャツの白と黒を2枚づつの計4枚を回して着てほぼ過ごしました。来年もあったら必ず書います。

www.gu-global.com

 

ちなみに次点ではユニクロエアリズムコットンオーバーサイズTシャツ(5分袖)。これはシワになりにくいけど放置するとやっぱりちょっとシワになる。けど生地感は凄く普通のTシャツっぽくなるので何となく少し生地感気にするようなお出かけの時はあり

「ZZ 観なくていいと思うんですけど」という声を聞いて

僕 ZZ 大好きなんですよ。あとGガンも大好きです。共通する部分として導入の何話かの空気感が辛くて辛くて観るのを辞めちゃう人が多いっていうのがありますね。

でもプルとプルツーの戦いのセリフとかマシュマーの散り際のカッコ良さとかハマーンの最後の諦めにも似た敗北とか色々見所あるので観て欲しい気持ちはあります。

あと、プルが可愛いです。小学生の時にアニメのキャラに恋をするといういきなり道を外してしまうことになったのはこのキャラのせいですね。(次がナディア)

追加

それぞれのシーンリンクしておきますね。ネタバレ大丈夫な人は是非

Netflix DGS Framework で Spring の Controller みたいに入力値のバリデーションする

こんな感じで AOP で処理してDataFetcherExceptionHandlerで良しなに処理してる。

package org.yoshiori.datafetchers.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.validation.DataBinder;
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;
import org.springframework.web.bind.MethodArgumentNotValidException;

import javax.validation.Validator;

@Aspect
@Component
public class GraphQLValidator {

    final Validator validator;

    public GraphQLValidator(Validator validator) {
        this.validator = validator;
    }

    /**
     * Validation like the Spring controller.
     *
     * @param jp
     */
    @Before("execution(* org.yoshiori.datafetchers..*(..)) && @within(com.netflix.graphql.dgs.DgsComponent)")
    public void validateParameters(JoinPoint jp) {
        var signature = jp.getSignature();
        if (!(signature instanceof MethodSignature)) {
            return;
        }
        var methodSignature = (MethodSignature) signature;
        var parameters = jp.getArgs();
        var annotations = methodSignature.getMethod().getParameterAnnotations();
        SpringValidatorAdapter adapter = new SpringValidatorAdapter(this.validator);

        for (var i = 0; i < annotations.length; i++) {
            for (var annotation : annotations[i]) {
                if (annotation.annotationType().equals(javax.validation.Valid.class)) {
                    var bindingResult = new DataBinder(parameters[i]).getBindingResult();
                    adapter.validate(parameters[i], bindingResult);
                    if (bindingResult.hasErrors()) {
                        var m = MethodParameter.forParameter(methodSignature.getMethod().getParameters()[i]);
                        throw new RuntimeException(new MethodArgumentNotValidException(m,
                            bindingResult));
                    }
                }
            }
        }
    }
}

使い方

@DgsData(parentType = "Query", field = "shows")
public List<Show> shows(@InputArgument @Valid Hoge hoe){
  ...
}

なんかもっと良い方法あったら教えて欲しい。

🇺🇸 スタートアップで経験したシードからシリーズAの資金調達まで

シリーズAの資金調達

japan.cnet.com

シリーズAの資金調達をしました。USのスタートアップでシードラウンドから経験するのって結構貴重な経験かなって思ったので資金調達までどんな感じだったのか描いてみようと思います。

そもそもシードとかシリーズAとか何?

ググってもらった方が正確な情報が出てくると思うので詳しく知りたい人はググってください。 すっごい雑に言うとこんな感じです。

  • シードラウンド
    • アイディア段階での調達、そのアイディア良さそうじゃんって思ってもらったら投資してもらう感じ
  • シリーズA
    • ある程度動くものができて顧客も見込めそうじゃんって思ってもらって投資してもらう感じ

僕が入社した段階

で、シードラウンドの途中で僕は入社しました。1年3ヶ月くらい前。

yoshiori.hatenablog.com

その時のプロダクトがどんな状況だったのかを説明すると、まだコアロジックのプロトタイプが出来ていたくらい。
ソースコードとかコミットログとか過去のテスト結果とか全部手元に有れば性能の出るモデルが作れることはわかったくらい。
そこからどうやってデータを集めるのか、そのモデルをどうやって実行してもらってどうやって結果を動かしてもらうのかとかから考え始めました。
簡単に言えばデータあれば性能出ることはわかったけどじゃぁそのデータってどうやって集めるの?と。
APIサーバ作ったり仕様考えたりもそうだし、データをどうやって持つのとか貯めるにしてもSaaSになるはずなのでデータ間のセキュリティも担保しなきゃいけないしとか、認証どうするとかそもそもインクリメントにデータ貯めて学習どうするんだとか
そこまで出来てWebinarとかやって「興味ある人はメールしてね」みたいな感じで募集をかけてそっからPMがSQL叩いてAPIトークン発行してメールで送るとか
それをカスタマーが自分でサインアップして自分でトークン発行したり出来るようにしたり、サイトから付加情報見れるようにしたり

正解は誰も知らないので何に注力するのか決めながら進んでいく感じでした。例えばDBは最後の砦だか最初から堅牢にセキュアになるように設計したり、逆にAPIサーバはデプロイ前のチェックを重厚にするよりエラー早期発見出来るようにしてすぐロールバック出来るようにしたり色々考えながら作って、作って運用して考えて変更してという感じで進んでました。っていうか今もそんな感じで進んでいます。

僕が感じたこと

とりあえず楽しい。やらなきゃいけない事はいくらでもあるのでその中から自分で取捨選択して方針決めていくのはやっぱり楽しい。何やっても基本的に0 -> 1になるので何かするたびに出来ることが増えるのは純粋にやっぱり面白い。今はやっと最低限の道具は揃った感じにはなった。でも勿論全部完璧なわけなくてガムテープでとりあえず補修してる的な部分もある。なので新しいカスタマーがデータを送ってきてくれるようになると想定してなかった事とか起きて水道管ゲームのあっちの水漏れ直したらこっちで水漏れしてみたいな感じで日々色々な問題に対応したりしている。

焦燥感は結構あった。シードラウンドは本当に調達した資金を食い潰しながら走っている状況でこれを経験するのは本当に初めてなので焦燥感は結構あった。昔所属してた会社で月数億単位で赤字とかは経験したことあるけど赤字でも売り上げがあるのとは全然違う感覚だった。赤字でも100円でも売り上げがあればあとはそれを拡大していけばどんどん収入増えるだろうけど、そもそも売り上げが何もないのでゼロをいくら掛けてもゼロにしかならない状態、なので早く売り上げを作れる状況まで持っていかなきゃいけないっていう焦りはずっとあった。まぁ、資金食い潰してるとか偉そうに言ってるけど別にどのくらい残ってるのかとか見てたわけではないので本当に個人的に感じてただけなんだけど。

あとは前にも描いたけど、英語はやっぱり苦労しているとか、同僚がみんな優秀で楽しいとかもあります。

そんなこんなで本題

そんな感じでシリーズAの資金調達をしたんだけどむしろここからが本当の勝負どころ!!!
で大変だけど楽しいこともいっぱいあると思ってます。そして人手も全然足りていない!!!
特に今はData Scientistが欲しいです!僕らのサービスのコアである機械学習部分の改善をしていける人を探しています!一緒に小さいベンチャーから世界にインパクトを与える仕事しませんか?楽しいのは保証します!!!

www.launchableinc.com

Launchableで働いて1年経った感想と仲間募集のお知らせ

最初に、メインのお知らせから Launchableでは新たに仲間を若干名募集しています!

https://www.launchableinc.com/careers

の三つです。 Software Engineer については同僚の書いたのを読んでもらうのがわかりやすいと思うので、それ以外について説明します。

blog.konboi.com

Data Engineer はぶっちゃけ Software Engineer と募集要項に書いてあることはそんなに違いはありません。でもその違いの部分がすごく重要です。

それが「データ処理プラットフォームの構築運用経験」と「SparkおよびJavaの使用経験」です。僕たちはそこの経験が豊富ではないので、とりあえず今できる形でやっている部分が多いです。なのでそこをどんどん改善していける人を募集しています。

Data Scientist はわかりやすいですね。その名の通りです。僕らの使っているモデルの更なる改善と拡張が主な責務です。

と言うことで積極採用中ですので是非ご応募ください!!

一年働いてみて

気がついたら1年と1ヶ月くらい経ってたので軽く英語以外について書いてみる。英語については前にまとめたので yoshiori.hatenablog.com

良いところは自分たちが作っているものが良いものだと感じれることと同僚が優秀なところに尽きると思います。

テストの時間を短くして開発者の生産性をアップすると言うのはとても良い事だと素直に言えるし、そのための仕事をしていることは誇りに思える。一年前にはなかった色々なものが形になり動き始めていると言う状況に携わっているのは単純に楽しい。

同僚も川口さん始め優秀な人ばかりなので毎日勉強になりつつとても楽しい。これはエンジニアだけに限らずPMもメチャクチャ優秀な人なので本当にストレスなく働ける。

まぁ、ほんと10人前後の会社なのでなんでも自分たちでやらなきゃ行けないので xxxのリサーチとxxxのコーディングと採用サイトのアップデートみたいな軸が違うから優先順位つけるのが難しいタスクを同時にこなしたり大変な部分はあるけれどもやっぱり楽しいです。

まだまだプロダクト自体も未完成だし色々なことに積極的にチャレンジできるのでその辺が楽しめる人には本当におすすめの会社だと思うので是非気軽に応募してね!!!(1回目の面接は僕と id:ninjinkun になります)

一年英語やってみてわかった事

前提条件書いておくと義務教育の英語レベルも無い人間の話です。三単現って言われて「麻雀の役?」とか思っちゃうレベル。

やってること

  • 毎日 Duolingo
  • 平日は毎日DMM英会話
    • 文法の教材はある程度やった
    • 会話の教材使ってる
      • 出てくる単語をiKnowで勉強する機能があるので予習してる

f:id:Yoshiori:20210519002952j:plain f:id:Yoshiori:20210519002957j:plain

だいたい毎日1時間くらい使ってる感じ。

どのくらい成長したか

これがホント後の話にも続くんだけど当初の予定より全然成長していない。英語のミーティングや1on1にビビらなくなったって言うだけ。これは言葉通りでビビらなくなっただけで半分以下しか理解出来てないし言いたい事全然伝えられない。

まあ、そんななのでもちろん凹む。メッチャ凹む。でもなんというかそれの理由が分かったのでそれを今日は書きたかった。

多分英語学習が進まなかったり途中で挫折しちゃうのは目標が高すぎるからだという事に気がついた。

どういうことかと言うと英語勉強するときの雑な目標って「ネイティブの人と普通にコミュニケーション取れるくらいになりたいな」だと思うんだけど、実はそのゴールはメチャクチャ遠い。むしろ最終ゴールがそれなんだよね。

いきなりそこをゴールにしちゃうと本当に日々の成長を何も感じられずに泣きそうになってやめちゃうんだと思う。

とは言え適切なゴールが何なのかは俺も分かってない。分かってるのは「そのゴールは遠いやつだからそれに絶望して歩みを止めてはダメ」と言うことだけ。

まぁ、なので凹みはします。はい。

どうやって習慣化したか

DMM 英会話は終わった瞬間に次の予定を入れてしまいます。途切れたらなかなか登録しにくくなるので。だいたい講師の人も2日先くらいのスケジュールしか登録してないので金曜のレッスン終わった後月曜日の予約を入れるのが結構大変だった。最近は必ず金曜日には月曜日のスケジュールを登録してくれてる良い先生を見つけたのでその人を予約してる。

Duolingo は毎日お風呂でやってる。もともとお風呂大好きでお風呂で本読んでたりしたんだけどその時間を英語勉強にしてる。お風呂入ったらまずはDuolingo起動すると言うのが習慣化というかパブロフの犬化されてて続いてる。

後、なんかスッゲー続けてる感出してるけど普通に風邪引いたりすると途切れちゃうし年末年始とかGWとかも途切れがち。

仕事ではどうか

仕事に支障はある。それはちゃんと認めないといけない気がしてる。使ってるツールは主にDeepLとgrammarlyで両方とも課金してる。

ついつい意識高くなって「自分で英語読まなきゃダメだ」とか「英語で考えなきゃダメだ」とか思ってしまうんだけど普通に仕事としても遅くなっちゃうので逆にちゃんとツールに 頼らないといけないと思ってる。コレは意外な感覚だった。 英語で全部やろうとしてなんて言うか糞詰まりみたいに思考が止まる様になってしまう事がよくある。

なので最近はまず日本語で考えてラフな箇条書きにしてから英語にするようになった。

よく言われている勉強法ではダメだって言われてるやつ。 「英語で考えて英語で話せないとダメ」って言うのをよく見るんだけど、俺の場合はそれに拘ってしまうと考えている事が凄くゆっくりしか出てこなくなってメッチャ狭い穴から搾り出してる様な感覚になり辛かった。

今後どうしていくか

プログリットみたいなやつで短期集中である程度地力を付けてから今の勉強法にした方が早いような気はしてるんだけど毎日最低3時間は確保しないといけないのは子育てと並行はなかなかに決断しにくい。3ヶ月それを続ける覚悟が出来たらやってみたいけどなかなかね。

追記

大事なこと書き忘れてた!「もっと良い学習方法あるかも?」とか考えて学ぶのを止めちゃうくらいなら1つでも単語覚えたほうが良い。マジで。これ結構陥りやすい罠。