ブログのしゅーくりーむ

技術的なメモとかライフログとか。

Netflix のデータパイプラインを読み解きたい

Netflix はマイクロサービスアーキテクチャ界においてプロダクションで成功例を積んでいる、いわば大先輩だと思われます。 彼らは数多くのイベント登壇や techblog の記事、 GitHub 上による OSS の公開を行っており、それらからアーキテクチャやその変遷を垣間見ることができると考えています。

本記事では筆者が最近悩んでいる、マイクロサービス前提の世界でのログ収集基盤において、 Netflix の様々な事例を調べた結果をつらつら書いていこうと思います。 あらかじめ本記事は正確性を担保しておらず、あくまで筆者個人が調べることができた範囲での記述に留まることをお断りさせていただきます。

Suro: 分散データパイプライン

2015 年くらいにメンテが止まってしまったのですが、分散データパイプラインをうたう Suro というソフトウェアが存在しました。

Suro に関しては解説記事も書かれていて、イベントログを集約し後続の S3 、 Kafka 、その他イベント処理システムに転送する役割を担っていたようです。 またバッチとリアルタイム、両方の性質を持つデータを受け付けていたようです。

medium.com

Suro がメンテされなくなった経緯は筆者もよく分かっていませんが、後続のデータパイプラインの仕組みを見るにバッチとリアルタイムの両方の要望に答えるのが辛かったのか、あるいはデータの分析基盤の変更に合わせて作り直す決定をしたものかと推測しています。

Cassandra のデータ転送のバッチレイヤーと Ursula によるスピードレイヤー

Suro と入れ替える形かどうそうでないかは定かで無いですが、以下の資料によると Lambda Architecture に似たバッチレイヤーとスピードレイヤーの分離を行っているのが見て取れます。

https://qconsf.com/sf2016/system/files/presentation-slides/netflix-cloud_analytics.pdf

バッチレイヤーでは、 Netflix で広く使われている Cassandra をベースに SSTable をダンプして、それを aegisthus という Cassandra 向けバルクデータパイプラインソフトウェアを使って、 Netflix がデータウェアハウスに使っている S3 に転送していたようです。 また SSTable のダンプは上記資料では明示されていないのですが、 Priam というツールが公開されておりこれを使っているのではないかと推測しています。

ちなみにこの aegisthus も、 2017 年にメンテナンスモードに移行し今ではコミットされていないようです。 最後のコミットに対してされたコメント を見るに、 Cassandra のデータのデータウェアハウスへの転送は今後 Spark のジョブに移行することを検討しており、 2018 年 8 月時点ではまだ移行途中?でかつ新しいツールは OSS にはなっていないようです。

スピードレイヤーを支える Ursula というソフトウェアの存在も上記スライドでは触れられていますが、こちらは OSS になっている気配はありませんでした。

データパイプライン周辺技術

パイプライン以外にも、 Netflixビッグデータ処理ジョブのオーケストレーションツールである genie や、 メタデータ管理ソフトウェアである metacat など多くの小道具を揃えているようです。

余談

上記のうち Suro や aegisthus については、 "マイクロサービスアーキテクチャ" にも記述があり参考になる部分があるかも知れません。

www.oreilly.co.jp

ここで挙げたツールは Netflix が Cassandra を広く採用していることや、大規模なデータを持つがゆえの様々な課題、背景などもあって作ったのだと考えられます。 これらがそのままマッチして利用できる企業は多いわけではないと思われますが、これらの変遷や構成などが読み解けるものがあるのかも、などと思いました(小並感)

株式会社メルカリに入社して1年が経過した

 ちょうど一年前の 2017年8月16日に株式会社メルカリに入社しました。キリがいいこともあり、ここらで個人的な振り返りをつらつら書いてみます。あらかじめ、はっきりいって個人の日記レベルの内容であることをお断りさせていただきます。

転職の経緯から入社まで

 2017 年 1 月頃から転職を考え初めていました。元の所属も Web サービスをなりわいとしており、それなりの愛着もあり良い経験をさせて頂いてはいたのですが、自分の能力向上を目指しつつより早いペースでプロダクトを提供していく体験を積みたく粛々と準備をしていました。 そんな中メルカリは、急成長しているサービスを提供しており外部への発信も多く、また所属エンジニアも強力な方々が多く自分にとっても良い経験になると踏んだため応募した次第でした。

 応募に際して僕の場合は特に知り合いのツテやエージェントを通さずに、採用ページの募集フォームにダイレクトアタックを仕掛けて選考に進みました。敢えて反省点をひねり出すならば、何とか所属するエンジニアにアプローチを仕掛けて雰囲気などを確かめるとより良かった気はします。採用ページや各種アウトプットからは見えない現場での葛藤やそこに根ざす文化を確認するのは、重要なことでしょうし。

入社後から今まで

 2017 年内はメルカリ SRE チームに所属して、ミドルウェア開発マンとして主にデータ収集基盤とその他細々としたコンポーネントの開発に携わっていました。 その後は 2018 年 2 月頃?(うろ覚え)に子会社メルペイに籍を移し、一部マイクロサービスの開発や、メルペイのためのデータ収集基盤を、マイクロサービスアーキテクチャに従った世界でのプロダクトでの課題も吸収しつつ考えるという仕事に従事し始めました。

 この間に起こった大きな変化として、個人的には前職が基本オンプレ前提のインフラ構成であったところからクラウドのリソースを多用する環境になったことと、会社的にはマイクロサービス化へ舵を切ったことです。 特に後者は今でも悩ましい事がいくつもあり、刺激の多い日々を過ごせているように感じています。

反省点と今後の目標

 ここ一年多くの刺激を受けつつ過ごせましたが、まだまだ自分が納得できるほど大きな成功に到達していないとも思えます。 元々、不確定な世界の中こつこつと少しずつ成果を積み上げ模索するのが好きであり得意であると考えているのですが、そのスタイルでは到達しにくい領域についても考えるべきかもという危機感も持ってきています。 そういった現状の自身の不足しているスキルやできる領域、やれる領域を明確にしていくのが今後の課題なのかなぁ、などと考えています。

おまけ: 仕事以外のここ一年の活動

ngx_mruby のノンブロッキング sleep

 メルカリ SRE チームに所属した際、そういえばちゃんと nginx を理解していないなと危機感を覚えつつ、勉強も兼ねて mruby スクリプトを動かすためのモジュール ngx_mruby にノンブロッキング sleep する仕組みを追加するパッチを書いてみていました。 このパッチは結局幾つかの問題にハマり、自分だけでは解決できず半ば放置していたのですが、 matsumotory さんはじめ GMO ペパボさんの方々に拾っていただき、 RubyKaigi 2018 で紹介されるまでに至りました。 ・・・諦めて単純に放置するでなく、もう少し能動的に相談しに行ったりとかすべきだったかも、など様々な反省があります。

rubykaigi.org

Go でいくつかツールを書いた

 すでに記事と化したものもあるのですが、 Go でいくつかツールを書きました。

syucream.hatenablog.jp

syucream.hatenablog.jp

自分の業務に直結するスキルが得られる以外にも、やっぱ何も考えなくてもシングルバイナリで使えるとツールとしては便利だと考えたため、ひたすら Go で書いてます。

で、

誰?

Cloud Spanner の DDL parser と DDL 変換ツールを作った

Cloud SpannerDDL parser の Go 実装と、 Spanner DDLMySQL DDL っぽいものに変換するツールを作りました。 本記事はこれらの紹介になります。

github.com

github.com

spar: Cloud Spanner DDL parser in Go

Spanner の DDLこんな感じ で、 SQL 方言に見えるもののカラムの型だの INTERLEAVE だの、仕様に対して独自の文法も結構な割合で持っているように見えます。 今回作った spar, DDL parser はこれらをパースして CREATE 文や ALTER 文などの構成要素をまとめて返してくれます。

リポジトリに同梱した DDL syntax checker のコードがシンプルな使用例としても有用です。 parser package の持つ Parse() に読みたい DDL の Reader を渡すとパースした結果を返してくれます。

package main

import (
    "io/ioutil"
    "log"
    "os"
    "strings"

    "github.com/syucream/spar/src/parser"
)

func main() {
    data, err := ioutil.ReadAll(os.Stdin)
    if err != nil {
        log.Fatal(err)
    }

    _, err = parser.Parse(strings.NewReader(string(data)))
    if err != nil {
        log.Fatal(err)
    }
}

パーサー部分は goyacc を使って生成しています。 また lexer 部分の一部は https://github.com/benbjohnson/sql-parser を参考にしています。

jackup: Jack up your DDL and translate between MySQL and Spanner

スパナときたら今度は別の工具も欲しくなってくると思われます。ということでジャッキを作りました。 jackup は標準入力か -f オプションで渡したパスのファイルの DDL を参照し、 spar を使ってパースした後、(現在は CREATE TABLE , CREATE INDEX しか読んでくれませんが) MySQL の同じような構造を持つ DDL を出力してくれます。 残念ながら一部の変換、特にカラムの長さに関する制限が Spanner のほうがゆるい部分があり、無理やり変換したり変換せず無視したりもしています。

このツールは Spanner を捨てて MySQL に以降したりなどクリティカルなユースケースに対応するつもりはなく、もう少しゆるく変換したらどうなりそうか確認したり、 MySQL 周りのエコシステム、例えば MySQL Workbench による ER 図生成に食わせたりしてみるために作ってます。 なお、このあたりの SQL 方言とその間の相互変換は、現状良いソリューションが無く今回自分は自前で実装したのですが、今後は Apache Calcite に淡い期待を寄せてみてもいいのかなと思っています。

Apache Calcite • Dynamic data management framework

技術書典4で配布した同人誌の原稿データを GitHub で公開しました

以下の記事でご紹介した、弊サークルの技術書典4の新刊を GitHub で public repo として公開しました。

syucream.hatenablog.jp

リポジトリはこちらになります。

github.com

ビルド済み pdf は無く、またその他配布した原稿にしか含まれないコンテンツなどあるかもしれませんが、ご了承ください。

技術書典4振り返り

いい機会なので振り返りをつらつら書きます

執筆環境について

  • Re:VIEW で原稿を記述
  • CircleCI で textlint 実行と pdf 自動ビルドするようにした
    • 入稿直前になって pdf がコレジャナイ感出るのマジ勘弁!なので
    • base の Docker Image に vvakame/review を使わせていただきました
  • 細かなレビューは GitHub の pullreq or Issue ベースで実施

進捗管理について

  • GitHub Issues の Milestone で、いくつかマイルストーンとその締切を設定
    • ゆるめの締切にした
    • 最初から早割入稿前提で管理してた。追い込み記事にこのバッファがあることで少し精神的に助かった
    • あまり予定通りにいかなかったのは大きな反省点
  • 多人数での執筆管理の経験が今回で初めてだったので、学びが多かった

本のテーマについて

  • テーマを 1 つに絞るか、開き直って雑誌形式にすべきだった
    • テーマが 2 つあるせいで、そのテーマ間に関連があるものだと誤解させてしまった

その他

  • メンバーのモチベーションが途中で低減してしまった
    • モチベ管理まですべきかは悩みどころ。同人誌であるなら、個々人が自分の記事の品質に納得が行っているならまぁいいのかな?
  • 表紙絵の担当が決まったのが後半だった
    • 早めに調整!!

そして技術書典5へ〜

弊サークル「まいにちがきんようび。」は技術書典5にも参加します!たぶんマイクロサービス周辺技術について執筆陣が好きな要素をひろってまとめた新刊を出すことになるかと思います(マイクロサービス関連以外にも、オマケとして記事を書くかもです) 引き続きよろしくお願いいたします!!

embulk-input-datastore を作った

TL;DR

Cloud Datastore からデータをぶっこ抜いてくるための embulk input plugin を作りました。

github.com

普通に embulk gem install embulk-input-datastore した上で input plugin としてご利用ください。 参照するエンティティ、プロパティを絞り込むために GQL を指定可能にしているのでご利用ください。 (逆にそれ以外の参照方法をサポートしていません。。)

in:
  type: datastore
  project_id: "your-gcppj-123"
  json_keyfile: credential.json
  gql: "SELECT * FROM myKind"
...

細かい話

実装言語

Kotlin で実装しています。その理由としては Java よりシンプルに記述可能で、かつそれなりに事例があるので採用してみた次第です。

qiita.com

テストは Spek を用いて RSpec 的なシンタックスでテストコードを記述し、アサーションライブラリとして kotlin-test を使うようにしました。 大した記述量では無いのですが、まぁまぁ様になっている気がします。

embulk の input plugin としての挙動

Cloud Datastore はいわゆる NoSQL データベースでありスキーマが固定されているわけではありません。 従ってエンティティによって同じプロパティ名で違う型のプロパティが存在したり、値がそもそも存在しなかったりします。

これらを embulk の(というか MessagePack の?)型にマッピングするのが面倒くさく、あまり良い対応方法も思いつかなかったので embulk-input-mongodb と同様単一の JSON フィールドに結果をまとめて出力するようにしました。 input plugin として値の読み出しは行うが、変換処理を行う場合は別の filter plugin などでなんとかせい、というスタンスです。

gradle-embulk-plugin の利用

gradle-embulk-plugin を便利に使わせていただきました!主に初期のテンプレートコード生成や gradle embulk_run による動作確認などでお世話になりました。 これから gradle でビルドしつつ embulk plugin 作るような方にはオススメしたいところです。

動作例

一応載っけてみます。 以下のような myKind カインドに対するエンティティたちが存在する場合に

f:id:syu_cream:20180626000814p:plain

こんな感じの設定ファイルを用意して

in:
  type: datastore
  project_id: "<my-project-id>"
  json_keyfile: credential.json
  gql: "SELECT * FROM myKind WHERE myProp >= 100 AND myProp < 200"

out:
  type: stdout

実行することで以下のような出力が得られました。

$ embulk run examples/datastore2stdout.yaml
2018-06-26 00:11:05.173 +0900: Embulk v0.9.7
2018-06-26 00:11:07.219 +0900 [WARN] (main): DEPRECATION: JRuby org.jruby.embed.ScriptingContainer is directly injected.
2018-06-26 00:11:14.783 +0900 [INFO] (main): Gem's home and path are set by default: "/Users/ryo/.embulk/lib/gems"
2018-06-26 00:11:17.364 +0900 [INFO] (main): Started Embulk v0.9.7
2018-06-26 00:11:17.639 +0900 [INFO] (0001:transaction): Loaded plugin embulk-input-datastore (0.1.1)
2018-06-26 00:11:17.743 +0900 [INFO] (0001:transaction): Using local thread executor with max_threads=8 / output tasks 4 = input tasks 1 * 4
2018-06-26 00:11:17.784 +0900 [INFO] (0001:transaction): {done:  0 / 1, running: 0}
{"extra":{"defaultProperty":"hogefuga"}, "myProp":100}
{"myProp":150}
2018-06-26 00:11:19.708 +0900 [INFO] (0001:transaction): {done:  1 / 1, running: 0}
2018-06-26 00:11:19.714 +0900 [INFO] (main): Committed.
2018-06-26 00:11:19.715 +0900 [INFO] (main): Next config diff: {"in":{},"out":{}}

おわりに

なんとなく動くところまで持っていけたので記事にしました。 Cloud Firestore も同じ要領で plugin 作れたりするのかなとぼんやり考えたりしました。

protobuf の Marshal/Unmarshal や Any 型について確認していた

あるメッセージの送り元と送り先でスキーマを共有してそのスキーマに基づいたメッセージをやり取りしたいことが多々あると思います。 そんな際にスキーマの記述を protobuf で行い、かつメッセージをシリアライズして送る構想について調査したメモを本稿で記載します。

背景

IoT だとか Microservices Architecture だとかウェブ・アプリサービスのユーザ行動分析だとか、AWS,GCP だとか何とか、世の中には多くのデータを生成するソースやデータ格納ストレージが生まれてきています。 そんな中、どのようなスキーマ・フォーマットでデータを送信するかという点は未だ多くの開発者を悩ませているのではと考えます。

これがビジネス上スケジュールの要件が厳しかったり、プロダクトの変化が大きかったり、データ送信ロジックを書く開発者が蓄積したデータを活用するシーンでサポートできるのであれば、スキーマレスデータベースにひとまずデータを格納しまくるのも良いかと思います。 しかしながら個人的にはそうではない状況や、将来的な組織・人員の拡充を考えるとスキーマがあって欲しい気持ちが高まります。 とはいえ、なるべくスキーマの変更には柔軟に対応できるようにあって欲しいものです。

f:id:syu_cream:20180605015503p:plain

スキーマ定義をどうするかについて最初に思いついたのは JSON Schema なのですが、人が読み書きするのには適さないと感じます。 その次に本稿で触れる、最近は gRPC メソッド定義などでもよく用いられる protobuf によるスキーマ定義を思いつきました。 (実際にいくつかのサンプルに触れる前に、以下の記事に影響を受けました。 protobuf にこれから入門してみたい方は読んでみていただけると!)

今さらProtocol Buffersと、手に馴染む道具の話

確認したかったこと

protobuf によるスキーマ定義自体は上記の記事より良さげだなと思っていたのですが、スキーマの更新の柔軟性、具体的には以下の点が気になっていました。

  • メッセージのフィールドを増やした際の受信側の挙動
  • タグの値が書き換わるような更新が起こった際の受信側の挙動
  • Any による型情報付きメッセージの受信側の挙動

これらを以下のシナリオのもと動作確認してみることにします。

  • Twitter みたいなサービスのユーザの行動ログを protobuf で定義する
  • 最初は v1 として Event 型で行動ログのスキーマを定義する
  • ある程度時期が経過した際、ツイートの URI のような追加情報を足したくなったので RichEvent という新しい型でログを扱うことにした
  • 更に時期が経過した際、根本的に構造を見直したくなり、新しい v1 Event 型を定義してログを扱うことにした
  • ログ送信コードを触る人と送信されたログを収集する人は異なり、システムも異なり、うまく連携できず受信側のスキーマ更新作業が遅れる場合がある

確認結果

NOTE: ここでは Go の protobuf packages を使ったりすること前提で書いています

実験用コードを以下に記載します。

github.com

異なるバージョン間の Marshal/Unmarshal

あまり綺麗にまとまっていませんが、以下のような形になりました。フィールドのタグと型が一致している部分に関してはバージョン更新が行われなかった場合でも最低限拾ってくれます。 しかしながら protobuf は NON NULL 指定をして Unmarshal 時にエラーを起こすようなことができない(はず?なにか良い方法があればコメントください)ので各言語向けに protoc plugin が吐き出す getter などで自前でチェックする必要がありそうです。

Check handling between messages might be compatible :
v1 -> v1.5
id:1 created_at:<seconds:1528128844 nanos:914007623 > event_type:TWEET user_id:1 value:"test tweet!"
v1.5 -> v1
id:1 created_at:<seconds:1528128844 nanos:914007623 > event_type:TWEET user_id:1 value:"test tweet!"

Check handling between messages might be NOT compatible :
v1 -> v2
id:1 event_at:<seconds:1528128844 nanos:914007623 > event_type:TYPE_TWEET
v2 -> v1
id:1 created_at:<seconds:1528128844 nanos:914007623 > user_id:1

Any

protobuf の Any 型は message の型情報を付与してくれるので、 Marshal するメッセージをこれで wrap してみます。 またメッセージを受け取る側では Unmarshal 前に型チェックを行うようにしてみます。 型チェックは Go では ptypes packageptypes.Is() で行えます。

これに基づき、 v2 Event を Unmarshal する際の動作は以下のようになりました。 v2 のメッセージを v1, v1.5 のメッセージとして Unmarshal しようとした際に ptypes.Is() のチェックが通らなくなっています。

Check handling Any :
type_url:"type.googleapis.com/logging.v2.Event" value:"\010\001\022\014\010\314\312\325\330\005\020\307\314\352\263\003\032\014\010\314\312\325\330\005\020\307\314\352\263\003 \001(\0020\0018\002B\013test tweet!"
Any is not loggingv1.Event
Any is not loggingv1.RichEvent
id:1 event_at:<seconds:1528128844 nanos:914007623 > processed_at:<seconds:1528128844 nanos:914007623 > event_type:TYPE_TWEET event_source:SOURCE_PUBLIC_TIMELINE user_id:1 user_agent:UA_IOS value:"test tweet!"

Any に wrap することによるバイナリサイズ増加

実験に用いた v1 Event 型で言うと、そのメッセージを直接 Marshal した場合と Any で wrap した場合とでは以下のようなサイズ差がありました。 当然ながら Any だと多少サイズが増えています。が、メッセージのフィールドが大きければ相対的に増加具合が目立たなくなるレベルになるかもしれません。

Check marshaled size w/ Any :
len(v1bin) : 33
len(v1anyBin) : 73

protobuf の周辺環境

夜に出ている protoc plugin を使って、 protobuf によるスキーマ定義を周辺ツールと連携して行うなど強力な開発・データ分析環境が構築できるかと思われます。

おわりに

protobuf によるスキーマ定義、なかなか良さそうに感じます。特に(メッセージサイズが増えることが許容できるなら) Any 型で包んでやり取りすると必要な際に簡単にメッセージ型チェックを行うことができ、便利だと思われます。 また protoc plugin も多数存在し、単に protobuf を直接使う以外にも他のスキーマ定義を生成するシンプルな DSL として使うのもありかも知れません(JSON Schema とか)。

この分野では Apache Avro と比較した検証ができていないので、どこかのタイミングでしてみたい気持ちもあります。

MySQL のテーブル定義から外部キー制約を推測するツールを作った

GW 中の自分へのプチ課題として、表題の通りのツール hakagi(葉鍵, Leaf and Key) を作りました。ツール名には特に意味はありません。

github.com

テーブル名やカラム名から外部キー制約を張れるような気がするカラムを選出し、制約追加のための ALTER TABLE をするクエリを吐き出したりします。

なぜ作ったのか

外部キー制約は言うまでもなくデータの不整合を避けるのに有用ですが、いくつかの理由からこれを用いない選択肢も存在すると思われます。 この辺りについては以下の記事なども参考にすると良さそうです。

外部キー制約を設けないことによるデメリットも幾つか生じると思っていて、その一つで自分が気になっている点として、「ER 図をツールで自動生成しにくい」があります。 例えば MySQL Workbenchsql ファイルや稼働する MySQL データベースから ER 図を自動生成してくれる機能を持ちますが、エンティティ間のリレーションは外部キー制約を無くすと追えなくなります。 この問題に対する緩和策として SchemaSpy を用いれば主キーをベースに外部キー制約を推測して ER 図生成を行ってくれます。 しかしながらこの推測のルールはあまり小回りが効くものではありません。

SchemaSpy のやっているような制約の推測を、図の自動生成とは分離してシンプルに扱い、かつルールを細かく追加したり ON/OFF したりしたい。 そう考え今回紹介するツールの実装を開始しました。

実装など

ワンバイナリでサクッと動かしたい希望があったため Go で書いています。 MySQL データベースからテーブル定義や主キーの情報を集めるため information_schema データベースを参照し、推測を行って制約追加クエリを標準出力に出します。 現在サポートする推測ルールは以下の通りです。

  • あるカラム名と同じ名前の、別のテーブルのカラムが存在し、かつそれが id という名前ではなくそのテーブルで主キーである場合外部キー制約を追加する
  • あるカラム名<別のテーブル名の単数形>_id であり、かつ別のテーブルで id が主キーである場合外部キー制約を追加する

標準出力に最終的に吐き出すフォーマットとしては今後は必要であれば PlantUML をサポートしたりしてもいいかも、と考えております。

動作例

以下のような、外部キー制約が全く設けられていないテーブル定義があったとします。

CREATE TABLE IF NOT EXISTS users (
  id    INT UNSIGNED NOT NULL AUTO_INCREMENT,
  name  VARCHAR(255) NOT NULL,
  age   INT UNSIGNED NOT NULL,
  email VARCHAR(255) NOT NULL,
  PRIMARY KEY(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS contents (
  id    INT UNSIGNED NOT NULL AUTO_INCREMENT,
  title VARCHAR(255) NOT NULL,
  text  TEXT NOT NULL,
  user_id INT UNSIGNED NOT NULL,
  PRIMARY KEY(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS content_comments (
  content_comment_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
  text               TEXT NOT NULL,
  content_id         INT UNSIGNED NOT NULL,
  user_id            INT UNSIGNED NOT NULL,
  PRIMARY KEY(content_comment_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS content_comment_reactions (
  content_comment_id INT UNSIGNED NOT NULL,
  user_id            INT UNSIGNED NOT NULL,
  reaction_id        INT UNSIGNED NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS reactions (
  id        INT UNSIGNED NOT NULL AUTO_INCREMENT,
  image_url VARCHAR(255) NOT NULL,
  PRIMARY KEY(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

これをベースに MySQL Workbench で ER 図を生成すると以下のようになります。

f:id:syu_cream:20180506200831p:plain

これに対し hakagi はいくつか外部キー制約を追加するクエリを吐き出してくれます。

$ ./hakagi -dbuser root -targets hakagi_example
ALTER TABLE content_comment_reactions ADD CONSTRAINT FOREIGN KEY (content_comment_id) REFERENCES content_comments(content_comment_id);
ALTER TABLE content_comment_reactions ADD CONSTRAINT FOREIGN KEY (user_id) REFERENCES users(id);
ALTER TABLE content_comment_reactions ADD CONSTRAINT FOREIGN KEY (reaction_id) REFERENCES reactions(id);
ALTER TABLE content_comments ADD CONSTRAINT FOREIGN KEY (content_id) REFERENCES contents(id);
ALTER TABLE content_comments ADD CONSTRAINT FOREIGN KEY (user_id) REFERENCES users(id);
ALTER TABLE contents ADD CONSTRAINT FOREIGN KEY (user_id) REFERENCES users(id);

このクエリも用いることで、以下のような ER 図を生成することができました。

f:id:syu_cream:20180506201000p:plain

おわりに

まだ少ないサンプルに対して少ない推測ルールを適用しているだけなので、誤った推測結果が出たり推測が足りなかったりする場面があるかもしれません。 このツールを用いる場面が数多くあれば、そこで出てきたユースケースを元にツールを機能拡張していきたいところです。