酒と泪とRubyとRailsと

Ruby on Rails と Objective-C は酒の肴です!

ActiveRecord::Type::Value を継承した独自タイプの作成

Rails5を少し触っていますが、「ActiveRecord::Type::Value」等を継承した独自クラスを設定する手順で少しハマったのでメモ。基本的には継承元のクラスを見ればなんとなくわかるはず。


やりたいこと

こんな感じでActiveRecordのattributeに独自クラスにキャストして使いたいっす。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# config/initializers/active_record/money_type.rb
class MoneyType < ActiveRecord::Type::Integer
  def cast(value)
    if !value.kind_of?(Numeric) && value.include?('$')
      price_in_dollars = value.gsub(/\$/, '').to_f
      super(price_in_dollars * 100)
    else
      super
    end
  end
end

ActiveRecord::Type.register(:money, MoneyType)

# /app/models/store_listing.rb
class StoreListing < ActiveRecord::Base
  attribute :price_in_cents, :money
end

store_listing = StoreListing.new(price_in_cents: '$10.00')
store_listing.price_in_cents # => 1000

こうすることで共通の処理を一箇所にまとめられたり、値がない場合などの処理を一箇所に書くことができます。

ドキュメントとしてはこちらが一番参考になりそう。

ActiveRecord::Attributes::ClassMethods

継承元のクラス

継承元のクラスだけでもこれだけあるので、どれをキャストするか考える。

1
2
3
4
5
6
7
8
9
10
11
ActiveModel::Type::BigInteger
ActiveModel::Type::Binary
ActiveModel::Type::Boolean
ActiveModel::Type::Decimal
ActiveModel::Type::DecimalWithoutScale
ActiveModel::Type::Float
ActiveModel::Type::Integer
ActiveModel::Type::String
ActiveModel::Type::Text
ActiveModel::Type::UnsignedInteger
ActiveModel::Type::Value

ActiveModel::Type::Value を継承元に使う

今回は、ActiveModel::Type::Value を継承元に使います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# config/initializers/active_record/money_type.rb
class OriginalJson
  def initialize(hash)
    @json = hash.try(:symbolize_keys)
  end

  def hoge
    @json['hoge']
  end

  def to_h
    @json
  end

  # For casting
  class Type < ActiveRecord::Type::Value
    # 型を書く
    def type
    end

    # DBから値を取り出す時のキャスト処理
    def deserialize(value)
      cast(value)
    end

    # ユーザー入力をキャストする処理
    def cast(value)
      if String === value
        decoded = ::ActiveSupport::JSON.decode(value) rescue nil
        ::OriginalJson.new(decoded)
      else
        super
      end
    end

    # DBの保存のために、DBが認識できる型に変換
    # +String+, +Numeric+, +Date+, +Time+, +Symbol+, +true+, +false+, +nil+
    def serialize(value)
      case value
        when Hash
          ::ActiveSupport::JSON.encode(value)
        when ::OriginalJson
          ::ActiveSupport::JSON.encode(value.to_h)
        else
          super
      end
    end
  end
end

# Adding OriginalJson::Type to ActiveRecord::Type
ActiveRecord::Type.register(:original_json, OriginalJson::Type)

雑な実装なので、状況に応じて書きなおしてください。

あとがき

Railsのコード読むのは結構勉強になる。

Special Thanks

おすすめの書籍