views:

63

answers:

3

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

+1  A: 

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.

mrjake2
The solution still seems a bit overcomplicated to me, but I realize that might be because of the lack of a more detailed description of the build process. As for the number of SQL queries: I am aware of this danger, but for now this looks like the most appropriate solution in my case. First of all, because the Property class contains a bunch more attributes than the ones that I mentioned. Second of all, I do intend to query on values. +1 for the effort you put in your answer!
Pascal Lindelauf
A: 

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
KandadaBoggu
How does this solve the problem of the property.value_type not being filled when the value needs to be set?
Pascal Lindelauf
+1  A: 

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 = {})".

Pascal Lindelauf
Nice one :) Wish I could upvote you more! Very handy tip.
nfm