💎
Rails でバージョン情報 (Gem::Version) をソート可能な状態で DB に保存する
10.1.0 のようなバージョン情報は単純な数値としては扱えないし、文字列として扱っても比較やソートがうまくできない (10.0 < 9.0 になってしまう)。
Gem::Version だと比較やソートが可能になるが DB での比較・ソートを可能にするためには、serialize の方法を工夫する必要がある。
serialize の方法
ここでは下記の形式のみを対象とする。
-
\A\d{1,4}+(\.\d{1,4})*[^ ]*\zつまり、1-4桁の数字で始まり.で区切られた1-4桁の数字が続き、最後に (スペースをのぞく) 任意の文字列があってもよい - 数字列は
0以外は0で始まらないものとする - 具体的には
1.23.456-alphaのようなもの
serialize 時は . で区切られた数字列は 4 桁になるようにスペース で padding する。
1.23.456-alpha
は
1. 23. 456-alpha
のようになる。
これで実質 . で区切られた数字列ごとの比較になるため、意図した比較・ソートが可能になる。
DB から読むときにはスペース を削除するだけでよい。
ActiveRecord::Type
Rails で扱いやすくするため Type を定義する。
config/initializers/version_type.rb を作成する。
(対応していない形式のための例外処理は省略)
class VersionType < ActiveRecord::Type::String
def self.serialize(value)
return nil if value.nil?
value.to_s.match(/\A([\d.]+)(.*)\z/)
parts, suffix = $~.captures
parts.to_s.split(".").map {|s| s.rjust(4, " ") }.join(".") + suffix
end
# attribute_name= で assign された値の変換
def cast(value)
case value
when Gem::Version
value
when NilClass
nil
else
begin
# `v1.0` などを `1.0` として扱うため数字の前の文字列を削除
parsed = value.to_s.gsub(/\A[^\d]*/, "").gsub(" ", "")
Gem::Version.new(parsed)
rescue ArgumentError
nil
end
end
end
# DB から読んだ値の変換
def deserialize(value)
return nil if value.nil?
parsed = value.to_s.gsub(" ", "")
Gem::Version.new(parsed)
end
# DB に書き込む値
def serialize(value)
self.class.serialize(value)
end
end
ActiveRecord::Type.register(:version, VersionType)
これで Model のなかで attribute :foo_version, :version のように書くと、assign や DB から読んだときに Gem::Version オブジェクトになり、DB に保存するときは前述の方法で serialize される。
Discussion