I have a model which has one actual column in the database. This column is stored as a JSON string of configuration. I use a bunch of virtual attributes which I want to map inside of this configuration JSON attribute. I basically dont want to create a bunch columns in the db, but rather use this one JSON attribute to contain everything. Is there a cleaner way than the below defs of achieving this?
class Device < ActiveRecord::Base
attr_accessible :configuration
serialize :configuration, JSON
attr_accessor :background_color, :title
# below is ew
def background_color; self.configuration["background_color"]; end
def background_color=(value); self.configuration["background_color"] = value; end
def title; self.configuration["title"]; end
def title=(value); self.configuration["title"] = value; end
end
Ideally i'd be looking for something like attr_maps_to_hash :configuration, [:background_color, :title]. Does something like this exist?
You can use ActiveRecord::Store for this.
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
If you are using PostgreSQL, check out HStore.
Rails as of 3.2 has key-value stores built into ActiveRecord-see here:
Where can i read more about Rails 3.2's Data Store with key-value in textfield?
in your case you can have a text field named configuration and then do this:
class Device < AR::Base
store :configuration, accessors: [:title, :background_color, ...]
...
This should work fine with forms, etc..
Two methods come to mind.
First, you could have an array of your attributes [:background_color, :title] and then iterate over them while calling define_method. You would defined two methods, define(method_name) and define("#{method_name}=").
Second, a similar idea but using method missing.
def method_missing(method_name, *args, &block)
...see if it's a get or set...
...do your stuff...
...rain dance...
...yay...
end
Related
I have a hash store as follows:
store :order_details, accessors: %w{item_name, external_id, total......etc}, coder: JSON
I'm deserializing this to a ruby object using a lib class elsewhere with a couple of methods in it for validations/operations
I would like to make use of the rails 5 attributes api instead so that I could directly have:
attribute :order_details, Type::OrderDetailType.new
(plus it would make it easier to add validations to each field in my hash)
I've seen examples online for using rails5's attributes api for simple virtual attributes(strings, integers...etc), but have not come across any info on how to implement it for a hash attribute.
Would appreciate it if someone could point me in the right direction.
Extend Type::OrderDetailType from ActiveRecord::Type::Value
You can override cast and serialize method.
def cast(value)
value
end
def serialize(value)
value
end
An example of Money type:
# app/models/product.rb
class Product < ApplicationRecord
attribute :price_in_cents, MoneyType.new
end
class MoneyType < ActiveRecord::Type::Integer
def type_cast(value)
# convert values like '$10.00' to 1000
end
end
product = Product.new(price_in_cents: '$10.00')
product.price_in_cents #=> 1000
Doc: http://edgeapi.rubyonrails.org/classes/ActiveRecord/Attributes/ClassMethods.html
Example: http://nithinbekal.com/posts/rails-5-features/
In PHP, I can set an attribute (that is not a column in database) to a model. E.g.(PHP code),
$user = new User;
$user->flag = true;
But in rails, when I set any attribute that doesn't exist in database, it will throw an error undefined method flag. There is attr_accessor method, but what will happen if I need about ten temp attributes?
but what will happen if I need about ten temp attributes?
#app/models/user.rb
class User < ActiveRecord::Base
attr_accessor :flag, :other_attribute, :other_attribute2, :etc...
end
attr_accessor creates "virtual" attributes in Rails -- they don't exist in the database, but are present in the model.
As with attributes from the db, attr_accessor just creates a set of setter & getter (instance) methods in your class, which you can call & interact with when the class is initialized:
#app/models/user.rb
class User < ActiveRecord::Base
attr_accessor :flag
# getter
def flag
#flag
end
# setter
def flag=(val)
#flag = val
end
end
This is expected because it's how ActiveRecord works by design. If you need to set arbitrary attributes, then you have to use a different kind of objects.
For example, Ruby provides a library called OpenStruct that allows you to create objects where you can assign arbitrary key/values. You may want to use such library and then convert the object into a corresponding ActiveRecord instance only if/when you need to save to the database.
Don't try to model ActiveRecord to behave as you just described because it was simply not designed to behave in that way. That would be a cargo culting error from your current PHP knowledge.
As the guys explained, attr_accessor is just a quick setter and getter.
We can set our Model attr_accessor on record initializing to be a Ruby#Hash for example using ActiveRecord#After_initilize method so we get more flexibility on temporarily storing values (idea credit to this answer).
Something like:
class User < ActiveRecord::Base
attr_accessor :vars
after_initialize do |user|
self.vars = Hash.new
end
end
Now you could do:
user = User.new
#set
user.vars['flag'] = true
#get
user.vars['flag']
#> true
All that attr_accessor does is add getter and setter methods which use an instance variable, eg this
attr_accessor :flag
will add these methods:
def flag
#flag
end
def flag=(val)
#flag = val
end
You can write these methods yourself if you want, and have them do something more interesting than just storing the value in an instance var, if you want.
If you need temp attributes you can add them to the singleton object.
instance = Model.new
class << instance
attr_accessor :name
end
Is it possible / advisable to have a member of a class that is not persisted to the database for a rails model?
I want to store the last type the user selects in a session variable. Since I cant set the session variable from my model, I want to store the value in a "dummy" class member that just passes the value back to the controller.
Can you have such a class member?
Adding non-persisted attributes to a Rails model is just like any other Ruby class:
class User < ActiveRecord::Base
attr_accessor :someattr
end
me = User.new(name: 'Max', someattr: 'bar')
me.someattr # "bar"
me.someattr = 'foo'
The extended explanation:
In Ruby all instance variables are private and do not need to be defined before assignment.
attr_accessor creates a setter and getter method:
class User < ActiveRecord::Base
def someattr
#someattr
end
def someattr=(value)
#someattr = value
end
end
There is one special thing going on here; Rails takes the hash you pass to User.new and maps the values to attributes. You could simulate this behavior in a plain ruby class with something like:
class Foo
attr_accessor :bar
def initialize(hash)
hash.keys.each do |key|
setter = "#{key}=".intern
self.send(setter, hash[key]) if self.respond_to? setter
end
end
end
> Foo.new(bar: 'baz')
=> <Foo:0x0000010112aa50 #bar="baz">
Classes in Ruby can also be re-opened at any point, ActiveRecord uses this ability to "auto-magically" add getters and setters to your models based on its database columns (ActiveRecord figures out which attributes to add based on the database schema).
Yes you can, the code below allows you to set my_class_variable and inside the model reference it as #my_class_variable
class MyCLass < ActiveRecord::Base
attr_accessor :my_class_variable
def do_something_with_it
#my_class_variable + 10
end
I have the following class:
class Profile < ActiveRecord::Base
serialize :data
end
Profile has a single column data that holds a serialized hash. I would like to define accessors into that hash such that I can execute profile.name instead of profile.data['name']. Is that possible in Rails?
The simple straightforward way:
class Profile < ActiveRecord::Base
serialize :data
def name
self.data['name']
end
def some_other_attribute
self.data['some_other_attribute']
end
end
You can see how that can quickly become cumbersome if you have lots of attributes within the data hash that you want to access.
So here's a more dynamic way to do it and it would work for any such top level attribute you want to access within data:
class Profile < ActiveRecord::Base
serialize :data
def method_missing(attribute, *args, &block)
return super unless self.data.key? attribute
self.data.fetch(attribute)
end
# good practice to extend respond_to? when using method_missing
def respond_to?(attribute, include_private = false)
super || self.data.key?(attribute)
end
end
With the latter approach you can just define method_missing and then call any attribute on #profile that is a key within data. So calling #profile.name would go through method_missing and grab the value from self.data['name']. This will work for whatever keys are present in self.data. Hope that helps.
Further reading:
http://www.trottercashion.com/2011/02/08/rubys-define_method-method_missing-and-instance_eval.html
http://technicalpickles.com/posts/using-method_missing-and-respond_to-to-create-dynamic-methods/
class Profile < ActiveRecord::Base
serialize :data # always a hash or nil
def name
data[:name] if data
end
end
I'm going to answer my own question. It looks like ActiveRecord::Store is what I want:
http://api.rubyonrails.org/classes/ActiveRecord/Store.html
So my class would become:
class Profile < ActiveRecord::Base
store :data, accessors: [:name], coder: JSON
end
I'm sure everyone else's solutions work just fine, but this is so clean.
class Profile < ActiveRecord::Base
serialize :data # always a hash or nil
["name", "attr2", "attr3"].each do |method|
define_method(method) do
data[method.to_sym] if data
end
end
end
Ruby is extremely flexible and your model is just a Ruby Class. Define the "accessor" method you want and the output you desire.
class Profile < ActiveRecord::Base
serialize :data
def name
data['name'] if data
end
end
However, that approach is going to lead to a lot of repeated code. Ruby's metaprogramming features can help you solve that problem.
If every profile contains the same data structure you can use define_method
[:name, :age, :location, :email].each do |method|
define_method method do
data[method] if data
end
end
If the profile contains unique information you can use method_missing to attempt to look into the hash.
def method_missing(method, *args, &block)
if data && data.has_key?(method)
data[method]
else
super
end
end
Is there a way to have a custom serialization for fields in rails, a method that runs when a field is saved and loaded to convert from/to a string which is what ultimately is saved on the database.
Specifically what I want to do is have a field of type symbol like gender, with possible values :male and :female storing "male" and "female" on the database. There are some workarounds, like:
def gender
read_attribute(:gender).try(:to_sym)
end
but that leaves obj.attributes unchanged, so it's a leaky abstraction.
You can do it in Rails 3.1. The object you want to serialize has to reply to load and dump methods.
Here is an example of serializing a string in Base64.
class User < ActiveRecord::Base
class Base64
def load(text)
return unless text
text.unpack('m').first
end
def dump(text)
[text].pack 'm'
end
end
serialize :bank_account_number, Base64.new
end
For more details see: http://archives.edgerails.info/articles/what-s-new-in-edge-rails/2011/03/09/custom-activerecord-attribute-serialization/index.html
def whachamacallit
read_attribute("whachamacallit").to_sym
end
def whachamacallit=(name)
write_attribute("whachamacallit", name.to_s)
end
store them as stings in the database, but extract them as symbols when you pull them out then convert back before you save.
would work with any number or combination of strings / symbols.
to limit it to only a select few
validates_inclusion_of :whachamacallit, :in => [ :male, :female, :unknown, :hidden ]
From http://blog.quov.is/2012/05/01/custom-activerecord-attribute-serializers/
class Recipe < ActiveRecord::Base
serialize :ingredients, IngredientsList
end
class IngredientsList < Array
def self.dump(ingredients)
ingredients ? ingredients.join("\n") : nil
end
def self.load(ingredients)
ingredients ? new(ingredients.split("\n")) : nil
end
end
you can define the models to_xml for a model and it will do that
http://api.rubyonrails.org/classes/ActiveRecord/Serialization.html
its possible to define Marshall.dump and put in that way i think, but its something to look into
You could use serialize method inside the model. Please reference to this link:
http://api.rubyonrails.org/classes/ActiveRecord/Base.html
(ps. search keyword "serialize" in that page ;D)
In short, you could do this:
class YourModel < ActiveRecord::Base
serialize :db_field
end
Rails would automatically serialize the field before saving to database, and deserialize it after fetched from the database.
well for just male/female you could just do a Boolean column like male and if it was false assume that meant female, add wrapper methods for it
def female?
return !self.male?
end
We just released a gem (AttributeHelpers) that does exactly this. Disclaimer: I am a maintainer for the gem.
It allows you to call attr_symbol :gender in your class definition and the serialization happens automagically.