Rails の timestamps カラム(created_at, updated_at) と別に日時カラムを作る理由が分からん

Rails を使っていて、日時カラムを作るケースがありますが、あれのメリットが知りたいと思って、ブログに書いてみた。

例: ユーザーの登録日時

# db/migrate/20161010010101_create_users.rb
class CreateUsers < ActiveRecord::Migration
  create_table :users do |t|
    t.string :name
    t.datetime :registered_at, null: false

    t.timestamps null: false
  end
end

こんな感じで、ユーザーの登録日時を created_at を使わず、 registered_at のように別カラムを用意して、自分たちで Time.now などを保存するような実装のメリットが知りたい。

timestamps で良いと思っている理由

カラムを使い分けることがほぼない

よく「レコード挿入日時と登録日時は意味が違う」という意見をお聞きするのですが、「ユーザーが登録するとレコードが1件できる」という事を考えると、そこまで意味が違うものなのか疑問。

そもそも「レコード挿入日時」と「登録日時」と使い分ける仕様がほぼ無い。(後述する非同期処理くらい?)

使った方が楽

ActiveRecord が自動的に生成するものを使うべきじゃない」って話も聞くけど、用意されている機能を使った方が冗長にコード書かなくて済むし、自分たちで再実装するより良いのでは? belongs :user, touch: true とか使えて便利ですし。

cache に使われる

標準だと View の cache で Modle の updated_at が使われるので、 updated_at を最終更新日としておいて方がシンプルに Rails のレールに乗れて良い。

日時カラムを別に用意する方が良いと思うケース

非同期処理を使う

「非同期処理(または登録処理)に時間がかかり、ユーザーのリクエスト 〜 DB挿入に時間がかかる」場合、値がズレるので別カラムは必要かもしれない。そのズレが問題になる仕様の場合は。

単純な更新日時じゃない場合

「特定のカラムが更新されたときだけ、更新日時を更新したい」みたいな仕様の場合は updated_at とは別にカラムを用意した方が良いと思う。

  • あえて非正規形なテーブルにしている
  • 既存のDB設計が腐っているが、直せない

みたいなケースだと、 updated_at をそのまま活用するのは難しいと思う。

まとめ

DB設計に問題がなければ、基本的には Rails 標準の timestamps を使って良いのではないかと思っています。

もし、別の考えなどあればコメントなど頂けると嬉しいです。

Rails の link_to_if にブロックを渡したときの挙動が分かりにくいので注意

Railslink_to_if にブロックを渡したときの挙動が分かりづらいので、使う場合の注意点をまとめてみました。

TL;DR

ブロックの内容を条件によってリンクにしたい場合は capture を使うのがシンプルで良いです。

<% body = capture do %>
  <%= image_tag 'icon.png' %>
  <span><%= user.name %></span>
<% end %>
<%= link_to_if condition, body, root_path %>

link_to_unlesslink_to_unless_current の場合も同じ。

link_to_if にブロックを渡す例

ユーザーのアイコンと名前を条件によってリンクにする場合を考えます。*1

<% if condition %>
  <% link_to root_path do %>
    <img href="icon.png"></img>
    <span><%= user.name %></span>
  <% end %>
<% else %>
  <img href="icon.png"></img>
  <span><%= user.name %></span>
<% end %>

これを DRY にするために link_to_if を使おうとしても、うまく動きません。

<% link_to_if condition, root_path do %>
  <img href="icon.png"></img>
  <span><%= user.name %></span>
<% end %>

# If the condition is true
# => <a href="/">/</a>
# else
# => <img href="icon.png"><span>name</span>

条件が true の場合、ブロックが 評価されない からです。

ブロックを必ず評価するメソッド

条件が true の時にaタグで囲むメソッドを作ってみました。

def link_to_block_if(condition, options = {}, html_options = {}, &block)
  if condition
    link_to(options, html_options, &block)
  else
    capture(&block)
  end
end

使い方はこんな感じ。

<% link_to_block_if condition, root_path do %>
  <img href="icon.png"></img>
  <span><%= user.name %></span>
<% end %>

Rails へプルリクを送ることを検討

rails/rails で検索してみると、 link_to_if でブロックを必ず評価するプルリクが出て、1度マージされた後 revert されていました。

また、 Qiita にも同じような記事がありました。

似たようなことで困っている人がいることは分かったので、 Rails にプルリク送ってみても良いかもしれないと考えた。

メソッド名の検討

link_to_block_if は微妙なので、他のメソッド名を考えてみましたが、どれも微妙・・・。

- link_to_content_if
- link_or_content_tag
- wrap_link_to_if

link_to_ifcache_if のように _if のメソッドは条件が true の時にブロックが評価されない。 また、 find_or_create_by のように _or_ のメソッドは前半が true の時にブロックが評価されない。

Rails の他メソッドと一貫性の無いメソッド名になるのはダメそう。

命名に時間がかかるメソッド

そもそも、命名に時間がかかるメソッドは何かがダメです。 そこで、1つのメソッドにする事を止めて、下記のように2つのメソッドで書いてみました。

<% body = capture do %>
  <%= image_tag 'icon.png' %>
  <span><%= user.name %></span>
<% end %>
<%= link_to_if condition, body, root_path %>

あれ、これで良いのでは・・・。Rails の標準メソッドの組み合せなので、後から読んでも分かりやすいし。

まとめ

  • link_to_if にブロックを渡した時の挙動は少し分かりづらい
  • ただ、 Rails_if のメソッドとしては一貫性がある
  • 命名に悩むような難しいメソッドは、そもそも設計がダメ

Rails にプルリクを投げる前に気づくことができて良かった。

RubyKaigi 2016 で色んな Rubyistlink_to_if について話を聞いて、助言を頂いたお陰です。感謝。

*1:似たような例だと「公開日時以降はバナーをリンク可能にする」「公開設定のリソースをリンク可能にする」など

tachikoma_ai を v0.3.0 にアップデートしました #mokumoku_onsen

7/16(土)〜18(月)の2泊3日で開発合宿に参加していて、前々からやりたかった tachikoma_ai の gem のアップデートをしました。

開発合宿について、詳しくは id:niwatako さんが記事を書かれているので、興味ある方はそちらを参照して下さい。

niwatako.hatenablog.jp

TachikomaAi について

TachikomaAi は sanemat/tachikoma の拡張で、 bundle update のときプルリクに github の比較URLをつける gem です。

f:id:sinsoku:20160229024048p:plain

実は tachikoma_ai を仕事でも使っているのですが、使っていて機能不足なところやバグを見つけたので、今回直しました。

変更内容

v0.2.0

  • 0.1.0 のような v のついてないタグに対応

v0.3.0

.gemspec の対応について

.gemspec の homepage 以外で github.com の URL を探す方法が見つからなかったので、 json ファイルでリンクを管理して対応しました。
もし TachikomaAi を使っている人で、対応 gem を増やしたい人は json にリンクを追加してプルリクください。

github 指定について

vendor/bundle/ruby/2.3.0/bundler/gems/ の下の .gemspec を使えば対応できそうだけど、ちょっと対応に時間かかりそう。
また時間のあるときに対応したい。

これで来週からお仕事での gem のアップデート確認が少し楽になる(∩´∀`)∩

Swift でトランプを実装しようとして、勉強になってる話

まだ全然実装できていないけど、とても勉強になっているので途中経過をブログに書いてみる。

github.com

なぜやっているのか

トランプくらい簡単に実装できる・・・そう思っていた時期が(ry

環境

この環境で勉強を始めたのが間違いだったかもしれない。

やりたいこと

  • 汎用的にトランプのゲームを作れるようなライブラリ設計にする
  • ポーカー、七並べ、大富豪など、メジャーなゲームを実装する

設計など

  • Card が Rank ( 1 〜 13 ) と Suit (スペード、ハート、クラブ、ダイヤ) を持つ
  • Card の強弱をゲームによって変える
    • Generics で変更できるようにした
  • まだ Jocker は未実装

Swift で実装方法が分からなかった箇所など

stringLiteralConvertible で特定文字だけ許容したい

let card: Card<DefaultComparing> = "13S"

これで「スペード13」が生成したかった。結局、できなかったので、 init? を使った。

https://github.com/sinsoku/PlayingCard/blob/908faee6fe4fe1d66e028ee481b6ad07c06f4322/Sources/PlayingCard/Card.swift#L14-L33

init?(_ value: String) {
  var characters = value.characters
  let suit = characters.popLast()
  let rank = Int(String(characters))

  guard let rankInt = rank where 1...13 ~= rankInt else {
    return nil
  }

  guard let suitStr = suit where ["S", "H", "C", "D"].contains(suitStr) else {
    return nil
  }

  switch suitStr {
    case "S": self = Card<T>(rank: Rank(rawValue: rankInt)!, suit: .Spade)
    case "H": self = Card<T>(rank: Rank(rawValue: rankInt)!, suit: .Heart)
    case "C": self = Card<T>(rank: Rank(rawValue: rankInt)!, suit: .Club)
    case "D": self = Card<T>(rank: Rank(rawValue: rankInt)!, suit: .Diamond)
    default: return nil
  }
}

無理矢理感がある・・・。
もうちょい綺麗になりそうだけど、よく分からんかったので他の実装を進めている。

戻り値を Set, Array で切り替えたい

https://github.com/sinsoku/PlayingCard/blob/908faee6fe4fe1d66e028ee481b6ad07c06f4322/Sources/PlayingCard/Util.swift

class Util {
    static func factory<C: CardInitializeable>(_ values: String...) -> Set<C> {
      let cards = values.map { C($0)! }
      return Set(cards)
    }
}

戻り値も Generics でうまく返したかったけど、よく分からなかった。

let arrCards: Array<Card<DefaultComparing>> = Util.factory("1S", "2S", "3S", "4S", "5S")
let setCards: Set<Card<DefaultComparing>> = Util.factory("1S", "2S", "3S", "4S", "5S")

本当は ArrayLiteral で定義できると綺麗なんだけど、これ init? だから出来ないんですよね・・・。

let setCards: Set<Card<DefaultComparing>> = ["1S", "2S", "3S", "4S", "5S"]

Swift で Generics を使って Comparable の挙動を変更する

昨日の続きを調べて、一応やりたかったことは実現できたのでブログにも書いておく。

sinsoku.hatenablog.com

サンプルコード

gist のコードの通りだけど、 Generics を使ってクラスの初期化時に Comparable に使う struct を渡せるようにしている。 こうやっておけば let array = [A<Q>()] みたいに同じ Comparable の型に限定した Array が作れて型安全。

WIP Pull Request Unhighlignter for GitHub が使えなくなったので Tampermonkey の UserScript で再実装した

WIP Pull Request Unhighlignter for GitHub が便利で今まで使っていたけど、昨日突然使えなくなった。

※ 原因はGitHubCSSセレクタが変わったため。現在は kyanny/chrome-ext-wip-pull-request-unhighlighter-for-github#6 がマージされ動くようになってる。

再実装した

待っていれば直ると思ったけど、予想以上に無いと困ったので最低限の機能を Tampermonkey の UserScript として再実装した。 ついでに、 pjax で画面遷移した時も動くようにした。

本当はプルリク投げた方が良いかなーと思いつつ、CSSセレクタの指定は好みある*1だろうし、テストコードも無かったので面倒で止めた。

*1:個人的にはclass指定よりidの方が壊れにくいと思うけど、本家はclass使っている