store.rb 5.1 KB
Newer Older
1 2
require 'active_support/core_ext/hash/indifferent_access'

3 4
module ActiveRecord
  # Store gives you a thin wrapper around serialize for the purpose of storing hashes in a single column.
C
chrismcc 已提交
5
  # It's like a simple key/value store baked into your record when you don't care about being able to
6 7 8 9 10 11 12 13 14
  # query that store outside the context of a single record.
  #
  # You can then declare accessors to this store that are then accessible just like any other attribute
  # of the model. This is very helpful for easily exposing store keys to a form or elsewhere that's
  # already built around just accessing attributes on the model.
  #
  # Make sure that you declare the database column used for the serialized store as a text, so there's
  # plenty of room.
  #
15 16 17
  # You can set custom coder to encode/decode your serialized attributes to/from different formats.
  # JSON, YAML, Marshal are supported out of the box. Generally it can be any wrapper that provides +load+ and +dump+.
  #
18 19 20
  # Examples:
  #
  #   class User < ActiveRecord::Base
21
  #     store :settings, accessors: [ :color, :homepage ], coder: JSON
22
  #   end
23
  #
24
  #   u = User.new(color: 'black', homepage: '37signals.com')
25 26 27 28 29 30
  #   u.color                          # Accessor stored attribute
  #   u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor
  #
  #   # There is no difference between strings and symbols for accessing custom attributes
  #   u.settings[:country]  # => 'Denmark'
  #   u.settings['country'] # => 'Denmark'
31 32 33 34 35
  #
  #   # Add additional accessors to an existing store through store_accessor
  #   class SuperUser < User
  #     store_accessor :settings, :privileges, :servants
  #   end
36 37 38 39
  #
  # The stored attribute names can be retrieved using +stored_attributes+.
  #
  #   User.stored_attributes[:settings] # [:color, :homepage]
M
Matt Jones 已提交
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
  #
  # == Overwriting default accessors
  #
  # All stored values are automatically available through accessors on the Active Record
  # object, but sometimes you want to specialize this behavior. This can be done by overwriting
  # the default accessors (using the same name as the attribute) and calling
  # <tt>read_store_attribute(store_attribute_name, attr_name)</tt> and
  # <tt>write_store_attribute(store_attribute_name, attr_name, value)</tt> to actually
  # change things.
  #
  #   class Song < ActiveRecord::Base
  #     # Uses a stored integer to hold the volume adjustment of the song
  #     store :settings, accessors: [:volume_adjustment]
  #
  #     def volume_adjustment=(decibels)
  #       write_store_attribute(:settings, :volume_adjustment, decibels.to_i)
  #     end
  #
  #     def volume_adjustment
  #       read_store_attribute(:settings, :volume_adjustment).to_i
  #     end
  #   end
62 63
  module Store
    extend ActiveSupport::Concern
64

65
    included do
66
      class_attribute :stored_attributes, instance_accessor: false
67 68 69
      self.stored_attributes = {}
    end

70 71
    module ClassMethods
      def store(store_attribute, options = {})
72
        serialize store_attribute, IndifferentCoder.new(options[:coder])
73 74 75 76
        store_accessor(store_attribute, options[:accessors]) if options.has_key? :accessors
      end

      def store_accessor(store_attribute, *keys)
77 78
        keys = keys.flatten
        keys.each do |key|
79
          define_method("#{key}=") do |value|
M
Matt Jones 已提交
80
            write_store_attribute(store_attribute, key, value)
81
          end
82

83
          define_method(key) do
M
Matt Jones 已提交
84
            read_store_attribute(store_attribute, key)
85 86
          end
        end
87

88 89
        self.stored_attributes[store_attribute] ||= []
        self.stored_attributes[store_attribute] |= keys
90 91
      end
    end
92

M
Matt Jones 已提交
93 94 95 96 97 98 99 100 101 102 103 104 105 106
    protected
      def read_store_attribute(store_attribute, key)
        attribute = initialize_store_attribute(store_attribute)
        attribute[key]
      end

      def write_store_attribute(store_attribute, key, value)
        attribute = initialize_store_attribute(store_attribute)
        if value != attribute[key]
          send :"#{store_attribute}_will_change!"
          attribute[key] = value
        end
      end

107 108
    private
      def initialize_store_attribute(store_attribute)
109 110 111 112 113 114
        attribute = send(store_attribute)
        unless attribute.is_a?(HashWithIndifferentAccess)
          attribute = IndifferentCoder.as_indifferent_hash(attribute)
          send :"#{store_attribute}=", attribute
        end
        attribute
115
      end
116

117
    class IndifferentCoder # :nodoc:
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
      def initialize(coder_or_class_name)
        @coder =
          if coder_or_class_name.respond_to?(:load) && coder_or_class_name.respond_to?(:dump)
            coder_or_class_name
          else
            ActiveRecord::Coders::YAMLColumn.new(coder_or_class_name || Object)
          end
      end

      def dump(obj)
        @coder.dump self.class.as_indifferent_hash(obj)
      end

      def load(yaml)
        self.class.as_indifferent_hash @coder.load(yaml)
      end

      def self.as_indifferent_hash(obj)
        case obj
137
        when HashWithIndifferentAccess
138 139 140 141 142 143 144 145
          obj
        when Hash
          obj.with_indifferent_access
        else
          HashWithIndifferentAccess.new
        end
      end
    end
146
  end
147
end