Runner in the High

技術のことをかくこころみ

結果整合性について

歴史

  • かつて、分散システムのデータ複製における唯一無二の理想は「更新されたデータは即座に反映される」というものだった。
  • 70年台の分散システム技術において試みられているものの多くは、いくら背後にたくさんのシステムが控えているとしても「使う人間からはひとつのシステムを使っている」ように見せることで、その観点から主に議論されていたものの多くはデータの一貫性(Consistency)をいかにして獲得するかという部分に主眼が置かれたものだった。
  • 90年台中頃からインターネット・システムの規模が大型化してくると、開発者の多くはデータの一貫性を犠牲にしてもスケーラビリティを担保することが重要なのではないかと考えはじめた。
  • AWSは世界規模で利用されるシステムを開発するにあたってあらゆる場所でレプリケーション技術を活用してきたが、その中で結果整合性モデルはレプリケーションにおいてデータの一貫性を担保するための技術として提供されてきた。
  • AmazonのCTOであるWerner Vogelsは「並行処理における書き込み・読み込みパフォーマンスの担保」と「データロックによるノードの可用性の低下の抑止」の2つの観点から、データの一貫性はスケーラブルな大規模システムにおいてはさほど重視される必要はないと考えている。

強整合性と結果整合性の違い

強整合性 結果整合性
データのロック あり なし
スケーラビリティ 低い 高い
一貫性 担保される すぐには担保されない
  • 強整合性 の場合、データの更新の際にデータベースをロックすることによってデータの一貫性(Consistency)を担保するが、ロックされる期間が長いほどその間のデータベース・アクセスがブロックされ、可用性(Availability)を犠牲にすることになる。
  • 結果整合性 はデータの更新でデータベースがロックされることはないため、可用性とスケーラビリティを維持することができる。その代わりノード間でのデータの一貫性はデータ複製にかかる時間に依存することになるため、必ずしも担保されない。

その他

結果整合性は必ずしも高度な分散システム固有の難解な技術ではなく、多くのモダンなRDBMSは同期・非同期レプリケーションのシステムが組み込まれている。同期的レプリケーションの場合にはレプリケーションの更新はトランザクションの一部として行われるが、非同期的レプリケーショントランザクションログの伝播の前にプライマリーでのデータ更新が失敗すると、ノード間で一貫性のないデータが生まれることになる。つまり非同期レプリケーションRDBMSにおける古典的な結果整合性のケースのひとつである。

参考

データ指向アプリケーションデザイン ―信頼性、拡張性、保守性の高い分散システム設計の原理

データ指向アプリケーションデザイン ―信頼性、拡張性、保守性の高い分散システム設計の原理

AnyValを継承する意味

ScalaでDDDなコードのアプリケーションを作ろうとしているときに UserId など値型はどうするべきか の記事を読み、「専用の値クラスを作る」のパターンでふと 「ここでケースクラスが AnyVal を継承する理由ってなんだ...?」 と思ったので調べた。

case class PersonId(value: Long) extends AnyVal
case class PersonName(value: String) extends AnyVal
case class OrganizationId(value: Long) extends AnyVal

case class Person(id: PersonId, name: PersonName, organizationId: OrganizationId)
// ...

AnyValを継承すると

AnyValを継承すると値オブジェクトになる。値オブジェクトを継承したクラスはひとつの値しかとれない。

// OK
class Melo(val a: Int) extends AnyVal

// NG
class Melo(val a: Int, val n: String) extends AnyVal

AnyVal を使うことによって 実行時のオブジェクト割り当てを回避することができる ようになる。具体的には上の例でいうと Melo クラスはコンパイル時は Melo クラスだが、実行時は Int として解釈される(アンボクシング)

だが、パターンマッチングなどで型検査が必要になると、 Int としてアンボクシングされた値が再び Melo としてボクシングされることになるため、パフォーマンスに影響を与える。

結論

雑に言うと AnyValを継承したクラスを使うとパフォーマンスが向上する っぽい。

個人的にはDDDにおける値オブジェクトをコードで表現するにあたって、もし 引数がひとつしかない のであれば、どんなケースでも AnyVal を継承しない理由がないように思える... というか、調べていて値クラスという名前がたまたまDDDっぽいだけであって、これならべつにエンティティの実装だって可能なら AnyVal 継承クラスでえんちゃうの、と思ってしまった。

ただ、例えばバリデーションロジックで 「〜文字以下かどうかをチェックする」 みたいなビジネスルールがあったとき、クラスの中に MAXIMUM_LENGTH みたいな定数を宣言するのは普通だが AnyVal を継承しているクラスの中で val による宣言ができないという制約がある。

コレに関しては、以下のようなメソッドを定義してしまえばいいのでは、という同期からのアイデアをもらったが、果たしてアリなのか...?

class Text(value: String) extends AnyVal {
  def MAXIMUM_LENGTH = 100
}

参考文献

Rubyにおけるポリモーフィズムとダック・タイピング

自分がOOPをそれっぽく学んだのは、サンディ・メッツの「オブジェクト指向設計実践ガイド」だが、この本だとダック・タイピングはバキバキにでてくる一方であまりポリモーフィズムについては詳しく書かれていない。thoughtbotのブログの記事、Rubyとポリモーフィズムによると、Rubyにおけるポリモーフィズムは「継承」と「ダック・タイピング」によって実装できるらしい。

継承

まずは継承のパターン。GenericParserというクラスを継承したis-a関係のJsonParserXmlParserを定義し、parseメソッドをオーバーライドしている。

class GenericParser
  def parse
    raise NotImplementedError, 'Implementation required.' 
  end
end

class JsonParser < GenericParser
  def parse
    # Jsonをパースする実装 
  end
end

class XmlParser < GenericParse
  def parse
    # XMLをパースする実装 
  end
end

parser = XmlParser.new
parser.parse

parser = JsonParser.new
parser.parse

ダック・タイピング

つぎにダック・タイピング。ダック・タイピングでは、各パーサ実装はなんのクラスも継承せず、とにかくparseというメソッドを持っているということだけが共通している。

class XmlParser
  def parse
    # Jsonをパースする実装 
  end
end

class JsonParser
  def parse
    # XMLをパースする実装 
  end
end

class GenericParser
  def parse(parser)
    parser.parse
  end
end

parser = GenericParser.new
parser.parse(XmlParser.new)
parser.parse(JsonParser.new)

実装の違い

  • 継承 のコードでは、パーサを使う部分でそれが XmlParser なのか JsonParser なのかが意識されてparseメソッドが呼び出されている。
  • ダック・タイピング のコードでは、parseメソッドを呼び出すGenericParserは、そのメソッドを持つparserが何かを意識していない。

ということが分かる。

継承によるコードは、それぞれのパーサが「誰なのか」(=なにを継承しているか)が分かることによって、パーサに何ができるか(どんなインターフェースを持っているか)を判断できる。一方で、ダック・タイピングとはすなわち「誰か」ではなく「何をするか」によってオブジェクトを見分けるため、パーサがparseメソッドを呼び出せるということを期待するだけでインターフェースは意識しない。

ダック・タイピングとポリモーフィズムの関係

「なるほど、ということはダック・タイピングはポリモーフィズムのサブセットなのだろうか?」 という疑問が湧いてくる。

これまで自分は、ポリモーフィズムとはis-a関係にある継承クラスが抽象クラスと共通したインターフェースで個々の振る舞いを特化したものに変えること、というような理解をしていた。なので、必ずしも共通したインターフェースが保証される関係ではないダック・タイピングはポリモーフィズムに該当しないと考えていたわけだ。だがWikipediaのポリモーフィズムのページによると、ダック・タイピングは動的ポリモーフィズム(英: Runtime Polymorphism)というものに該当するようだ。この意味では、ダック・タイピングは継承に並んでポリモーフィズムを実現するための手段であると言っても良いのではないかと思う。

とはいえ、Googleポリモーフィズムとダック・タイピングについて調べるとStackoverflowの質問がそこそこヒットするが、回答者によってポリモーフィズムとダック・タイピングの関係をどう捉えるかというのは割れているように見える。ある人は 「ダック・タイピングはポリモーフィズムするための方法の一つで間違いないよ」 と言ったり、またある人は ポリモーフィズムはインターフェースによる明示性(explicitness)を必要をするものだからダック・タイピングは違う」 と言ったりしている。やはり、これに関してはもう少しOOPの原典的なものにあたったほうがいいのかもしれない。

雑なDNSの理解

  • Domain Name Systemの略
  • インターネットに接続されているすべてのコンピューターはIPアドレスを割り振られているが、数字でサイトを記憶しておくのは難しいので、DNSで覚えやすい文字列への解決を行う。
  • ARPANET時代はひとつのHOSTS.TXTにすべて書き込まれたものをFTPで共有していたが、ファイルがでかくなりすぎたのでDNS鯖が生まれた。
  • UNIX系OSにある /etc/hostsDNSと同じ役割を持っている。
    • これはARPANET時代のHOSTS.TXTの名残で、ローカルだけでDNSに問い合わせないドメイン名解決を行いたいときにはここに書いておく。
    • 開発で localhost:3000 とかを hoge.net とかにしておくと楽になったりする(?)
    • localhost127.0.0.1 に解決されているのはこのファイルによって。

名前解決

たとえばPCのブラウザで hogehoge.net へリクエストを送るまでの例

  1. [PC] ブラウザで hogehoge.net を開く。
  2. [PC] /etc/hosts に該当するIPアドレスとの組み合わせがあればそれで解決
  3. [家庭用ルーター] プロバイダからDNSルートサーバのIPアドレスを受け取る
  4. [家庭用ルーター] hogehoge.netDNSルートサーバに問い合わせる。
  5. [DNSルートサーバ] net DNSサーバが管理しているという旨を家庭用ルーターに伝える
  6. [家庭用ルーター] net DNSサーバへ hogehoge.net を問い合わせる
  7. [net DNSサーバ] hogehoge.netドメインを解決し、IPアドレスである 123.45.6.78 を家庭用ルーターへ伝える。
  8. [家庭用ルーター] PCへ 123.45.6.78 を伝える
  9. [PC] ブラウザは 123.45.6.78 へリクエストを送ってデータを取得したりする。

参考: インターネット10分講座 DNS - JPNIC

D言語で作るTCPサーバの最小構成

コネクションプーリングとかワーカスレッドの多重化とかやってないめっちゃ簡易版。

import std.stdio;
import std.socket;
import core.thread;

void main() {
  Socket server = new TcpSocket();

  server.setOption(SocketOptionLevel.SOCKET, SocketOption.REUSEADDR, true);
  server.bind(new InternetAddress(8080));
  server.listen(1);

  char[1024] buffer;

  enum header =
    "HTTP/1.0 200 OK\nContent-Type: text/html; charset=utf-8\n\n";

  string response = header ~ "Hello World!\n";

  while(true) {
    Socket client = server.accept();

    client.receive(buffer);
    writeln(cast(string)buffer);

    client.send(response);

    client.shutdown(SocketShutdown.BOTH);
    client.close();
  }
}

なんらかの処理において、できるだけその処理は依存するモデルのデータ構造をしりすぎないほうがよい?

  • たとえばSPAにおいて 「あるカテゴリに紐づく記事一覧を取得する」 という実装があるとする

  • このような処理を実装するにあたっては、各レイヤによって知ってよいことと、知っててはいけないものが変わってくる。

  • 例えばバックエンドが、RDBMSのようなIDによってデータを区別するようなミドルウェアを使っているのであれば、むろんAPIの呼び出しなどを行うデータアクセス・レイヤへアプリケーションが保持しているエンティティのIDがなんらかの方法で渡ることになるが、以下のように明示的にデータ構造がIDという属性を持っているということを期待している(=知っている)ようなコードを書くのは良くないと思っている。

const articles = fetchArticlesByCategoryId(category.id)
  • この実装を自分が改善するとしたら、以下のようにする
const articles = fetchArticlesByCategory(category)
  • この改善によって、fetchArticlesByCategory はもう category のデータ構造を知る必要がなくなった。これは、 category のデータ構造の変更が発生したとしても fetchArticlesByCategory を使うレイヤは影響を受けない(=変更に強い)ことを意味する。

  • fetchArticlesByCategory はその名前から分かるようにリポジトリ・レイヤの責務なので、リポジトリcategory などの依存するドメイン・モデルのデータ構造を処理の中で知識として持っていることは問題ない。バックエンドの仕様が変わってIDの代わりにカテゴリ名でフィルタするようになりました! みたいになっても、リポジトリ・レイヤだけ弄ればよいので、ドメイン・レイヤは変更から守られる。

  • もっと言うと、ドメイン・レイヤの中に id みたいなインフラ・レイヤを感じさせるような言葉が出てくるのがすでに危険信号のように見える。それとも id がインフラ・レイヤを感じさせる、というのは自分がRDBMSのことを考え過ぎなのだろうか...

  • ちなみにこの記事は書いててデメテルの法則だな〜と思った。

2017年を振り返る

 振り返ります

1月

  • 丁度大学の後期の期末試験を受けていた。2016年夏頃(後期)から大学に復帰したので、そのテストを消化してた。都市社会学のレポートを書くのに清澄白河をウロウロして写真を撮った記憶がある。ジェントリフィケーションについて書いた。それから、まだこの頃は教職課程のガイダンスなどがいくつかあって、このあとすべての努力が無駄になるとは知らずに頑張っていた。

  • 初めて渋谷WOMBへ行って、Zomboyを生で見た。Zomboyもスゴかったが、ぎゅうぎゅう詰めすぎてびっくりした。 www.womb.co.jp

2月

  • 大学の先輩と同期と一緒にスノボをしに竜王スキーパークへ行った。中級者コースから初心者コースのふもとのリフト乗り場まで一直線で滑るとめちゃくちゃ距離があって気持ちいので、最高のスキー場だと思う。それから、中級者コースの途中にある滑っていかないと入れないレストランのタコライスがすごくおいしい。ここでタコライスを食べてからタコライスにドハマりしはじめた。 www.ryuoo.com

3月

  • 内定先の開発合宿に参加した。どこに行ったかは忘れたけど、すごく天気がよくて山のたくさんあるところに行った。

  • 一年休学していた僕より一足先に大学の同期達が卒業していくので、サークルの卒業旅行でグアムへ行った。なんてことはなかったが、海で日焼け止めをちゃんと塗ってなかったせいで、全身大やけどを負い、赤く腫れて水ぶくれたような症状に僕を含めて男子三人がなった。死ぬかと思った。夜寝てても、歩いてても、何をしててもヒリヒリ痛い。飛行機はガタガタゆれるし椅子に背もたれがあるしで、痛くて痛くてしょうがない。帰国してから速攻で近くの皮膚科にいくと、コワモテのオネエの先生が優しく診て塗り薬を出してくれた。それからは少しづつ完治していったが、もう南国はこりごりだという気分になった。それでもグアムは基本的に気温も天気も最高で、場所としては申し分ない。でももうビーチで裸にはなりたくない。

  • この辺から内定先でAPJというものが始まる。これは技術職の内定者と総合職の内定者がチームを組んで、ひとつのAndroidアプリを作るというもの。基本的にコードを書かない側の人間であっても、技術に少しでも触れる経験をするというコンセプトらしい。僕らは、位置情報を利用したSNSのようなものを作ることにした。

4月

  • Junction Asiaに参加した。会場は寺田倉庫だった。僕らは4人1組のチームで参加して、顔認証から悪質な訪問販売などをブラックリスト化するインターフォンのようなものを開発したが、惜しくも賞は取れなかった。久しぶりに1泊2日のハッカソンに参加したが、2日目の朝のあのしんどい感じはいつ経験しても慣れない。

  • 1年生のころから少しづつ教職課程の単位を取りづつけていたが、取得単位の関係でこの年の5月ないし6月に予定されていた教育実習にいかせることはできないという通告を教職課程事務局から受ける。というのも、僕が1年間の休学をしていたことによって、卒業に必要な単位と教育実習へ行くのに必要な単位の講義が2017年度の授業予定で重複してしまったからだ。教職課程事務局はすでに僕がとっている単位から、必要科目として認定単位の振替をできるかもしれないとかなんとか言っていたが、それに関してはなんの処置も行われなかった。最終的には、教職課程を辞退する紙にサインをさせられ、それまでに取ってきた教職課程の単位をすべてフイにすることになった。武蔵大学には、休学をしようが留学をしようがまともに4年間で卒業ができるようなキャリアパスをしっかり用意してもらいたいと思う。

5月

  • 卒論のことを考え始める。

  • 3月から始まったAPJの遠隔ミーティングを頻繁にし始める。

  • 高校の頃に軽音楽部にいた後輩が意識の高い学生になっていて、ミクシィインターンをしているというので渋谷で食事をした。話を聞いてみると、軽音楽部に所属していたまた別の後輩が、スタートアップ界隈で活動をしていて、それにいろいろと影響を受けたらしい。人間どこでどう変わるかわからないものだなと思う。

  • この頃からなぜか急に思い立ってStackoverflowでスコアを伸ばそうとし始める。5月の時点で303ポイントまで上げた。

6月

  • 内定バイトを始める。ReactNative+TypeScriptを本格的に触り始める。

  • 卒業論文の題目を決める時期なのだが、こんなタイミングでテーマが決まるはずがなく、適当なテーマで提出する。アメリカのスタートアップカルチャーについて書きたいなと思っていたが、先行研究がなさ過ぎて頓挫する。

  • 2015年夏のインターンの知り合いが東京の大学院に来たということで遊びに行ったが、そのタイミングで新たなプロジェクトがスタートする。

  • 内定先の会社が上場する。東京証券取引所に行って鐘を鳴らす場面を生で見た。

7月

  • 軽井沢に旅行に行った

  • 3月から本格的に始まっていたAPJが終わる。位置情報を利用したSNS的なもの、という感じでそれっぽいものを作ったが、あまりいい評価は得られなかった。我々の他には、社内の技術本を貸し借りするためのアプリや、成分表の写真を撮るだけでハラルフードを見分けられるアプリなど、いろいろなものがでてきてとても良さがあった。

8月

  • 家族で箱根に旅行に行った。小田急箱根ハイランドホテルといういい感じのところだった。コースのディナーを食べたり、ピーター・ティールのZERO TO ONEを読み終えたりした。 www.hakone-highlandhotel.jp

  • IndiegogoでGPD Pocketを買った。結局大学で卒論を書くのに大活躍してくれた。 izumisy-tech.hatenablog.com

9月

  • Meguro.rbでROM.rbについてLT発表をした。初めてで緊張したけど、とてもエキサイティングでいい経験になった。

  • ダンケルクを見た。戦争色が上手く再現されすぎていて本当に戦争イヤだなと思った。死にたくない。ダンケルクに始まった話ではないが、20歳を過ぎたあたりから戦争映画を見ると涙が出てしまうことが多くなった。 wwws.warnerbros.co.jp

10月

  • 右下の親知らずを抜いた。1時間超のドリル採掘作業はほんとにつらかった。抜歯後の痛みはどちらかといえば中が痛むとかそういうのではなく、縫合の糸で歯茎が引っ張られて痛いというのが大部分で、それだけで食事のモチベーションを無くした。ウィダーインゼリーを大量購入していたが、ゼリーを吸う動きも口の中の筋肉を使うからか奥歯の部分の歯茎が疼いてキツかった。

  • 9月末のMeguro.rbを筆頭に、思い立って4回のLT発表をした。 izumisy-tech.hatenablog.com

  • 楽天テクノロジーカンファレンスというデカイLT大会もあったが、ネタが出てこず参加しなかったのは後悔している。

  • Jelly Proという小さいスマホを買った。LINE MOBILEで運用している。 izumisy-tech.hatenablog.com

11月

  • 10月末頃から急にまたStackoverflowの意欲が再燃し、323だったのを551くらいまで上げる。

  • 卒論の提出が12月に控えているというのもあり、6月からやっていた内定バイトを年末までストップすることにする。教授も自分も焦りがでてくる。このあたりから毎日卒論のことが頭の片隅にあってすごくフラストレーションが溜まってくる。

  • 最後の卒論題目提出期間(うちの大学は2度に渡ってテーマを決めるタイミングがある)だったが、結局大したアイデアも出ず、行き当たりばったりなテーマで提出する。

  • 気分で髪を明るくし始めた。

12月

  • なぜか卒論で忙しかったのにStackoverflowで人助けに精をだしてしまう。そのおかげかスコアは順調に551から771まで伸びる。

  • 卒論の提出をした。結局最後の最後はギリギリまで教授にヘルプをしてもらいながらそれっぽいものが完成したので安心した。

  • 卒論提出の直後に今度は左下の親知らずを抜いた。今度は歯茎が引っ張られていたいとかそういうものはなかったが、今度は歯茎の腫れがずっと収まらず、これをかいているいまも奥にしこりがあるような感じがして違和感がある。右の奥歯はほとんど全快したが、歯茎が完成していないせいで食べ物が挟まったりして困っている。早く治らんかな。

  • Slip.itというソーシャル機能の一切ないブックマーキングアプリを作ったので、公開した。 slipit.me

まとめ

なんかまとまりのない一年でしたね。

グアム行ったのが今年だということを完全に忘れてました。来年もスノボ行きたい。

入門ROM.rb 第3回: Repository

さーて、お待ちかねのROM.rb入門の第3回はRepositoryから。

前回の記事はRelationを説明していますので、読んでない人は参考までに。 izumisy-tech.hatenablog.com

Repository

リポジトリと聞くと文脈的にPoEAAやDDDっぽい雰囲気を感じる人が多いのではないかと思う。

どちらも、最も重要な部分はレイヤード・アーキテクチャの考え方で責務を分離することなのではないかと思うが*1、ROM.rbのリポジトリも、公式のコア・コンセプトの説明*2によるとレイヤード・アーキテクチャの考え方と近く、ドメイン・ロジックとデータアクセス・レイヤを粗結合なものにするためのboundary(境界)としての役割を担っている。

An important function of repositories is to act as a boundary between the data access logic and Application Domain logic. This boundary helps to reduce the complexity of rehydrating your entities and keeps a direct dependency on a particular datastore out of your domain.

リポジトリの重要な点は、ドメイン・レイヤからデータアクセス・レイヤの直接的な依存を追い出す(keeps a direct dependency on a particular datastore out of your domain)というところだ。

このコンセプトと対象的なアプローチはActiveRecordパターンだ。ActiveRecordパターンはあえてドメイン・モデルをデータアクセス・レイヤのデータ構造を粗結合にすることによって、インピーダンス・ミスマッチ*3を減らし、高速なアプリケーション開発を可能にしている。

だが一方で、ActiveRecordパターンでは一挙に「モデル」と呼ばれる責務がデータアクセス・レイヤとドメイン・モデルを兼ねてしまうため、どうしてもモデルが肥大化し見通しが悪くなってしまう場合が多い。この点、ROM.rbではActiveRecordと異なり、データアクセス・レイヤとドメイン・レイヤの間にboundary(境界)としてリポジトリを挟み、それぞれの責務を異なるものとして粗結合な設計を出来るようにしようとしている。

Relationとの違い

前回の記事で説明したRelationは、Repositoryがドメインレイヤに近い存在であるのに対して、どちらかといえばインフラストラクチャ・レイヤに近い存在だ。

Relations ... provide APIs for reading the data from various databases, and low-level interfaces for making changes in the databases. Relations are adapter-specific, which means that each adapter provides its own relation specialization, exposing interfaces that make it easy to leverage the features of your database.

つまり、RelationがHTTPやSQLなどのような、アプリケーション本体の外側にあるアダプタ固有(adapter-specific)の操作をAPIとして提供するのに対して、RepositoryはそれらのRelationを更にドメイン・レイヤにふさわしい形でラップし、ドメイン・モデルがRelation(アダプタ)の操作から影響を受けないようにするためのレイヤであるということだ。

それでは実際に使ってみよう。

require "rom"
require "rom/sql"


#
# ## Relationを定義
#
# ROMのRelationは`ROM::Relation[:アダプタ名]`を継承したクラスとして定義する
# associationsブロックの中で`belongs_to`や`has_many`などの関連も定義をする
# ここで定義したRelationは下で出てくるROMの初期化の際にROMへの登録を行う必要がある。
#

module Relations
  class Users < ROM::Relation[:sql]
    schema(:users) do

      # Relationの関連定義はActiveRecordと似ている。
      # has_many, belongs_toやhas_many-throughなどがあるのでドキュメントを参照のこと
      # http://rom-rb.org/learn/sql/associations/

      associations do
        has_many :books
      end

      # schemaブロックの中では関連の定義に加えて以下のように明示的に
      # Relationのカラムを定義できる。この場合、プライマリ・キーは
      # `Types::Serial`である必要がある

      attribute :id, Types::Serial
      attribute :name, Types::String
      attribute :age, Types::Int
    end

    # Repositoryの中で呼び出せるクラスメソッドを以下のように定義できる
    # ROM.rbのSQLアダプタ(rom-sql)は内部でSequalizeを使っているので
    # where句などもその記法に従う

    def by_pk(id)
      where(id: id)
    end

    def adult
      where{age > 20}
    end
  end

  class Books < ROM::Relation[:sql]
    schema(:books) do
      associations do
        belongs_to :user
      end

      # booksの場合にはuserに対してbelongs_toで関連を持っているので
      # その関連に対して`Types::ForeignKey(:users)`のように明示的に
      # 利用する外部キーを指定できる。ない場合には自動で推論される。

      attribute :id, Types::Serial
      attribute :title, Types::Int
      attribute :user_id, Types::ForeignKey(:users)
    end
  end
end


#
# ## Repositoryを定義
#
# Repositoryからは定義されているRelationを触ることができる
#

module Repositories
  class User < ROM::Repository[:users]
    # ROM.rbではRepositoryがRelationに対してどのようなアクセスをできるのか
    # という点を明確に制限できる。ここでは:createをコマンドとして有効化しているが
    # :create以外にも:updateや:deleteがある。
    commands :create

    # 以下のようにクラスメソッドを定義できる。
    # これらのデータはROM::Structによってラップされて返される
    # ここではusersのみを読んでいるが、booksも同様にメソッドのなかでアクセスできる

    def adults
      users.adult
    end

    # `aggregate`メソッドを使うことによって、Relationで定義されている
    # 関連Relationのデータを子要素としてフェッチできる
    # `aggregates`を呼ぶ場合には明示的にRelationを指定しなくてもよさそう。

    def all
      aggregate(:books)
    end

    def by_id(id)
      aggregate(:books).by_pk(id).one
    end
  end
end


#
# ## ROMにRelationを登録
#
# 上で定義した2つのRelationをROMに登録
#

config = ROM::Configuration.new(:sql, "sqlite::memory")
config.register_relation(Relations::Users)
config.register_relation(Relations::Books)
rom = ROM.container(config)

# SQLiteでテーブルを作るマイグレーションを適用

migration = ROM::SQL.migration(rom) do
  change do
    create_table(:users) do
      primary_key :id
      string :name
      integer :age
    end

    create_table(:books) do
      primary_key :id
      foreign_key :user_id, :users
      string :title
    end
  end
end

gateway = rom.gateways[:default]
migration.apply(gateway.connection, :up)


#
# ## Repositoryを使ってみる
#
# RepositoryはRelationとは異なり、ROMへ登録するのではなく、ROMのインスタンスを
# コンストラクタDIすることでRepositoryのインスタンスを作るという形になる。
#

userRepository = Repositories::User.new(rom)
userRepository.create(name: "Justine", age: 10)
userRepository.create(name: "Jessy", age: 18)
userRepository.create(name: "Michael", age: 23)

# テストのためにBooksテーブルにRelation経由でデータを入れてみる
# (外部キー成約があるので、userレコードを作ったあとじゃないとダメ)

books = rom.relations[:books]
books.insert(title: "Good Book", user_id: 1)
books.insert(title: "Nice Book", user_id: 1)

#
# ### Repositoryからレコードを取得
#
# ここでは`by_id`や`adults`などの、Repositories::Userの中で定義した
# クラスメソッドを呼び出すことができる。Relationの結果が常にハッシュなのに対して
# Repositoryによる取得の返り値はROM::Structという構造体の形になる。
# これはデフォルトでtrueになっている`auto_struct`をfalseに指定することで
# 止めることができる
#

users = userRepository.all
users.each { |user| p user }
# => #<ROM::Struct::User id=1 name="Justine" age=10 books=[
#       #<ROM::Struct::Book id=1 title="Good Book" user_id=1>,
#       #<ROM::Struct::Book id=2 title="Nice Book" user_id=1>
#     ]>
#    #<ROM::Struct::User id=2 name="Jessy" age=18 books=[]>
#    #<ROM::Struct::User id=3 name="Michael" age=23 books=[]>

single_user = userRepository.by_id(1)
p single_user
# => #<ROM::Struct::User id=1 name="Justine" age=10 books=[
#       #<ROM::Struct::Book id=1 title="Good Book" user_id=1>, 
#       #<ROM::Struct::Book id=2 title="Nice Book" user_id=1>
#     ]>

users = userRepository.adults
users.each { |user| p user }
# => #<ROM::Struct::User id=3 name="Michael" age=23>

このように、RepositoryはRelationをドメインモデル(ROM::Struct)へと変換するインターフェイスの役割を果たしていることが分かる。

加えて、たとえばRepositoryが返すドメインモデルのインスタンスを独自のクラス定義にマッピングしたくなるかもしれない。その場合にはauto_structをtrueに維持したうえで、ROM::Structを継承した独自のクラスにRepositoryの中でマッピングするよう指定できる。そのやり方に関しては、次回以降説明していこうと思う。

*1:DDDとレイヤードアーキテクチャはイコールではないが、便宜的に。

*2:http://rom-rb.org/4.0/learn/getting-started/core-concepts/#repositories

*3:簡単に言うと、永続化レイヤ(たとえばデータベース)のデータ構造と永続化対象(たとえばドメインモデル)のデータ構造の整合性の差異