I have the following classes in my ActiveRecord model:
def Property < ActiveRecord::Base
# attribute: value_type (can hold values like :integer, :string)
end
def PropertyValue < ActiveRecord::Base
belongs_to property
# attribute: string_value
# attribute: integer_value
end
A PropertyValue object is intended to hold only a string value or an integer value, depending on the type, specified in the value_type attribute of the associated Property object. Obviously, we shouldn't bother the user of the PropertyValue class with this underlying string_value/integer_value mechanism. So I'd like to use a virtual attribute "value" on PropertyValue, that does something like this:
def value
unless property.nil? || property.value_type.nil?
read_attribute((property.value_type.to_s + "_value").to_sym)
end
end
def value=(v)
unless property.nil? || property.value_type.nil?
write_attribute((property.value_type.to_s + "_value").to_sym, v)
end
end
I want to offer the user a view to fill in a bunch of property values, and when the view is posted, I'd like to have PropertyValue objects instantiated based on the list of attributes that is passed in from the view. I'm used to using the build(attributes) operation for this. However, the problem now occurs that I don't have any control over the order in which the attribute initialization takes place. Thus the assignment of the value attribute will not work when the association with the Property attribute has not yet been made, since the value_type cannot be determined. What is the correct "Rails" way to deal with this?
BTW, as a workaround I have tried the following:
def value=(v)
if property.nil? || property.value_type.nil?
#temp_value = v
else
write_attribute((property.value_type.to_s + "_value").to_sym, v)
end
end
def after_initialize
value = #temp_value
end
Apart from the fact that I think this is quite an ugly solution, it doesn't actually work with the "build" operation. The #temp_value gets set in the "value=(v)" operation. Also, the "after_initialize" in executed. But, the "value = #temp_value" does not call the "value=(v)" operation strangely enough! So I'm really stuck.
EDIT: build code
I indeed realized that the code to build the Property objects would be handy. I'm doing that from a Product class, that has a has_many association with Property. The code then looks like this:
def property_value_attributes=(property_value_attributes)
property_value_attributes.each do |attributes|
product_property_values.build(attributes)
end
end
At the same time I figured out what I did wrong in the after_initialize operation; it should read:
def after_initialize
#value = #temp_value
end
The other problem is that the property association on the newly built property_value object will never be set until the actual save() takes place, which is after the "after_initialize". I got this to work by adding the value_type of the respective property object to the view and then having it passed in through the attributes set upon post. That way I don't have to instantiate a Property object just to fetch the value_type. Drawback: I need a redundant "value_type" accessor on the PropertyValue class.
So it works, but I'm still very interested in if there's a cleaner way to do this. One other way is to make sure the property object is attached first to the new PropertyValue before initializing it with the other attributes, but then the mechanism is leaked into the "client object", which not too clean either.
I would expect some sort of way to override the initializer functionality in such a way that I could affect the order in which attributes get assigned. Something very common in languages like C# or Java. But in Rails...?
One option is to save the Property objects first, and then add the PropertyValue objects afterwards. If you need to you could wrap the whole thing in a transaction to ensure that the Properties are rolled back if their corresponding PropertyValues could not be saved.
I don't know what your collected data from the form looks like, but assuming it looks like the following:
#to_create = { :integer => 3, :string => "hello", :string => "world" }
You could do something like this:
Property.transaction do
#to_create.keys.each do |key|
p = Properties.create( :value_type => key.to_s )
p.save
pval = p.property_value.build( :value => #to_create[key] )
pval.save
end
end
That way you don't have to worry about the nil check for Property or Property.value_type.
As a side note, are you sure you need to be doing all this in the first place? Most database designs I've seen that have this kind of really generic meta-information end up being highly non-scalable and are almost always the wrong solution to the problem. It will require a lot of joins to get a relatively simple set of information.
Suppose you have a parent class Foo that holds the property/value pairs. If Foo has ten properties, that requires 20 joins. That's a lot of DB overhead.
Unless you actually need to run SQL queries against PropertyValues (e.g. "get all Foos that have the property "bar"), you could probably simplify this a lot by just adding an attribute called "properties" to Foo, then serializing your Properties hash and putting it in that field. This will simplify your code, your database design, and speed up your application as well.
Oh jeeezzzz... this is insanely simple, now that I puzzled on it a little more. I just need to override the "initialize(attributes = {})" method on the PropertyValue class like so:
def initialize(attributes = {})
property = Property.find(attributes[:property_id]) unless attributes[:property_id].blank?
super(attributes)
end
Now I'm always sure that the property association is filled before the other attributes are set. I just didn't realize soon enough that Rails' "build(attributes = {})" and "create(attributes = {})" operations eventually boil down to "new(attributes = {})".
Probably you should try to use ActiveRecord get/set methods, i.e.:
def value
send("#{property.value_type}_value") unless property || property.value_type
end
def value=(v)
send("#{property.value_type}_value=", value) unless property || property.value_type
end
Related
Is there a better way to set values to setter methods when they are made dynamically using attr_accessor method? I need this for setting values for them from another model in rails. I'm trying to do something like below.
Model_class.all.each do |mdl|
attr_accessor(mdl.some_field)
end
Then I know that it creates a set of get and setter methods. What I want to do is, when these methods are get created, i want some value to be specified for setter method.Thanks in advance.
attr_accessor has no magic embedded. For each of params passed to it, it basically executes something like (the code is simplified and lacks necessary checks etc):
def attr_accessor(*vars)
vars.each do |var|
define_method var { instance_variable_get("##{var}") }
define_method "#{var}=" { |val| instance_variable_set("##{var}", val) }
end
end
That said, the attr_accessor :var1, :var2 DSL simply brings new 4 plain old good ruby methods. For what you are asking, one might take care about defining these methods (or some of them, or none,) themselves. For instance, for cumbersome setting with checks one might do:
attr_reader :variable # reader is reader, no magic
def variable=(val) do
raise ArgumentError, "You must be kidding" if val.nil?
#variable = val
end
The above is called as usual:
instance.variable = 42
#⇒ 42
instance.variable = nil
#⇒ ArgumentError: You must be kidding
Here is another possible implementation for this:
def attr_accessor(*args)
args.each do |attribute|
define_method(attribute.to_sym) { eval("##{attribute}") }
define_method((attribute.to_s + '=').to_sym) {|value| eval("##{attribute} = value") }
end
end
I use the readonly? function to mark my Invoice as immutable after they've been sent; for by InvoiceLines, I simply proxy the readonly? function to the Invoice.
A simplified example:
class Invoice < ActiveRecord::Base
has_many :invoice_lines
def readonly?; self.invoice_sent? end
end
def InvoiceLine < ActiveRecord::Base
def readonly?; self.invoice.readonly? end
end
This works great, except that in one specific scenario I want to update an InvoiceLine regardless of the readonly? attribute.
Is there are way to do this?
I tried using save(validate: false), but this has no effect. I looked at persistence.rb in the AR source, and that seems to just do:
def create_or_update
raise ReadOnlyRecord if readonly?
...
end
Is there an obvious way to avoid this?
A (somewhat dirty) workaround that I might do in Python:
original = line.readonly?
line.readonly? = lambda: false
line.save()
line.readonly? = original
But this doesn't work in Ruby, since functions aren't first-class objects ...
You can very easily redefine a method in an instantiated object, but the syntax is definition rather than assignment. E.g. when making changes to a schema that required a tweak to an otherwise read-only object, I have been known to use this form:
line = InvoiceLine.last
def line.readonly?; false; end
Et voila, status overridden! What's actually happening is a definition of the readonly? method in the object's eigenclass, not its class. This is really grubbing around inside the guts of the object, though; outside of a schema change it's a serious code smell.
One crude alternative is forcing Rails to write an updated column directly to the database:
line.update_columns(description: "Compliments cost nothing", amount: 0)
and it's mass-destruction equivalent:
InvoiceLine.where(description: "Free Stuff Tuesday").update_all(amount: 0)
but again, neither should appear in production code outside of migrations and, very occasionally, some carefully written framework code. These two bypass all validation and other logic and risk leaving objects in inconsistent/invalid states. It's better to convey the need and behaviour explicitly in your model code & interactions somehow. You could write this:
class InvoiceLine < ActiveRecord::Base
attr_accessor :force_writeable
def readonly?
invoice.readonly? unless force_writeable
end
end
because then client code can say
line.force_writable = true
line.update(description: "new narrative line")
I still don't really like it because it still allows external code to dictate an internal behaviour, and it leaves the object with a state change that other code might trip over. Here's a slightly safer and more rubyish variant:
class InvoiceLine < ActiveRecord::Base
def force_update(&block)
saved_force_update = #_force_update
#_force_update = true
result = yield
#_force_update = saved_force_update
result
end
def readonly?
invoice.readonly? unless #_force_update
end
end
Client code can then write:
line.force_update do
line.update(description: "new description")
end
Finally, and this is probably the most precision mechanism, you can allow just certain attributes to change. You could do that in a before_save callback and throw an exception, but I quite like using this validation that relies on the ActiveRecord dirty attributes module:
class InvoiceLine < ActiveRecord::Base
validate :readonly_policy
def readonly_policy
if invoice.readonly?
(changed - ["description", "amount"]).each do |attr|
errors.add(attr, "is a read-only attribute")
end
end
end
end
I like this a lot; it puts all the domain knowledge in the model, it uses supported and built-in mechanisms, doesn't require any monkey-patching or metaprogramming, doesn't avoid other validations, and gives you nice error messages that can propagate all the way back to the view.
I ran into a similar problem with a single readonly field and worked around it using update_all.
It needs to be an ActiveRecord::Relation so it would be something like this...
Invoice.where(id: id).update_all("field1 = 'value1', field2 = 'value2'")
Here is an answer, but I don't like it. I would recommend to think twice about the design: If you make this data immutable, and you do need to mutate it, then there may be a design issue. Let aside any headache if the ORM and the datastore "differ".
One way is to use the meta programming facilities. Say you want to change the item_num of invoice_line1 to 123, you can proceed with:
invoice_line1.instance_variable_set(:#item_num, 123)
Note that the above will not work directly with ActiveRecord models' attributes, so it would need be adapted. But well, I would really advice to reconsider the design rather than falling for black magic.
Here's an elegant solution how to disallow modification generally but allow it if it is specifically requested:
In your model, add the following two methods:
def readonly?
return false if #bypass_readonly
return true # Replace true by your criteria if necessary
end
def bypass_readonly
#bypass_readonly=true
yield
#bypass_readonly=false
end
Under normal circumstances, your object is still readonly, so no risk of accidentally writing to a readonly object:
mymodel.save! # This raises a readonly error
However in privileged places where you are sure that you want to ignore the readonlyness, you can use:
mymodel.bypass_readonly do
# Set fields as you like
mymodel.save!
end
Everything inside the bypass_readonly block is now allowed despite readonly. Have fun!
This overrides the #readonly? method for one particular only, not affecting anything else:
line.define_singleton_method(:readonly?) { false }
readonly_attrs = described_class.readonly_attributes.dup
described_class.readonly_attributes.clear
# restore readonly rails constraint
described_class.readonly_attributes.merge(readonly_attrs)
This worked for us with Rails 7.
I have a price attribute in my model.
Can I use attribute-getter, which is named just like the attribute
def price
... logic logic ..
return something
end
in order to override the attribute itself ?
Currently it doesn't work. If I call model.price it works, but when it somes to saving the object via model.save, it stores the default value.
Can it be done in a painless way, or should I make a before_save callback?
If you set a value in Ruby you access the setter method. If you want to override the setter you have to do something like this:
def price=(_price)
# do some logic
write_attribute(:price, _price)
end
This is of course a discussion point. Sometimes you can better use a callback. Something like this:
before_save :format_price
private
def format_price
# Do some logic, for example make it cents.
self.price = price * 100
end
Since you seem to want the "real" value stored in the database, what you probably want to do is modify the setter. This way the actual value is stored, and the price getter can just return it unmodified.
You can do this via the lower level write_attribute method. Something like:
def price=(value)
# logic logic
self.write_attribute(:price, value)
end
If you want to manipulate the attribute's value right before it's saved then using a callback would be a better way, since this is what callbacks are for.
Mostly in rails if you write my_obj.attr it looks up attr in the database and reports it back. How do you create a custom def attr method that internally queries the database for attr, modifies it and returns? In other words, what's the missing piece here:
# Within a model. Basic attr_reader method, will later modify the body.
def attr
get_from_database :attr # <-- how do I get the value of attr from the db?
end
Something like that:
def attr
value = read_attribute :attr
value = modify(value)
write_attribute :attr, value
save
value
end
Neutrino's method is good if you want to save a modified value back to the database each time you get the attribute. This not recommended since it will execute an extra database query every time you try to read the attribute even if it has not changed.
If you simply want to modify the attribute (such as capitalizing it for example) you can just do the following:
def attr
return read_attribute(:attr).capitalize #(or whatever method you wish to apply to the value)
end
I've got a model in which attributes are allowed to be null, but when a null attribute is read I'd like to take a special action. In particular, I'd like to throw a certain exception. That is, something like
class MyModel < ActiveRecord::Base
def anAttr
read_attribute(:anAttr) or raise MyException(:anAttr)
end
end
that's all fine, but it means I have to hand-code the identical custom accessor for each attribute.
I thought I could override read_attribute, but my overridden read_attribute is never called.
Not sure why you'd need to do this, but alas:
def get(attr)
val = self.send(attr)
raise MyException unless val
val
end
#object.get(:name)
That's funny, we were looking into this same thing today. Check into attribute_method.rb which is where all the Rails logic for the attributes exists. You'll see a define_attribute_methods method which you should be able to override.
In the end, I think we're going to do this in a different way, but it was a helpful exercise.