個人のメモとしてNotionを使いはじめた

この記事は suusan2go Advent Calendar 2019 - Adventar の9日目の記事です。

個人のメモとしてNotionを使い始めました。

これまで

個人が無料で使えるkibelaを使ったり、Boostnoteを使っていたりしました。Notionは仕事で使ったことがあったものの、以下の点が微妙だなーというのが正直な感想でした。

  • ピュアなマークダウンではかけない
    • 例えば ####Heading として評価されないとか
  • 検索が弱い
    • 使っていたのは半年以上前の話なので分からないけど、日本語での検索が弱い印象でした
  • カンバン・リストも結局は専用のツールのほうが使いやすい
    • オールインワンであることはメリットなのだけど、正直開発のタスク管理としてはJiraやZenHubのほうが使いやすかったです
  • Slack通知が弱い
    • 下書きみたいな概念がないので、書いてる途中でガンガンSlackに通知が飛んでいき、結果として通知が意味をなさないみたいな状況がありました
  • 記事のタイムスタンプがファーストビューでは見れないので何が新しい記事かが分からないし、Slack通知の件もあって情報にキャッチアップしていくのが難しい
  • 本文中で :emoji: が使えない。そんなに多用するわけじゃないんだけど、ないと寂しい…

※今はよくなってるよ!とかあれば教えてください。

いきなりディスってしまったけど、個人用として使いだしたら結構いいなーと思えたので、その話をします。

個人で使いはじめて感じたNotionのいいところ

Web Clipperがある

Web Clipper の機能が提供されたのが大きくて、個人として使い始めたのはこれが理由でした。パブリックなページなら、中身のテキストも取ってきて編集できるのがいい感じです。

f:id:suzan2go:20191209155612p:plain

調べ物してるときに、いくつかページをぐるぐるして「あーあのページに書いてあったこと試してみればよいのでは?」と閃いたあと、肝心のページになかなかたどり着けないみたいなことありませんか。自分はよくあります。とりあえずWeb Clipperで取得してコピーしておけば、あとはNotion上で完結できるので、重宝しています。

下書き・公開という概念がない

これはチームで使っていたときにはむしろよくない点でもあったのだけど、個人として使う分にはむしろしっくり来ました。基本的に「あー書いたのに消えてしまったーーー」ということが無いし、保存みたいな概念がなく勝手に保存されていくので普通にメモみたいな感じで使えています。

カンバン・カレンダーを一つのページに埋め込める

これもチームで開発する上ではやはり専門のツールが欲しいなと思ってしまったところだけど、個人でちょっとタスク管理したいくらいの用途だと組み込みのカンバンやカレンダーで十分高機能で、これも重宝しています。自分のダッシュボードみたいなものを作って、勉強しておきたいことなどをまとめておいたり、稼働のスケジュールを考えたりなどしています。

個人で使いはじめて感じたこと

調べたわけじゃないので完全に想像なんですけど、Notionって最初は個人向けに作られていたのではという気がするんですよね。そう考えるとチームで使ったときに自分が感じた使いにくさみたいなものの説明ができる気がする。まだチームで使うツールとして自分がNotionを選ぶかというと選ばないけど、個人で使うツールとしては結構手に馴染むので、どこかのタイミングで「チームのWikiとしてNotionいいぞ!!!」となるのかもしれない。

改めて ロードマップ を見ると、API や BetterSearch、TimlineView なんかも Coming Up とのことなので、この辺が来たらチームとしても使いやすいツールになりそうだなーと期待しています。

Cloud Dataflow で Cloud SQLからBigQueryにサーバーレスにデータ連携する その2

この記事は suusan2go Advent Calendar 2019 - Adventar の8日目の記事です。

その1はこちら

suzan2go.hatenablog.com

上記の記事では以下のようなJSONを用意してCloud SQLからBigQueryにデータ連携する仕組みを作ったのだけど、テーブル追加の度にこのJSONを書くのが面倒になって、指定したテーブルだけをガッと全部とってこれないかなと思って作り直した。

- query: "select * from users"
  output_table_name: users
  fields:
    - name: id
      type: INT64
      mode: REQUIRED
    - name: avatar_url
      type: STRING
      mode: REQUIRED
    - name: nick_name
      type: STRING
      mode: REQUIRED

普段全くJava書いてないので、結構変なコードがあるかもしれない・・・というエクスキューズをしておきます :pray:

BQのスキーマ指定なしでもDBからスキーマを自動生成してデータを同期したい

もともとの記事でYAMLを用意していた理由は以下の2つ。

  • 同期したい・したくないカラムを柔軟にカスタマイズできるように
  • 参考にしたSQL to BQのテンプレートがBQのスキーマ情報を要求していた

前者についての要望はもちろんあるものの(emailをコピーしたくないとか)、どちらかというと後者の実装面での制約が大きかったので、ガッと作ることにした。 調べてみると、BigQueryIOは withSchemaFromView というAPIを持っていて、DataflowのPipelineの中で計算したschema情報を指定できるようなので、それを使うことにした。

コードでいうと以下のような変更になる

BigQueryIO.writeTableRows()
        .withoutValidation()
        .withCreateDisposition(BigQueryIO.Write.CreateDisposition.CREATE_IF_NEEDED) // テーブルがなければ作成する
        .withWriteDisposition(BigQueryIO.Write.WriteDisposition.WRITE_TRUNCATE) // 既存のテーブルを消して書き込む
-       .withSchema(schema.getTableSchema())
+       .withSchemaFromView(schema)
        .ignoreUnknownValues()
        .withCustomGcsTempLocation(options.getBigQueryLoadingTemporaryDirectory())
        .to(new CustomTableValueProvider(schema.getOutputTableName(), options.getOutputDataset())));

PostgreSQLのInformation Schemaの利用

方針としてはPostgreSQLを見に行って、テーブルのカラム情報と型情報を取得し、それをもとにBQのスキーマを組み立てていけばよい。

基本的にテーブルのカラムの情報は information.schema.columns を見に行けばよい。 table_name で絞り込み data_typecolumn_nameis_nullable といったカラムの情報をもとにテーブル情報を取得できる。注意しなければいけないのはArray型で、以下のドキュメントにも書いてあるがこのテーブル Array までしか分からず Array<int> なのかどうなのかを知るには elemenmt_types まで見に行く必要がある。

35.16. columns

実際に発行するクエリとしては以下のような形になる。

 String schemaQueryTemplate = "SELECT c.column_name as COLUMN_NAME, c.is_nullable AS IS_NULLABLE, c.data_type as DATA_TYPE_NAME, e.data_type AS ELEMENT_TYPE_NAME,\n" +
         "    pg_catalog.col_description(format('%%s.%%s',c.table_schema,c.table_name)::regclass::oid,c.ordinal_position) AS COLUMN_DESCRIPTION\n" +
         "FROM information_schema.columns c LEFT JOIN information_schema.element_types e\n" +
         "     ON ((c.table_catalog, c.table_schema, c.table_name, 'TABLE', c.dtd_identifier)\n" +
         "       = (e.object_catalog, e.object_schema, e.object_name, e.object_type, e.collection_type_identifier))" +
         "where table_name = '%s'";

このクエリ結果をもとに、地道にBQのスキーマを組み立てればよい。

Pipelineで上記のクエリ結果からBQのスキーマ情報を組み立てる

JdbcIOというクラスを使って、上記のクエリ結果を一行一行処理していき、最後にまとめていけば良い。

結果をBigQueryのスキーマ情報に変更する

これはかなり力技なのだけど、DBのTypeをもとにBQのTypeに変換していく。上述したようにArrayだけはelement_typesの型を見に行かないと実際の型が分からないことに注意が必要。

    // 変換用のMap ※一部抜粋
    private static final Map<String, String> postgresqlToBqTypeMap = ImmutableMap.<String, String>builder()
            .put("integer", "INT64")
            .put("character varying", "STRING")
            .put("date", "DATE")
            .build();


.withRowMapper((JdbcIO.RowMapper<Map<String, String>>) resultSet -> {
    String columnName = resultSet.getString("COLUMN_NAME");
    String typeName = resultSet.getString("DATA_TYPE_NAME");
    String elementTypeName = resultSet.getString("ELEMENT_TYPE_NAME");
    String description = resultSet.getString("COLUMN_DESCRIPTION");
    String isNullableString = resultSet.getString("IS_NULLABLE");

    String mode;
    String type;
    if(typeName.equals("ARRAY")) {
        type = postgresqlToBqTypeMap.get(elementTypeName);
        mode = "REPEATED";
    } else {
        type = postgresqlToBqTypeMap.get(typeName);
        mode = isNullableString.equals("YES") ? "NULLABLE" : "REQUIRED";
    }

    if(type == null) {
        throw new IllegalArgumentException(
                "can't detect bigquery schema type from postgres type: " + typeName + " elementType: " + elementTypeName
        );
    }

    Map<String, String> map = new HashMap<String, String>();

    map.put("mode", mode);
    map.put("type", type);
    map.put("name", columnName);
    map.put("description", description != null ? description : "");
    return map;
})

データを結合して一つのスキーマにする

上記の状態から最終的なアウトプットとして { [table名]: <BQ schema json>} な形式に変換して上げる必要がある。ここで使うのが Combine というもの。

beam.apache.org

このCombineの概念をちゃんと理解せずに、 「reduce みたいなもんでしょ」とか舐めてたらめちゃくちゃハマってしまった。サンプルとして以下のようなコードが書かれていたので余計に勘違いしてしまったのだけれど、もうちょっとやっていることは複雑。

public static class SumInts implements SerializableFunction<Iterable<Integer>, Integer> {
  @Override
  public Integer apply(Iterable<Integer> input) {
    int sum = 0;
    for (int item : input) {
      sum += item;
    }
    return sum;
  }
}

公式ドキュメントにも書いてあるとおり、実際の処理の流れとしては以下のようになっている。

  • Create Accumulator creates
    • 集計用のデータをローカルに作成
  • Add Input
    • それぞれの集計用のデータにinputを追加
  • Merge Accumulators
    • 集計用のデータを一つにまとめていく ※この処理は複数回呼ばれる
  • Extract Output
    • アウトプットのデータ構造を作成

Merge Accumulatorsは複数回呼ばれるのでそれを想定した作りにしておく必要があって、それを理解してなくてかなり時間を食ってしまった。実装でいうと上記の処理に対応した関数をもつクラスを作ってやればよく、素朴に実装すると以下のようになる。

    public static class SchemaInfoToBigQuerySchemaFn extends Combine.CombineFn<Map<String, String>, List<Map<String, String>>, Map<String, String>> {
        public CustomTableValueProvider  tableValueProvider;
        private final Logger LOG = LoggerFactory.getLogger(SchemaInfoToBigQuerySchemaFn.class);

        SchemaInfoToBigQuerySchemaFn(CustomTableValueProvider value) {
            this.tableValueProvider = value;
        }

        @Override
        public List<Map<String, String>> createAccumulator() { return new ArrayList<Map<String, String>>(); }

        @Override
        public List<Map<String, String>> addInput(List<Map<String, String>> accum, Map<String, String> item) {
            accum.add(item);
            return accum;
        }

        @Override
        public List<Map<String, String>> mergeAccumulators(Iterable<List<Map<String, String>>> accums) {
            List<Map<String, String>> merged = createAccumulator();
            for (List<Map<String, String>> accum : accums) {
                merged.addAll(accum);
            }
            return merged;
        }

        @Override
        public Map<String, String> extractOutput(List<Map<String, String>> accum) {
            TableSchema tableSchema = new TableSchema();

            List<TableFieldSchema> fieldSchemaList = new ArrayList();
            for (Map<String, String> item : accum) {
                fieldSchemaList.add(
                        new TableFieldSchema()
                                .setName(item.get("name"))
                                .setType(item.get("type"))
                                .setMode(item.get("mode"))
                                .setDescription(item.get("description"))
                );
            }
            tableSchema.setFields(fieldSchemaList);
            String destinationValue =  tableValueProvider.get();
            TableDestination destination = new TableDestination(destinationValue, "");

            Map<String, String> schemaMapValue = Maps.newHashMap();

            String json = new Gson().toJson(tableSchema);
            schemaMapValue.put(destination.getTableSpec(), json);

            return schemaMapValue;
        }
    }

まとめ

  • PostgreSQLからデータを抜いて、実行時に動的にBQのスキーマを取得することができた
  • 頑張ったしApache Beamの勉強にもなったけど、運用的には最初のJSONを自動生成してカスタマイズできるほうが嬉しかったかもなぁーと今は思っている・・・・・・

大企業に残った友人と話して分かった大企業の今

この記事は suusan2go Advent Calendar 2019 - Adventar の7日目の記事です。

少し前に、自分が新卒で入った会社の同期の友人から転職相談を受けて、自分がいたときとは会社の状況とか従業員としての状況とか色々結構変わったんだなと思ったので書いておく。

大きくわけると2つ変わったことがあった。

  • 時代の流れもあって会社自体が当時から変わったこと
  • 30代になって当時から変わっていること

自分は新卒で入った会社に3年くらいいて20代後半でベンチャーに行ったんだけれども、正直一時期は「大企業よりもベンチャーの方が潰しの効く経験詰めるしいい」「給与もエンジニアなら悪くない」「働きやすさもベンチャー企業の方が上」みたいなことを思っていた。しかし友人と話してみると、自分の持っていたイメージは大分古いのかもなと感じた。

大企業ってどこよ

所謂大手のSIer、メーカーで従業員が十数万人といった規模の会社。自分のプロフィールとか見に行くと普通に書いてあるので気になる人は探しにいってください。また大企業のなかでのある部署の話なので、あまり一般化はできないと思う。なのでタイトルはちょっと釣り気味です。またこのブログの公開については友人から許可を得ています。

大企業での働きやすさ

少なくとも自分がいた当時は働きやすいとは全くいえない環境で、8:00に出社して11:00に会社をでるみたいな生活が普通だった。フレックスなんて制度もなくリモートワークなんてありえなかった。しかし話を聞いてみると以下のような状況らしい。

  • フレックス(コアタイム無しのフルフレックスも業務上必要であれば認められている)
  • リモートワークも朝にメールを一通出せばOK

所謂WEB系のベンチャーでもリモートワークとフルフレックスを認めている会社は実はそれほど多くないと思うので驚いた。また残業についても、「赤字を出しても案件を取る / 死ぬほど残業して働く」みたいな働き方は割と認められなくなってきているらしい。(ただこれは部署によるところも大きい気がする。未だに月80時間残業くらい普通にやってる人間を知っているので)

ただパソコンにキーロガーみたいなのが仕込まれてて、パソコンを開いているか、どの画面を開いているかは常に記録されて勤怠システム上で閲覧可能になっているらしく、そういうところはアレだなと思った…

大企業での給与

自分が最初に会社をやめて転職したときには結構給与を下げた。それでも3年後くらいには最初の会社時代の給与を大きく超える額になったし、フリーランスになった今は金銭的にはかなり楽になっている。自分が給与を下げても最初の転職に踏み切れたのは、「このまま残っても給与が上がるイメージが持てなかった」というのもあるのだけど *1、友人と話していると30歳を超えてくると大企業もグッと給与が上がってくる事がわかった。会社として若手を早くマネージャーに上げようという流れもあるらしく、来年や再来年にマネージャーに昇進すると一気に800〜1000万くらいの年収にはなるらしい。

また、会社の中にいた一日中仕事と関係ない本を読んでるか昼寝してるだけの人、仕事は全然できないけどXXXエキスパートなる肩書を持っている人、そういう方々も700万から800万くらい年収をもらっていたということが分かってきて、大企業の懐の広さすげえなと思った。

大企業でのキャリア

今回話した友人は、国レベルの仕事や、世界中の拠点と関わる仕事をやっているらしく、そういうスケールの大きさはやはり大企業でしかできないことだろうなと思った、一方で本人も言っていたのだけど、「自分が何の仕事をしているか」「どんな成果を出したか」を個人レベルで語るのが結構難しい。エンジニア、デザイナーであれば、「こんなサービスを作りました」「こんなシステムを作りました」のように言えるし、SEでも「大企業XXXXプロジェクトのPMやりました」みたいなことは言いやすい。が、大企業の中だと何ていうか外部に一般化できない仕事をしている部署というのがチョイチョイあって、そういう部署に行くと職務経歴の説明が難しい。その友人も部署ではかなり評価されているようなのだけれど、「このままこの部署でマネージャーになって、つぶしの効く経験ができるのだろうか」ということに悩んでいるようだった。

ただ、自分はもともと大企業の中に残ってそこでしか通用しないスキルを磨くことに危機感があったのだけど、今は「エンジニアとしてコードを書いてアプリケーション / サービスが作れる」というだけのスキルはどんどんコモディティ化しているなと感じるし、正直なところWEBエンジニアとして次に何をしていくべきかということに結構悩んでもいる。なのでこういったキャリアの悩みは、単純に大企業だから / スタートアップ・ベンチャーだから、という話でもないのかもしれないなと今は感じている。

まとめ

正直にいうと、30歳になるまでは大企業で悩んでいる友人らには「スタートアップ / ベンチャーいいよ!」という勧め方をしていたのだけど、30歳超えて大企業のなかでも評価されている人らには中々そういう安易な勧め方はできなくなってきたなと感じる。本人がやりたいかどうかとは別にしても、大きな企業じゃないと出来ないことというのは確実にある。要は大企業、スタートアップ / ベンチャーにいることのどちらにもリスクがありメリットもデメリットもあるので、当たり前の話だけど一つの物差しで簡単に測れるものではないなと思う。

次に行く会社の選択肢としては基本的にWEBベンチャーしか考えていなかったのだけど、普通のWEB企業と比べても遜色ない環境でエンジニアとして働ける大企業もボツボツと出てきている印象があって、大企業の中で内製化された(しようとしている)部署があるなら、そういうところでも面白いかもなぁーと漠然と最近は考えている。

*1:もちろん一番の理由は自分でコードを書きたかったからだったけど

Vuetifyの TreeviewをDrag and Drop可能にしたコンポーネントを作った

この記事は suusan2go Advent Calendar 2019 - Adventar の6日目の記事です。

仕事でVuetifyを使っていて、 TreeviewコンポーネントのUIでドラッグアンドドロップ出来るようにする必要があった。Treeviewというのは以下のように、階層構造をもつデータ構造をいい感じにするコンポーネントf:id:suzan2go:20191207225659p:plain

が、以下のIssueにもあるように現状はドラッグアンドドロップをサポートする予定はないとのこと。

github.com

There is currently no plan to implement drag & drop functionality

というわけで、作った

作ったもの

こんな感じでコンポーネントを定義してやれば、Treeviewかつドラッグアンドドロップ可能なコンポーネントが使えるようになる。

<template>
<v-draggable-treeview
  group="test"
  v-model="items"
></v-draggable-treeview>
</template>

<script>
export default {
  data() {
    return {
      items: [{ id: 1, name: "hoge", children: [{ id:11, name: "hoge-child1" }] }]
    }
  }
}
</script>

github.com

見ての通り、もともとのTreeviewのUIでドラッグアンドドロップが可能になっている

https://user-images.githubusercontent.com/8841470/70327688-b6ca2800-187a-11ea-907e-79d7dc3afca9.gif

中身

実装はめちゃくちゃ力技で、オリジナルのTreeviewがコンポーネントが描画するDOM構造をもとに、Vue.Draggable を噛まして子要素をドラッグアンドドロップ可能な作りで組み直している。そのためprops / event / slotも自分が必要だった最低限しか実装していない… もともとはVuetifyのコンポーネントを拡張して作ろうと思ったのだけど、普段使ってないclassコンポーネントを読み解くのが辛かったので、とりあえず仕事上必要になった最低限の仕様をDOM構造だけトレースして実装するにとどめている。

難しかったのは、複数のTreeA、TreeB... のように複数のTreeがあったときに、TreeBのchildrenからTreeAのchildrenに要素を移した場合の挙動で、結論からいうとこれは単純に親にprops / emitしていく形式だとレースコンディションが起きて意図した結果にならない。TreeBのchildrenからTreeAのchildrenに要素を移した場合にprops / emitを上のコンポーネントに伝えていくと以下のようなことが起こる。

  • [TreeA] から [TreeB] に TreeAの子要素aを移す
  • ① [TreeB]に子要素aを追加したvalueをemitする
  • ② [TreeA]の子要素aを抜いたvalueをemitする

rootコンポーネント[TreeA, TreeB] のようなvalueをpropsとして渡されているとすると、全てをprops / emitで処理する場合にはrootコンポーネントで以下のような形で emit を実施したい。

  • this.$emit('input', [TreeA, 更新後のTreeB])
  • this.$emit('input', [更新後のTreeA, 更新後のTreeB])

しかし実際には①でemitした結果はすぐにpropsとして反映されるわけではないので、 以下のようになりドラッグアンドドロップした要素がどこかに消えてしまう。

  • this.$emit('input', [TreeA, 更新後のTreeB])
  • this.$emit('input', [更新後のTreeA, TreeB])

というわけで、全て一度localのstateにして受け取って、更新があったらemitするような形式にしている。というか仕事で必要だったのは一つのTree内での並べ替えだけで、ライブラリ化するにあたって全てのTreeで要素を異動できるように変えたんだけども、思いの外これが大変で時間を食ってしまった。

まとめ

コンポーネント化してnpmパッケージ化するにあたっては、クックブックがとても参考になりました。Rollup触るのも初めてだったけど割とシュッとかけてよかった。

jp.vuejs.org

Deep Workを読んだ

この記事は suusan2go Advent Calendar 2019 - Adventar の5日目の記事です。

この本を読んだあとも別にTwitterをやる頻度が落ちてない時点で「察し・・・」という感じではるのだけど、 少し前にDeep Work という本を読みました。

最近あまり集中して仕事をしてないなという感覚が結構続いていたときに、だいぶ前にito naoyaさんがRebuild.fmで紹介していたことを思い出して買いました。

本の内容

主張はシンプルで、以下。

  • メールを常にチェックするとか、TwitterFacebookを見るとかそういうことやってると、集中力が低く注意散漫な状態で仕事をすることになるぞ!それはシャローワークというんや!
  • SNS見るのやめろ!集中して仕事に没頭しろ!それがDeep Workや!

2行で終わるような話ではあるのだけど、Deep Workをすることでどんなに素晴らしい成果をだせるか、そしていかにDeep Workな環境を作るかということが本書のなかでは解説されています。 ビル・ゲイツが年に二度引きこもって「考える週(Think Weeks)」を設け、その間は引きこもってただ本を読んだり大きな構想を練ったりするみたいな話がDeep Workの例紹介されていますが、当然普通に働いている人が「Deep Workするので今週は引きこもるわ!」とかいうのは難しいわけで、いろんなパターンでDeep Workする方法が紹介されています。

自分がやっていること

本書のなかで書かれている内容のなかでも朝仕事を始めるときに、タイムボックスを作って予定をたてるということを意識してやるようになりました。もちろん今までも今日何やるかというのは当然考えてやってたわけですけど、それをもうちょっと厳密にちゃんとやるようになったということです。できるビジネスマンなら当たり前にやってそうなことなので今更感あってとても恥ずかしいんですけど、具体的には個人のメモ書きとして使っている notion に、以下のようなフォーマットで朝やることを書いて帰るまえに振り返るということをやるようになりました。

## 今日やること
- xxxxの実装を終わらせる ( 10:00~13:00 )
- 昼飯 ( 13:00~14:00 )
- xxxx についてドキュメントにまとめる (14:00〜15:00)
- MTG(15:00〜16:00)
.
.
.


## 明日やること

## K

## P

## T

タイムスパンを区切ることでダラダラTwitterをみながら仕事をしたりせず、良い緊張感をもって仕事ができるようになりました。また自分の想定よりも時間がかかっていることがわかるので、自分の作業見積もりを改善する機会にもなっています。ポモドーロとかも過去に試したんですけど、自分は集中すると数時間没頭したくなるのであまりリズムが合わなくて、なんとなくこんな感じの運用になっています。ただ最近は集中しすぎて仕事をやめられないというパターンもあり、数時間座りっぱなしで結果的に夕方になってお昼ご飯を食べるみたいなパターンも出てきて、あまりよくないなと思っているところ。Apple Watch を買って座りすぎを注意してもらおうかなー。

まとめ

本自体は結構著者の主観による主張みたいなものも多い気がしましたが、自分の仕事の仕方を見直す機会にはなったのでよかったのかなと思います。昔、一ヶ月以上かかる見積もりの機能をどうしても早く出す必要があったとき、エンジニア2人で集中スペース的なところに閉じこもってやったら一週間かからずに完成したみたいなことがあったんですが、あれは今思えば Deep Workだったのかなー。

GoのORM、SQLBoilerのススメ

この記事は suusan2go Advent Calendar 2019 - Adventar の4日目の記事です。

フリーランス始めてから、エムスリーでお世話になった @maeharin さんがCTOをしてる ANNONE という会社をお手伝いしています。自分はその中で、Goによるアプリ向けのAPI,Reactによる管理画面、Flutterアプリ、CloudSQL=> BQの同期、新規事業のRails newなどと幅広く色々とやらせてもらっているのですが、今回はあのね というアプリ向けに作られたGoのAPIのDBアクセスをSQLBoilerに切り替えた話をします。

Go APIの構成

特定のフレームワークは使っておらずルーティングには gorilla/mux、DBアクセスには sqlxを使っていて、シンプルなレイヤードアーキテクチャを採用した以下のような構成になっています。

api
├── application
├── cmd
├── controller
├── domain
├── middleware
├── migration
├── repository
├── util
└── view

DBアクセスはrepositoryに記載されたinterfaceを実装する形で実現されています。

type Repository interface {
    FindClinicByID(q sqlx.Queryer, clinicID int64) (*domain.Clinic, error)
    CreateClinic(tx *sqlx.Tx, clinic *domain.Clinic) (int64, error)
}
// FindClinicByID IDでクリニックを検索する
func (r *repository) FindClinicByID(q sqlx.Queryer, clinicID int64) (*domain.Clinic, error) {
    c := domain.Clinic{}
    query := `
      select
          c.*
      from clinics as c
      where
          c.id = $1
  `
    if err := sqlx.Get(q, cd, query, clinicID); err != nil {
        if err == sql.ErrNoRows {
            return nil, errors.WithStack(NewRecordNotFoundError(fmt.Sprintf("clinic(id: %d) is not found", clinicID)))
        }
        return nil, errors.WithStack(err)
    }
    return cd, nil
}

// CreateClinic クリニックを登録する
func (r *repository) CreateClinic(tx *sqlx.Tx, diary *domain.Clinic) (int64, error) {
    query := `
      insert into clinics(
          , name
          , create_timestamp
          , update_timestamp
      ) values (
          , :name
          , :create_timestamp
          , :update_timestamp
      )
      returning id
  `

    stmt, err := tx.PrepareNamed(query)
    defer stmt.Close()
    if err != nil {
        return int64(0), errors.WithStack(err)
    }
    var id int64
    err = stmt.Get(&id, &diary)
    if err != nil {
        return int64(0), errors.WithStack(err)
    }

    return id, nil
}

sqlxによるDBアクセスのpros / cons

Goではありませんが、過去のプロジェクトでDomaなどSQLを書いてそれをオブジェクトにマッピングする形式のライブラリは使ったことがあり、SQLを書いてオブジェクトにマッピングするという手法はシンプルで気に入っています。特に複数のテーブルをジョインしてオブジェクトにマッピングしたいといったこともシンプルに実現できますし、コードを読めばどんなクエリを発行しようとしてるのかすぐに見える点もよいです。

しかしながら、スタートアップで開発スピードが要求され、変更も多い環境のなかでは、辛い点も見えてきました。

Insert / Updateの記述が辛い

上にも書きましたが、テーブルが増える度に以下のようなクエリとそれをマッピングするコードを毎回1から書く必要があるのは結構しんどいものがあります。カラムが増えたり変わった場合にはも他のクエリで使っているinsert / update文も忘れずに追随していかなければいけません。

 query := `
        insert into clinics(
            , name
            , create_timestamp
            , update_timestamp
        ) values (
            , :name
            , :create_timestamp
            , :update_timestamp
        )
        returning id
    `

そもそも開発初期段階や / 70%の機能ではシンプルなCRUDで十分なケースも多い

柔軟にSelect文がかけるというのはとても良い体験なのですが、全てのテーブルでSelect文をガッツリ書く必要があるかというとそんなことはなく、体感的には70%以上の機能ではシンプルなCRUDがシュッと実現できればそれで十分という感覚があります。むしろ一つテーブルを追加する度にCRUDSQLとstructにマッピングするコードのボイラープレートを大量に書く必要があるのは、特に新規機能開発で2、3のテーブルを追加する必要がある場合には結構ストレスでした。

TypeSafeではない

最初の問題点にも繋がりますが、Create文やUpdate文を長々と書く必要がある割にカラム名TypoしたりDBと対応の異なる型をStruct側に定義してしまったりしてもコンパイル時には気が付けません(これを Type Safeでないと言っていいのかわかりませんが・・・)。カラム追加や変更が合った場合に特にInsert / Update文で間違えなくこれに追随していくのはかなり大変に思えました。

SQLBoiler

上記にあげた課題を全て解決しつつ、普通にSelect文が書こうと思えば書けるようなツールはないだろうかと調べていたところで、以下のブログで紹介されているSQLBoilerというツールにたどり着きました。

Go の ORM / query builder 消耗日記 - blog.izum.in

github.com

SQLBoilerとは

SQLBoiler is a tool to generate a Go ORM tailored to your database schema. とあるように、DBのスキーマをもとにテーブルに対応するstructとCRUDな操作を提供するfunctionを自動生成してくれます。

REATE TABLE pilots (
  id integer NOT NULL,
  name text NOT NULL
);

ALTER TABLE pilots ADD CONSTRAINT pilot_pkey PRIMARY KEY (id);

CREATE TABLE jets (
  id integer NOT NULL,
  pilot_id integer NOT NULL,
  age integer NOT NULL,
  name text NOT NULL,
  color text NOT NULL
);

ALTER TABLE jets ADD CONSTRAINT jet_pkey PRIMARY KEY (id);
ALTER TABLE jets ADD CONSTRAINT jet_pilots_fkey FOREIGN KEY (pilot_id) REFERENCES pilots(id);

生成されるコードはこんな感じです。これとは別にFind等のメソッドがたくさん生成されます。

type Pilot struct {
  ID   int    `boil:"id" json:"id" toml:"id" yaml:"id"`
  Name string `boil:"name" json:"name" toml:"name" yaml:"name"`

  R *pilotR `boil:"-" json:"-" toml:"-" yaml:"-"`
  L pilotR  `boil:"-" json:"-" toml:"-" yaml:"-"`
}

type pilotR struct {
  Licenses  LicenseSlice
  Languages LanguageSlice
  Jets      JetSlice
}

type Jet struct {
  ID      int    `boil:"id" json:"id" toml:"id" yaml:"id"`
  PilotID int    `boil:"pilot_id" json:"pilot_id" toml:"pilot_id" yaml:"pilot_id"`
  Age     int    `boil:"age" json:"age" toml:"age" yaml:"age"`
  Name    string `boil:"name" json:"name" toml:"name" yaml:"name"`
  Color   string `boil:"color" json:"color" toml:"color" yaml:"color"`

  R *jetR `boil:"-" json:"-" toml:"-" yaml:"-"`
  L jetR  `boil:"-" json:"-" toml:"-" yaml:"-"`
}

type jetR struct {
  Pilot *Pilot
}

DBとの対応だけではなくJSONのタグも記述してくれるので、自分は後述する通り使っていませんがDBで取得した結果をそのままレスポンスとして返せばいいようなシンプルなアプリでは便利かもしれません。外部キー制約をもとにrelationを自動で貼ってくれ、nullableなものには null パッケージを適用してくれます。

基本的なデータの操作

詳細はREADMEに譲りますが、SQLBoilerでコードを生成すると以下のような操作は全て自動生成されたstructと関数だけで実装することが可能になります。

// IDで引く
clinicl, err := models.FindClinic(db,  clinicID)
// 全て取得する
clinics, err: = models.Clinics().All( db)
// where
clinics, err := models.Clinics(qm.Where("pref_id = ?", prefID), qm.And("name = ?", name)).One(db)
// insert
err :=clinicl.Insert(tx, boil.Infer())
// update
_, err := clinic.UPdate(tx, boil.Infer())
// Relationships
prefecture, err := clinic.Prefecture()

また必要なときにはsqlxのようにSQLを書いてオブジェクトにマッピングするという手法を取ることも可能です

// Custom struct for selecting a subset of data
type JetInfo struct {
  AgeSum int `boil:"age_sum"`
  Count int `boil:"juicy_count"`
}

var info JetInfo

// Use query building
err := models.NewQuery(Select("sum(age) as age_sum", "count(*) as juicy_count", From("jets"))).Bind(ctx, db, &info)

// Use a raw query
err := queries.Raw(`select sum(age) as "age_sum", count(*) as "juicy_count" from jets`).Bind(ctx, db, &info)

あのねアプリ向けAPIでの使い方

ActiveRecord-like productivity と語っているため?かはわかりませんが、SQLBoilerは自動生成したstructを使ってDBの内容をそのままJSONにして返すということも可能な作りになっています。しかし、以下の観点から元々存在していたrepositoryレイヤを活用してSQLBoilerの依存範囲をrepositoryパッケージにとどめています。

  • もともと存在するrepositoryレイヤを活用すれば、sqlxから安全に移行することが可能であること
  • SQLBoilerへのアプリケーションの依存を強めると、SQLBoilerから脱却することが難しくなること
  • Go言語の性質上、生成されたstructの挙動をオーバーライドするような実装をすることが難しいこと

実装としては以下のような感じになっており、アプリケーション用のstructである domain.Clinic をそのままSQLBoilerに渡さずmodels.Clinicマッピングし、また自動生成された models.Clinicをrepositoryの外には出さずに domain.Clinic し直すという感じになっています。sqlxのときにSQLを書いていたのがオブジェクトのマッピングに変わってしまっているといえばそうなのですが、Goのコード上でかけるのでIDEのサポートでfill-structといったツールを使ってガッとstructを埋めることもできますし、何よりちゃんと補完が聞くのでSQLを生で書いたいたときよりもかなり生産性があがりました。

// CreateClinic クリニック情報を登録する
func (r *repository) CreateClinic(tx boil.Executor, clinic *domain.Clinic) (int, error) {
    model := models.Clinic{
        Name:            clinic.Name,
        CreateTimestamp: time.Now(),
        UpdateTimestamp: time.Now(),
    }
    err := model.Insert(tx, boil.Infer())
    if err != nil {
        return 0, errors.WithStack(err)
    }

    return model.ID, nil
}

// FindClinicByID IDでクリニックを検索する
func (r *repository) FindClinicByID(q boil.Executor, clinicID int) (*domain.Clinic, error) {
    model, err := models.FindClinic(q, int(clinicID))
    if err != nil {
        return nil, errors.WithStack(err)
    }
    clinic := mapToClinic(*model)
    return clinic, nil
}

structの詰め替えをするのは非効率ですし、大量のデータを処理しなければいけないときにはパフォーマンス上の問題となる可能性もありますが、通常のアプリ向けAPIでは問題になるケースはそれほど多くないのではないでしょうか。

使ってみた所感

元々の狙いどおり、sqlxで辛さを感じていた箇所はSQLBoilerでかなり楽をすることができるようになりました。一方でSQLBoilerも万能ではなく、例えば現在のところLeft Outer Joinをサポートしていません。

github.com

複雑なクエリを発行する必要がある場合や上述したstructの詰め替えコストを許容できない場合には、sqlxは引き続きよい選択肢なのかなと思います。場合によっては併用もありなのかなーと思いつつ一つのアプリで2つのDBライブラリを要求するのはなぁ・・・みたいなことを思っています。

PR枠

ANNONE では、こんな技術を使って開発してます。

全方面エンジニア募集中のようなので、興味のある方はぜひ @maeharin さんにDMを!

大変な家事・育児をお金の力で楽にする

この記事は suusan2go Advent Calendar 2019 - Adventar の3日目の記事です。

自分は去年の11月に二人目の子供が生まれて、現在3歳になる娘と専業主婦の妻と合わせて4人家族で生活しています。2人の育児は、あんなに大変だった1人目の育児がイージーモードに感じるくらい大変でした。特に夜ご飯〜お風呂〜寝かしつけの時間帯はワンオペだと辛く妻だけではダウンしてしまうので遅くとも20時には帰宅、下の子は夜泣きがいまだに酷くて一緒に寝ている妻は朝起きるのがかなりしんどいので(定期的に自分も交代しています)、朝ごはんと幼稚園のお弁当はほとんど自分が作っています。

フリーランスで稼働時間の融通がきく今だからこそ自分も家事育児を最大限やってなんとか回ってるというのが実情です。しかしフリーランスといえど仕事の忙しくなるタイミングというのはあって、そうなると自分も妻もキャパを超えてしまってめちゃくちゃストレスになっていました。

エンジニア関連で子育てというと、家族でSlack使って家事育児を効率化とかTrelloで情報共有とか家庭内スクラムだとかキラキラした事例がたくさんあります。エンジニアとしてそういうのもいいなーと思いますが、そんなこと考える余裕もないのでとにかく早く何とかして楽になりたいというのが正直な気持ちです。というわけで、フリーランスで働いていて比較的金銭的に余裕のある今だからこそできる「金で殴る」という解決方法を取ることにしました。

というわけで参考になるかわかりませんが、自分がいまお金をかけていることを紹介します。

育児

ベビーシッター

まず真っ先に考えたのがこれでした。週一でも預けられるとかなり楽になるはずだなーと。しかしながら、0歳児にも対応してくれて、かつ3歳児も一緒に見てくれるというベビーシッターの方はなかなか見つからず、見つかっても自分たちの希望するような時間帯で対応いただける方は見つかりませんでした。下の子も最近1歳になったので、また検討はしたいなと思っています。

乳幼児一時預かり事業

自分の住んでいる横浜市では乳幼児一時預かり事業というのをやっています。これは生後57日~小学校入学前の子供を理由を問わず預かってくれるもので、市の事業だけあって自己負担は1時間あたり300円以下とかなり良心的です。 www.city.yokohama.lg.jp

探してみると自分たちの近所でもこの事業に参加している施設があったので、いまは週一で子供を預かってもらっています。ただし事前予約は二ヶ月前からかつかなり激戦で、キャンセル待ちからの繰り上げでギリギリ預けられるみたいなパターンが多いです。定期預かりの申し込みも年1かつ先着順のようなので、かなり熾烈な争いになっています…

託児所の一時預かり

託児所も利用しています。こちらは買い物やクリニックにいくときに妻が使っていますが、当日予約OKの施設は特にありがたいです。上記の乳幼児一時預かり事業と比べると当然ですが少し高くなり、1時間あたり1000円~といった感じになっています。

mamas-smile.com

家事

食洗機やお掃除ロボットは既に導入済みで、以下のようなサービスを利用しています。

料理

二人目が生まれる前から利用していますが、平日の夜ご飯はヨシケイに頼っています。メニューを考える必要がなく、食材を買いに行く必要もないのでお勧めです。

yoshikei-dvlp.co.jp

掃除

家事代行は色々とサービスがありますが、子供のいる家に知らない人を上げるとなると仲介しかしない事業者のサービスはちょっと嫌だったので、ちょっと割高ではありましたがダスキンのメリーメイドというサービスを利用していて、週1日来てもらっています。

www.duskin.jp

家事代行のサービスでは専門の掃除器具などは使わず、家にあるもので掃除するという形態になっているのですが、さすがそこはダスキンで掃除のクオリティはめちゃくちゃ高いです。シンクやお風呂が「そういえば新築のときはこんな色してたな…」と思うほどピカピカになりました。もちろん掃除だけでなく洗濯物を取り込む、洗い物をするといった家事もやってくれます。ただし料理は別料金になってしまうようです。こちらは首都圏だと、1週間に1回または、2週間に1回(1回・1名・2時間)で9,900円となっています。

まとめ

上記のようなサービスを利用しはじめて家事・育児の負担が幾らかは軽減され、よくイライラしてしまっていた妻も自分も少し心に余裕が持てるようになりました。育児・家事を楽にしたいけどどうしたらいいか分からない!という人も多いと思うので、誰かの参考になれば嬉しいです。