キー、その名状しがたきもの
URL とキーについて考えていたら、URL に含むキーについていろいろ思うところがあったので雑に dump しておく。
暗黙のうちに REST を前提にしてしまっているところはあるが、意識的に REST とは文脈を切り離して素朴に書いてみる。
重複する部分もあるが、URL 側とキー側、それぞれの観点から書いてみる。
URL に求めること
自分が URL に求めることはいくつかあるが主に以下のようなことを求めている
- URL だけで目的のページにたどり着けること
- ページの簡易的な説明になっていること
- ASCII で構成されていること
それぞれの意図するところと重要度について簡単に書く。
[MUST] URL だけで目的のページにたどり着けること
あるページにたどり着くために「URL が指し示すページに行って、次に◯◯というボタンをクリックして……」みたいなことはやりたくない。
URL を入力したら一発で目的のページにたどり着いて欲しい。
(Addressable & Stateless であってほしい)
[SHOULD] ページの簡易的な説明になっていること
例としてブログの URL に含まれる path を考えてみる。
このとき、path が /articles/1234
となっているよりは /articles/introduction-to-key
とかのほうが実際にページを見る前に内容が予測できて嬉しい。
[MAY] ASCII で構成されていること
状況によるが、可能であれば ASCII だけで構成されていると嬉しい。
ASCII 以外を URL に含めるとエンコードされて URL が長くなりがちだし、可読性も下がってしまう。
とはいえ、ASCII 以外を含めるメリットが大きいときもあるので、そういうときは無理に ASCII にはしない。
キーに求めること
URL の中で path に含まれるキーにあたる部分以外について上の条件を満たすのはわりと難しくない。キーをどうするかが悩ましい。
キーに求めることは以下のようなもの。
- リソースを一意に特定できること
- リソースを一意に特定する以外の意味を持たないこと
[MUST] リソースを一意に特定できること
これはキーの定義上必須。
ただし、path に含まれる文字列がそのまま物理的に保存されている必要性は必ずしもない。
[SHOULD] リソースを一意に特定する以外の意味を持たないこと
キーとはリソースなりレコードなりを一意に特定するものであり、それ未満でもそれ超過でもない。
仮に auto increment な primary key を作成順の判別に使っているとしたら、それはキーの目的外利用になる。そもそも、作成順は作成日時から導出できるので整数列にしてしまうということは情報を損失している。
人間は整数を見るとどうしても比較したりソートしたりしてしまうので、この観点からはキーは乱数(UUID v4 やランダム文字列)だと嬉しい。*1
対立とバランス
上に書いたものの中で真っ向から対立するものがある。
- URLはページの簡易的な説明になっていること
- キーはリソースを一意に特定する以外の意味を持たないこと
URL はページの簡易的な説明になっていてほしいけど、URL に含まれるキーはリソースを一意に特定する以外の意味を持ってほしくない。
このふたつの間でどういう風にバランスを取るかというのが良い URL/キーをつくる上で重要だと思っている。
いくつかの例を考えてみる。
例1:商品管理アプリケーション
小売店の商品を管理するアプリケーションを考えてみる。
この店舗が扱う商品すべてに JAN コード(バーコード)が付与されているなら JAN コードは商品を代表する属性として妥当なように思われる。
そこで JAN コードを path に含むキーとして採用して /products/4569951116179
のような path にするのが良いかもしれない。
例2:石コレクターのための石管理アプリケーション
そのへんで拾ってきた石を管理するアプリケーションを考えてみる。
石はこの上なく自然のものだが、石に自然キーはない。
そこで何かしらのサロゲートキーを用意してやる必要があるが、ここで auto increment な整数を使うとキーに必要以上の意味を持たせてしまうことになるため、この場合は /stones/5f498e23
のようなランダムな文字列をキーとして含む path が良いかもしれない。
例3:薄い本管理アプリケーション
薄い本を管理するアプリケーションを考える。
薄い本には ISBN のようなものはないのでこれをキーにすることはできない。*2
とはいえ、本にはタイトルというその本を代表する属性がある。ただし、タイトルは重複する可能性があるのでそのままキーにすることはできないし、多くの場合、エンコーディングが必要な文字を含んでいるのでそのままだと path に含めて扱いにくい。そこで、本を代表する属性であるタイトルを元に新たにキーを考える。
たとえば /books/sekaiichi-kawaii-boku
のような感じ。
キーの選び方
例1で触れたように、リソースを代表する属性がありそれがキーである場合、代表する属性を path に含めるキーにするのが良い気がする。
例2で触れたように、リソースを代表する属性がない場合、人工的で意味を見出しにくいキーを path に含めるのが良い気がする。
例3で触れたように、リソースを代表する属性はあるがそれがキーではない場合、代表する属性を元に人工的で意味のあるキーを path に含めるのが良い気がする。
例1は自然キーを使った path、例2は意味のない人工キーを使った path になっていて対照的。
例3はそれらの中間でリソースを代表する属性から意味のある人工キーを作り出している。いわば、半自然キーとでも言うべきものになっている。
おわりに
URL を考えるだけでもこんなに苦労するのでプログラミングはむずかしい。
書きながらキーについて思うところが増えてきたので、近いうちにまた何か書くかもしれない。
Ruby で 1 < 2 < 3 みたいな比較演算を書けるようにする試み
TL;DR
華麗に失敗した。
発端
こんなモチベーションなので実用は目的にしていない。
何番煎じかもわからないけど、考えてみたかったので考えてみた。
問題をシンプルにする
仕様を絞る。
- 対象クラスを絞って
Integer
だけにする - 境界値を含まない場合だけを考える(
<
,>
だけ) ()
による演算の優先順位変更とかも考えない
アイデア
「左から順番に比較して、その結果を boolean じゃなく以前の 比較結果を保持するいい感じのオブジェクト で返せば良いのでは?」
左から順番に比較していくだけなら、
- 前回の比較が
true
だったのかどうか - 前回の比較における右辺
が取れればいけるだろうという感じ。
初めに考えていた方針
Comparable
をいい感じに魔改造すればいけそう- いい感じのオブジェクト は
TrueClass
,FalseClass
あたりを継承すれば良さそう
とぼんやり考えていたところ、
Comparable
を変更してもInteger
の挙動が変わらないTrueClass
,FalseClass
が Singleton なので継承するとnew
できない
あたりの問題が起こったので、Comparable
じゃなく Integer
を直接触るようにしつつ、比較結果オブジェクトをそのまま条件式で使えるようにするのは諦めて to_b
を呼んで boolean に変換するアプローチに変更。
実装
比較結果を保持するオブジェクトComparisonResult::TrueClass
, ComparisonResult::FalseClass
を作って、Integer#<
, Integer#>
がそれを返すように変更。
コードとしてはこんな感じ。
require 'singleton' module ComparisonResult class TrueClass def initialize(left, right) @left = left @right = right end def to_b true end def <(other) @right < other end def >(other) @right > other end end class FalseClass include Singleton def to_b false end def <(_) self end def >(_) self end end end class Integer def <(other) if (self <=> other) == -1 ComparisonResult::TrueClass.new(self, other) else ComparisonResult::FalseClass.instance end end def >(other) if (self <=> other) == 1 ComparisonResult::TrueClass.new(self, other) else ComparisonResult::FalseClass.instance end end end # < raise '1 < 2' unless (1 < 2).to_b == true raise '2 < 1' unless (2 < 1).to_b == false raise '1 < 2 < 3' unless (1 < 2 < 3).to_b == true raise '1 < 3 < 2' unless (1 < 3 < 2).to_b == false raise '2 < 1 < 3' unless (2 < 1 < 3).to_b == false raise '2 < 3 < 1' unless (2 < 3 < 1).to_b == false raise '3 < 1 < 2' unless (3 < 1 < 2).to_b == false raise '3 < 2 < 1' unless (3 < 2 < 1).to_b == false # > raise '1 > 2' unless (1 > 2).to_b == false raise '2 > 1' unless (2 > 1).to_b == true raise '1 > 2 > 3' unless (1 > 2 > 3).to_b == false raise '1 > 3 > 2' unless (1 > 3 > 2).to_b == false raise '2 > 1 > 3' unless (2 > 1 > 3).to_b == false raise '2 > 3 > 1' unless (2 > 3 > 1).to_b == false raise '3 > 1 > 2' unless (3 > 1 > 2).to_b == false raise '3 > 2 > 1' unless (3 > 2 > 1).to_b == true
感想
一応動く何かはできたけど、思っていたことは全然できなかった。
対象クラス・境界値・優先順位あたりはゴリゴリやればいけるはずだけど、 to_b
が生えているのが「これはない……」という感じ。
oneshot task に規約と仕組みを導入する
Cookpad TechConf 2018 の突発 LT で話した内容をもう少しちゃんと書いてみます。
アドホックなタスクの必要性
日々、サービスを運用しているとアドホックなデータの変更や処理の再実行などが必要になります。
典型的には以下のようなケースです。
- データマイグレーション
- サポート対応
- 不具合対応
- その他もろもろ
ここではこのようなアドホックなタスクを oneshot task と呼ぶことにします。
oneshot task をスクリプトとして書く
oneshot task をスクリプトとして書くといくつかのメリットが得られます。
似たようなことをする方法はいくつかありますが、それらの方法と比べたときの主なメリットは次のようなものです。
vs. 管理画面
管理画面をポチポチすれば対応できるものによっても場合によってはスクリプトを書く価値があります。
大量のデータを処理する必要があるときにはスクリプトを書いたほうが簡単確実ですし、スクリプトであればトランザクションなども使えるのでアトミックな更新をすることができます。 また、事前条件・事後条件をチェックした上で必要に応じてトランザクションをロールバックするなどより安全に配慮した処理を行なうこともできます。
vs. 手オペ
ここで手オペと呼んでいるのは本番環境で bin/rails console
するとか DB に直接 SQL を投げるとかそういったものです。
サービスの最初期など場合によっては便利な手オペですが、ミスを起こしやすくリカバリしにくいので基本的にはスクリプトにしておくべきです。
スクリプトにすることによって、テストを書くことができるようになり、PR にしてレビューできるようになり、Git に残るので過去の変更内容を見返すことができるようになります。
考慮すべきこと
スクリプト化するメリットを書きましたが、スクリプトにすることによっていくつか考えることが生じます。
ファイルをどこに置くのか
Rails には oneshot task の置き場所に関する規約はないので、どこに置くかを決めておく必要があります。
いつまでも残ったら邪魔じゃないのか
一時的に使用するスクリプトをリポジトリに入れることによって他のコードへの依存が発生します。 将来、依存されているコードを変更したくなったときに依存している箇所を書き換えてよいのかどうか逐一判断する必要が生じます。
ファイル名・タスク名の命名とかファイル作成とかが面倒
単純にファイルを作るのが面倒という話です。
稀によくあるタスクだと名前が衝突したりするのでぶつからない名前を考えるのが地味に面倒だったりします。
規約と仕組みをつくる
日々、スクリプトを書く必要が生じる中でいちいち上のようなことを考えるのは面倒なので規約と仕組みで解決します。
規約
置く場所を決める
決めの問題なのでどこでも良いんですが、Rails で oneshot task を書くとなると rake task が一般的だと思うので lib/tasks/oneshot/
に入れることにします。
そうすることによって、置く場所をいちいち考える必要がなくなるとともに、このディレクトリにあるファイルは(実行後であれば)消しても良いとわかるようになります。
命名規則を決める
ファイル名・タスク名の命名にも規約をつくり、規約によってファイル名・タスク名にタイムスタンプをつけるようにして YYYYMMDD_foo_bar.rake
というファイル名と oneshot:foo_bar_YYYYMMDD
というタスク名にします。
そうすることによって、命名がぶつかることを防止するとともに、タイムスタンプを元に消して良いかどうか判断しやすくなったり rm lib/tasks/oneshot/2017*
というような一括削除が行えるようになります。
仕組みをつくる
規約をつくったので、その規約にのっとってファイルをつくっていけば良いんですが、規約通りにファイルをつくるというのは明らかに人間よりもコンピュータが得意な仕事です。
そこで規約通りにファイルを作成するための仕組みをつくります。
幸い、Rails には generator の仕組みがあって、普段よく使っている bin/rails g hogehoge
を自分でつくれるのでこの仕組に乗っかって bin/rails g oneshot FooBar
できるようにします。
generator については以下のページによくまとまっています。
使いまわしたいので gem にした
リンク先を読んでもらうとわかるのですが、generator の仕組みは非常にシンプルで generator をつくるのは全然難しくありません。
ただ、複数のプロジェクトで同じような generator を何度も書くのは面倒なので、使いまわせるように切り出して oneshot_task_generator という gem に切り出しました。
使い方
$ bin/rake generate oneshot FooBar
という感じで実行すると lib/tasks/oneshot/20180205_foo_bar.rake
というファイルができます。中身はこんな感じ。
# Skeleton for this file was generated by `bin/rails generate oneshot FooBar` # For copy and paste: `bin/rake oneshot:foo_bar_20180205` namespace :oneshot do desc '' task foo_bar_20180205: :environment do end end
基本的には規約通りのディレクトリに規約通りのテンプレを吐き出すだけのシンプルなものです。
カスタマイズ
gem にする前に運用していたバージョンでは oneshot でよく使うコード(e.g. トランザクション)をテンプレに埋め込んでいました。
そういった簡単なカスタマイズもできるようにしています。
# config/initializers/ohesnot_task_generator.rb OneshotGenerator.configure do |config| config.body = <<-BODY ActiveRecord::Base.transaction do # Write transactional code here end BODY end
# Skeleton for this file was generated by `bin/rails generate oneshot FooBar` # For copy and paste: `bin/rake oneshot:foo_bar_20180208` namespace :oneshot do desc '' task foo_bar_20180208: :environment do ActiveRecord::Base.transaction do # Write transactional code here end end end
おわりに
自分が欲しいものを規約と仕組みに落とし込んで、しばらく運用してみたところ便利だったので gem にしたというお話でした。
技術的にも思想的にも別に大したことはしてないんだけど、こういう地味に便利になるみたいなのが結構好きです。
使ってみて「ここをこうして欲しい!」とか「英語がクソ」みたいなのがあれば issue なり PR なりいただければと思います。