Skip to content
Browse files

Added ActiveRecord::Base.store for declaring simple single-column key…

…/value stores [DHH]
  • Loading branch information...
1 parent 8f11d53 commit 85b64f98d100d37b3a232c315daa10fad37dccdc @dhh dhh committed Oct 13, 2011
View
11 activerecord/CHANGELOG
@@ -1,5 +1,16 @@
*Rails 3.2.0 (unreleased)*
+* Added ActiveRecord::Base.store for declaring simple single-column key/value stores [DHH]
+
+ class User < ActiveRecord::Base
+ store :settings, accessors: [ :color, :homepage ]
+ end
+
+ u = User.new(color: 'black', homepage: '37signals.com')
+ u.color # Accessor stored attribute
+ u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor
+
+
* MySQL: case-insensitive uniqueness validation avoids calling LOWER when
the column already uses a case-insensitive collation. Fixes #561.
View
1 activerecord/lib/active_record.rb
@@ -69,6 +69,7 @@ module ActiveRecord
autoload :Schema
autoload :SchemaDumper
autoload :Serialization
+ autoload :Store
autoload :SessionStore
autoload :Timestamp
autoload :Transactions
View
2 activerecord/lib/active_record/base.rb
@@ -2145,7 +2145,7 @@ def populate_with_current_scope_attributes
# AutosaveAssociation needs to be included before Transactions, because we want
# #save_with_autosave_associations to be wrapped inside a transaction.
include AutosaveAssociation, NestedAttributes
- include Aggregations, Transactions, Reflection, Serialization
+ include Aggregations, Transactions, Reflection, Serialization, Store
NilClass.add_whiner(self) if NilClass.respond_to?(:add_whiner)
View
49 activerecord/lib/active_record/store.rb
@@ -0,0 +1,49 @@
+module ActiveRecord
+ # Store gives you a thin wrapper around serialize for the purpose of storing hashes in a single column.
+ # It's like a simple key/value store backed into your record when you don't care about being able to

backed => baked.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ # 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.
+ #
+ # Examples:
+ #
+ # class User < ActiveRecord::Base
+ # store :settings, accessors: [ :color, :homepage ]
+ # end
+ #
+ # u = User.new(color: 'black', homepage: '37signals.com')
+ # u.color # Accessor stored attribute
+ # u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor
+ #
+ # # Add additional accessors to an existing store through store_accessor
+ # class SuperUser < User
+ # store_accessor :settings, :privileges, :servants
+ # end
+ module Store
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ def store(store_attribute, options = {})
+ serialize store_attribute, Hash
+ store_accessor(store_attribute, options[:accessors]) if options.has_key? :accessors
+ end
+
+ def store_accessor(store_attribute, *keys)
+ Array(keys).flatten.each do |key|
+ define_method("#{key}=") do |value|
+ send(store_attribute)[key] = value
+ end
+
+ define_method(key) do
+ send(store_attribute)[key]
+ end
+ end
+ end
+ end
+ end
+end
View
29 activerecord/test/cases/store_test.rb
@@ -0,0 +1,29 @@
+require 'cases/helper'
+require 'models/admin'
+require 'models/admin/user'
+
+class StoreTest < ActiveRecord::TestCase
+ setup do
+ @john = Admin::User.create(name: 'John Doe', color: 'black')
+ end
+
+ test "reading store attributes through accessors" do
+ assert_equal 'black', @john.color
+ assert_nil @john.homepage
+ end
+
+ test "writing store attributes through accessors" do
+ @john.color = 'red'
+ @john.homepage = '37signals.com'
+
+ assert_equal 'red', @john.color
+ assert_equal '37signals.com', @john.homepage
+ end
+
+ test "accessing attributes not exposed by accessors" do
+ @john.settings[:icecream] = 'graeters'
+ @john.save
+
+ assert 'graeters', @john.reload.settings[:icecream]
+ end
+end
View
1 activerecord/test/models/admin/user.rb
@@ -1,3 +1,4 @@
class Admin::User < ActiveRecord::Base
belongs_to :account
+ store :settings, accessors: [ :color, :homepage ]
end
View
1 activerecord/test/schema/schema.rb
@@ -37,6 +37,7 @@ def create_table(*args, &block)
create_table :admin_users, :force => true do |t|
t.string :name
+ t.text :settings
t.references :account
end

22 comments on commit 85b64f9

@sikachu
Ruby on Rails member
  • Guide update please
  • It would be nice to be able to define a data type on those store keys, so it would be acting more like a real column :)
@clemens

This is cool – now one doesn't have to do it manually.

@ozeias
ozeias commented on 85b64f9 Oct 13, 2011

This is very cool and clean!

@flippingbits

Awesome feature!

@sikachu why do you need data types?

@sikachu
Ruby on Rails member

@flipping because I don't want this to happen:

class User < ActiveRecord::Base
   store :settings, :accessors => [:answer_of_life, :alive]
end
user.answer_of_life = 42
user.answer_of_life #=> "42"

user.alive = false
user.alive #=> "false"

I actually think that we only need :integer, :float, :boolean, :string (default).

@flippingbits

Ah, I see the problem.

I'd suggest that the usage of the feature stays the same but the internal storage changes.
We could additionally store the data type of the given value by using the class method.
So, there's no need to define these data types yourself.

@kurko
kurko commented on 85b64f9 Oct 13, 2011

Something like this, @sikachu?

store :settings, :accessors => [:answer_of_life => :integer, :alive => :boolean]
@flippingbits

BTW, the following test is green:

test "data types of store attributes" do
  @john.settings[:age] = 42
  @john.save

  assert_equal Fixnum, @john.reload.settings[:age].class
end

Does the problem with the data types really exist?

@colszowka

Wouldn't an :accessors option on ActiveRecord::Base.serialize have been sufficient here? I think adding another thing to the API that masks actual serialization of Hashes might be a bit puzzling to newcomers.

@colszowka

@flippingbits: Actually, yes. The values in the Hash are stored in AR serialization in the format they arrive in. So if you set an integer in your tests / console you're fine. But when you're getting input from a rails form, it'll be a String because no magic typecasting happens like with proper AR attributes. The worst thing about this are Booleans. Those arrive as "1" or "" from checkboxes in the form (I think, not 100% sure) by default and therefore you need to pre-process them manually to have true/false in your settings store. So a massive +1 on explicit typecasting based upon AR's column type casting mechanisms. I think it'd be possible to give this either an array or as @kurko propsed (with wrong brackets ;) a hash so people could still screw typecasting if they'd rather bother with the mess :)

@flippingbits

@colszowka It seems that I've forgotten forms :) Thanks for the hint!

@kurko
kurko commented on 85b64f9 Oct 14, 2011

ops, I copied the code and edited leaving the wrong brackets.

Personally, I think it's less than low priority.

@matthuhiggins

Could typecasting the values of an attributes hash be an ActiveModel concern? It seems like a common need for models.

ActiveRecord reads each column type and converts attribute values. Data mappers for CouchDB, Cassandra, etc., convert input into defined types, which are stored as json, and vice versa. Does an ActiveModel::TypeCasting module make sense? (Similar to Dirty, it depends on ActiveModel::AttributeMethods).

This thought is inspired by some of the above comments, though it may be too much bloat for only the store.rb module.

@codesnik

@matthuhiggins ActiveModel::TypeCasting made sense in the earliest rails 2.x. Whole "before_typecast" urges to be moved out.
Don't need that column in your database, just an accessor? sorry, have to write your own conversion logic. Want to show original enetered value, not typecasted, in your form on validation error? whole new bunch of boilerplate code. Want to make date accessor with "date(1i)" kind attributes? sorry, that's AR thing. And every other storage framework just tries to mimic AR typecasting, more or less succesfully.

I've tried to approach that problem several times, but failed to abstract it well enough for general use. Maybe someone with better abstraction skills could do that.

@matthuhiggins

@codesnik I think it should start out by sticking to active_record types, (which is probably equivalent to json types plus a time type). This would allow for another piece of active_record to be refactored into active_model. An implementor might only need to implement one method (define_attribute_types).

@laserlemon

How about namespacing the accessors?

class User < ActiveRecord::Base
  store :credit_card, :accessors => [:number, :expiration], :namespace => true
end
user.credit_card_number
user.credit_card_expiration

Could also be configured:

class User < ActiveRecord::Base
  store :credit_card, :accessors => [:number, :expiration], :namespace => :card
end
user.card_number
user.card_expiration
@laserlemon

Please don't store credit cards like that.

@jaylevitt

Would core be interested in patches to store these as an hstore in Postgres? We've been using them for at least five days now and they seem to map perfectly to this concent; the timing's ideal, and it'd let you do database-side queries against stores when you want to.

@jaylevitt

@matthuhiggins @codesnik Come to think of it, would you guys be interested in collaborating on broader typecasting patches? That's bugged me since day 1, and I have some active use cases we could develop against (a user-defined type in Postgres, hstore for hashes). Email me at jay@jay.fm.

@kurko
kurko commented on 85b64f9 Oct 19, 2011

@jaylevitt this might interest you: github.com/softa/activerecord-postgres-hstore

@jaylevitt

@kurko thanks - looks like a great gem, but it too could use some nice hash<->hstore magic; as it is you have to declare the fields as string, and call Hash.to_hstore yourself, and call special functions to delete keys and such. That's exactly the type of stuff AR could do with typecasting. Open call for contributors - I have really wanted to do this, but haven't contributed a Rails patch before and could use a co-conspirator.

@kurko
kurko commented on 85b64f9 Oct 21, 2011

I suggest you to post on Lighthouse and see what the core team thinks about it. I think that maybe they'll tell you it's a feature for the pg adapter, not Active Record.

Although I'm out of time these days, I'll watch and participate whenever I can.

Please sign in to comment.
Something went wrong with that request. Please try again.