Permalink
Browse files

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

…/value stores [DHH]
  • Loading branch information...
dhh committed Oct 13, 2011
1 parent 8f11d53 commit 85b64f98d100d37b3a232c315daa10fad37dccdc
View
@@ -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.
@@ -69,6 +69,7 @@ module ActiveRecord
autoload :Schema
autoload :SchemaDumper
autoload :Serialization
autoload :Store
autoload :SessionStore
autoload :Timestamp
autoload :Transactions
@@ -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)
@@ -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

This comment has been minimized.

Show comment
Hide comment
@phillipoertel

phillipoertel Oct 13, 2011

backed => baked.

@phillipoertel

phillipoertel Oct 13, 2011

backed => baked.

# 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
@@ -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
@@ -1,3 +1,4 @@
class Admin::User < ActiveRecord::Base
belongs_to :account
store :settings, accessors: [ :color, :homepage ]
end
@@ -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

This comment has been minimized.

Show comment
Hide comment
@sikachu

sikachu Oct 13, 2011

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 :)
Member

sikachu replied Oct 13, 2011

  • 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 comment has been minimized.

Show comment
Hide comment
@clemens

clemens Oct 13, 2011

Contributor

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

Contributor

clemens replied Oct 13, 2011

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

@ozeias

This comment has been minimized.

Show comment
Hide comment
@ozeias

ozeias Oct 13, 2011

This is very cool and clean!

ozeias replied Oct 13, 2011

This is very cool and clean!

@flippingbits

This comment has been minimized.

Show comment
Hide comment
@flippingbits

flippingbits Oct 13, 2011

Contributor

Awesome feature!

@sikachu why do you need data types?

Contributor

flippingbits replied Oct 13, 2011

Awesome feature!

@sikachu why do you need data types?

@sikachu

This comment has been minimized.

Show comment
Hide comment
@sikachu

sikachu Oct 13, 2011

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).

Member

sikachu replied Oct 13, 2011

@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

This comment has been minimized.

Show comment
Hide comment
@flippingbits

flippingbits Oct 13, 2011

Contributor

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.

Contributor

flippingbits replied Oct 13, 2011

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

This comment has been minimized.

Show comment
Hide comment
@kurko

kurko Oct 13, 2011

Something like this, @sikachu?

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

kurko replied Oct 13, 2011

Something like this, @sikachu?

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

This comment has been minimized.

Show comment
Hide comment
@flippingbits

flippingbits Oct 13, 2011

Contributor

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?

Contributor

flippingbits replied Oct 13, 2011

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

This comment has been minimized.

Show comment
Hide comment
@colszowka

colszowka Oct 13, 2011

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 replied Oct 13, 2011

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

This comment has been minimized.

Show comment
Hide comment
@colszowka

colszowka Oct 13, 2011

@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 :)

colszowka replied Oct 13, 2011

@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

This comment has been minimized.

Show comment
Hide comment
@flippingbits

flippingbits Oct 13, 2011

Contributor

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

Contributor

flippingbits replied Oct 13, 2011

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

@kurko

This comment has been minimized.

Show comment
Hide comment
@kurko

kurko Oct 14, 2011

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

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

kurko replied Oct 14, 2011

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

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

@matthuhiggins

This comment has been minimized.

Show comment
Hide comment
@matthuhiggins

matthuhiggins Oct 14, 2011

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.

matthuhiggins replied Oct 14, 2011

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

This comment has been minimized.

Show comment
Hide comment
@codesnik

codesnik Oct 14, 2011

Contributor

@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.

Contributor

codesnik replied Oct 14, 2011

@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

This comment has been minimized.

Show comment
Hide comment
@matthuhiggins

matthuhiggins Oct 14, 2011

@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).

matthuhiggins replied Oct 14, 2011

@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

This comment has been minimized.

Show comment
Hide comment
@laserlemon

laserlemon Oct 14, 2011

Contributor

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
Contributor

laserlemon replied Oct 14, 2011

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

This comment has been minimized.

Show comment
Hide comment
@laserlemon

laserlemon Oct 14, 2011

Contributor

Please don't store credit cards like that.

Contributor

laserlemon replied Oct 14, 2011

Please don't store credit cards like that.

@jaylevitt

This comment has been minimized.

Show comment
Hide comment
@jaylevitt

jaylevitt Oct 19, 2011

Contributor

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.

Contributor

jaylevitt replied Oct 19, 2011

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

This comment has been minimized.

Show comment
Hide comment
@jaylevitt

jaylevitt Oct 19, 2011

Contributor

@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.

Contributor

jaylevitt replied Oct 19, 2011

@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

This comment has been minimized.

Show comment
Hide comment
@kurko

kurko Oct 19, 2011

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

kurko replied Oct 19, 2011

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

@jaylevitt

This comment has been minimized.

Show comment
Hide comment
@jaylevitt

jaylevitt Oct 20, 2011

Contributor

@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.

Contributor

jaylevitt replied Oct 20, 2011

@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

This comment has been minimized.

Show comment
Hide comment
@kurko

kurko 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.

kurko replied 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.