rails model subclassing -> multi table inheritance or polymorphism - ruby-on-rails

Here is my setup, followed by an explanation of what I am trying to accomplish.
class Layer < ActiveRecord::Base
has_and_belongs_to_many :components
end
class Component < ActiveRecord::Base
has_and_belongs_to_many :layers
end
class ImageComponent < Component
# I want this table to inherit from the Component table
# I should be able to add image-specific fields to this table
end
class VideoComponent < Component
# I want this table to inherit from the Component table
# I should be able to add video-specific fields to this table
end
What I want to be able to do:
layer.components << ImageComponent.create
layer.components << VideoComponent.create
In practice, I realize that ImageComponent and VideoComponent will actually have to inherit from ActiveRecord::Base. Is there any way to nicely implement model subclassing in Rails?
Right now I have my Component model setup to be polymorphic such that ImageComponent and VideoComponent each has_one :component, as: :componentable. This adds a layer of annoyance and ugliness to my code though:
image_component = ImageComponent.create
component = Component.create
component.componentable = image_component
layer.components << component
I guess a simple way to explain this is that I want to implement a habtm relationship between Layers and Components. I have multiple types of Components (i.e. ImageComponent, VideoComponent) that each have the same base structure but different fields associated with them. Any suggestions on ways this can be accomplished? I feel that I am missing something because my code feels hackish.

The "official" way to achieve this in Rails is to use Single Table Inheritance. Support for STI is built into ActiveRecord: http://api.rubyonrails.org/classes/ActiveRecord/Base.html#class-ActiveRecord::Base-label-Single+table+inheritance
If you want to use Multi Table Inheritance you would have to implement it by yourself...

here the main issue is between the Component and its types and not Layer and Component. i had a similar problem. will explain the solution specific to ur problem.
Store the type(Image/Video) as resource for Component and have a controller for Component and not all the types()
let the model structure be as
Component < ActiveRecord::Base
accepts_nested_attributes_for :resource
belongs_to :resource, :polymorphic => true, :dependent => :destroy
def resource_attributes=(params = {})
self.resource = spec_type.constantize.new unless self.resource
self.resource.attributes = params.select{|k| self.resource.attribute_names.include?(k) || self.resource.class::ACCESSOR.include?(k.to_sym)}
end
#component will be either image or video and not both
Image < ActiveRecord::Base
has_one :component, as :resource
Video < ActiveRecord::Base
has_one :component, as :resource
and a single controller as ComponentsController for CRUD of Component. Since the Component accepts attributes for resource(ie image/video), u can save the component as well as the resource and add normal validations for each resource.
the basic view for adding a Component can be as
= form_for(#component, :url => components_path, :method => :post) do |f|
= fields of Component
= f.fields_for :resource, build_resource('image') do |image|
= fields of resource Image
= f.fields_for :resource, build_resource('video') do |video|
= fields of resource Video
the fields for Image/Video can be added using the helper method
module ComponentsHelper
def build_resource(klass)
klass = "{klass.capitalize}"
object = eval("#{klass}.new")
if #component.resource.class.name == klass
object = #component.resource
end
return object
end
end
since the Component can have only one related resource(image/video), u need to select the the resource type on the view(in my case it was a dropdown list) and depending upon the selected resource show its fields and hide/remove all other resources fields(if image is selected, remove video fields using javascript). When the form is submitted, the method from Component model filters out all the key-value pairs for the intended resource and creates the component and its related resource.
Also
1) keep the field names for each resource unique cause when the form is submitted, the hidden resource(unwanted resources) fields are submitted which overwrite the intended resource fields.
2) the above model structure gives problem for resource attr_accessor only(they are not accessible on rails console). it can be solved as
ACCESSOR = ['accessor1', 'accessor2'] #needed accessors
has_one :component, :as => :resource
attr_accessor *ACCESSOR
See how to implement jobpost functionality that has 3 fixed categoris
i hope this helps.

With STI, you are sharing the same table with several model classes, so if you want subclassed models to have unique fields (database columns), then they need to be represented in that common table. From the comments in your example, it appears that this is what you want.
There is a trick you can do, however, which involves having a string column in the table that each model can use to store custom serialized data. In order to do this, it has to be OK that these data elements aren't indexed, because you won't be able to easily search on them within SQL. Let's say you call this field aux. Put this in the parent model:
require 'ostruct'
serialize :aux, OpenStruct
Now let's say you want the fields called manager and experience in a subclassed model, but none of the other STI models need this field and you won't need to search based on these attributes. So you can do this in the subclassed model:
# gets the value
def manager
return self.aux.manager
end
# sets the value
def manager=(value)
self.aux.manager = value
end
# gets the value
def experience
return self.aux.experience
end
# sets the value
def experience=(value)
self.aux.experience = value
end
In this example, single table inheritance still works fine and you also get custom persistant attributes for subclassed models. This gives you the benefits of sharing code and database resources among several models, but also allows each model to have unique attributes.

Related

Defining attributes at runtime based on data from related object

I'm building an application where users are part of an Organisation. An organisation has many Lists, which in turn have many ListItems.
Now, I would like for admin users to be able to specify which attributes are available on list items, based on the organisation they belong to (or rather, on the organisation their list belongs to), without having to touch any code.
So far, when defining attributes that are not bound to a specific column in the database, I have used document_serializable, a nifty little gem (based on virtus) which serializes virtual attributes to a JSONB column in the db. I like this approach, because I get all of virtus' goodies (types, coercion, validations, etc.), and because data ends up sitting in a JSONB column, meaning it can be loaded quickly, indexed, and searched through with relative ease.
I would like to keep using this approach when adding these user-defined attributes on the fly. So I'd like to do something like:
class ListItem < ApplicationRecord
belongs_to :list
delegate :organisation, to: :list
organisation.list_attributes.each do |a, t|
attribute a, t
end
end
Where Organisation#list_attributes returns the user-defined hash of attribute names and their associated types, which, for example, might look like:
{
name: String,
age: Integer
}
As you might have guessed, this does not work, because organisation.list_attributes.each actually runs in the context of ListItem, which is an instance of Class, and Class doesn't have an #organisation method. I hope that's worded in a way that makes sense1.
I've tried using after_initialize, but at that point in the object's lifecycle, #attribute is owned by ActiveRecord::AttributeMethods::Read and not DocumentSerializable::ClassMethods, so it's an entirely different method and I can't figure out wether I can still access the one I need, and wether that would even work.
Another alternative would be to find the organisation in question in some explicit way, Organisation#find-style, but I honestly don't know where I should store the information necessary to do so.
So, my question: at the moment of instantiating (initializing or loading2) a record, is there a way I can retrieve a hash stored in a database column of one of its relations? Or am I trying to build this in a completely misguided way, and if so, how else should I go about it?
1 To clarify, if I were to use the hash directly like so:
class ListItem < ApplicationRecord
belongs_to :list
delegate :organisation, to: :list
{
name: String,
age: Integer
}.each do |a, t|
attribute a, t
end
end
it would work, my issue is solely with getting a record's relation at this earlier point in time.
2 My understanding is that Rails runs a model's code whenever a record of that type is created or loaded from the database, meaning the virtual attributes are defined anew every time this happens, which is why I'm asking how to do this in both cases.
at the moment of instantiating (initializing or loading) a record, is
there a way I can retrieve a hash stored in a database column of one
of its relations?
Yes. This is fairly trivial as long as your relations are setup correctly / simply. Lets say we have these three models:
class ListItem < ApplicationRecord
belongs_to :list
end
class List < ApplicationRecord
belongs_to :organisation
has_many :list_items
end
class Organisation < ApplicationRecord
has_many :lists
end
We can instantiate a ListItem and then retrieve data from anyone of its parents.
#list_item = ListItem.find(5) # assume that the proper inherited
foreign_keys exist for this and
its parent
#list = #list_item.list
#hash = #list.organisation.special_hash_of_org
And if we wanted to do this at every instance of a ListItem, we can use Active Record Callbacks like this:
class ListItem < ApplicationRecord
belongs_to :list
# this is called on ListItem.new and whenever we pull from our DB
after_initialize do |list_item|
puts "You have initialized a ListItem!"
list = list_item.list
hash = list.organisation.special_hash_of_org
end
end
But after_initialize feels like a strange usage for this kind of thing. Maybe a helper method would be a better option!

Converting Rails attributes to property-value pairs

I have a Media model that has a bunch of standard metadata attributes and is persisted in the database as normal. What I want to do now is to add some configurable metadata attributes to this model on top of the existing attributes. A list of these attributes will be defined in a config file and loaded in at runtime. They'll be stored in the database in a different table as a series of property-value pairs with an association to the main model.
So, my code currently is,
class Media < ActiveRecord::Base
has_many :custom_metadata
attr_accessible :title, :language, :copyright, :description
end
and
class CustomMetadata < ActiveRecord::Base
belongs_to :media
attr_accessible :name, :value
end
What I want to do is to be able to access and update the custom metadata attributes on the Media model in the same way as the standard metadata attributes. For example, if the custom metadata attributes are called publisher and contributor, then I want to access them in the Media model as #media.publisher and #media.contributor even though they will be in the association #media.custom_metadata where its values would be something like [{:name => 'publisher', :value => 'Fred'}, {:name => 'contributor', :value => 'Bill'}]
It seems to be that virtual attributes would be the best way of achieving this but all of the examples I can find of people using virtual attributes is where the names of the attributes are static and known rather than dynamic from a run-time configuration, so they can define methods such as publisher and publisher= which would then contain code to write to the relevant associated property-value record.
I can define attributes on the class with attr_accessor *Settings.custom_metadata_fields (assuming Settings.custom_metadata_fields returns [:publisher, :contributor]) and also allow mass-assignment using a similar technique with attr_accessible.
The part I get stuck on is how to populate the virtual attributes from the association when loading the data from the record and then, in reverse, how to pass the data in the virtual attributes back into the association before the record is saved.
The two ways I currently see this working are either using method_missing or attribute_missing, or perhaps via initialize and a before_save callback? In either case, I'm not sure how I would define it given that my model has a mix of normal attributes and virtual attributes.
Any suggestions?
Using callbacks sounds reasonable.
What database are you using? If PostgreSQL, maybe you should take a look at HStore extension (http://www.postgresql.org/docs/9.2/static/hstore.html)
it will perform better, and there are some gems making it easy to use.
After looking into the callbacks some more I discovered the after_initialize callback and this is much better than using the initialize method as I'd first planned.
In the end, this was the final code for the Media model and I didn't change anything in the CustomMetadata model from what I defined in the question,
class Media < ActiveRecord::Base
has_many :custom_metadata
attr_accessor *Settings.custom_metadata_fields
attr_accessible :title, :language, :copyright, :description
attr_accessible *Settings.custom_metadata_fields
validates_presence_of *Settings.required_custom_fields
before_save :save_custom_metadata
after_initialize :load_custom_metadata
def load_custom_metadata
MediaMetadata.custom_all_fields.each do |field|
custom_record = custom_metadata.where(:name => field.to_s).first_or_initialize()
send("#{field}=", custom_record.value)
end
end
def save_custom_metadata
MediaMetadata.custom_all_fields.each do |field|
custom_record = custom_metadata.where(:name => field.to_s).first_or_initialize()
custom_record.value = send(field)
if custom_record.value.blank?
custom_record.destroy
else
custom_record.save
end
end
end
end
This solution had a couple of nice benefits. Firstly, it doesn't affect any of the normal attributes on the Media model. Secondly, only custom metadata with actual values are stored in the custom metadata table. If the value is blank, the record is removed completely. Finally, I can use standard validations on the model attributes as shown for my required custom metadata attributes.

At what level in a model object does ActiveRecord not load associated objects

I have a couple of models that are composites of multiple objects. I basically manage them manually for saves and updates. However, when I select items out, I don't have access to the associated properties of said item. For example:
class ObjectConnection < ActiveRecord::Base
def self.get_three_by_location_id location_id
l=ObjectConnection.find_all_by_location_id(location_id).first(3)
r=[]
l.each_with_index do |value, key|
value[:engine_item]=Item.find(value.engine_id)
value[:chassis_item]=Item.find(value.chassis_id)
r << value
end
return r
end
end
and each item:
class Item < ActiveRecord::Base
has_many :assets, :as => :assetable, :dependent => :destroy
When I use the ObjectLocation.find_three_by_location_id, I don't have access to assets whereas if use Item.find(id) in most other situations, I do.
I tried using includes but that didn't seem to do it.
thx
Sounds like the simplest solution would be to add methods to your ObjectConnection model for easy access like so:
class ObjectConnection < ActiveRecord::Base
def engine
Engine.find(engine_id)
end
def chassis
Chassis.find(chassis_id)
end
# rest of class omitted...
I'm not exactly sure what you're asking... If this doesn't answer what you're asking, then can you try to be a little bit more clear with what exactly you are trying to accomplish? Are the Chassis and Engine mdoels supposed to be polymorphic associations with your Item model?
Also, the code you're using above won't work due to the fact that you are trying to dynamically set properties on a model. It's not your calls to Item.find that are failing, it's your calls to value[:engine_item]= and value[:chassis_item] that are failing. You would need to modify it to be something like this if you wanted to keep that flow:
def self.get_three_by_location_id location_id
l=ObjectConnection.find_all_by_location_id(location_id).first(3)
r=[]
l.each_with_index do |obj_conn, key|
# at this point, obj_conn is an ActiveRecord object class, you can't dynamically set attributes on it at this point
value = obj_conn.attributes # returns the attributes of the ObjectConnection as a hash where you can then add additional key/value pairs like on the next 2 lines
value[:engine_item]=Item.find(value.engine_id)
value[:chassis_item]=Item.find(value.chassis_id)
r << value
end
r
end
But I still think that this whole method seems unnecessary due to the fact that if you setup proper associations on your ObjectConnection model to begin with, then you don't need to go and try to handle the associations manually like you're attempting to do here.

Using one form to create two models with overlapping attributes in Rails

This post seems good for how to create two models with one form. But how would you do it if the two models share one or more of the attributes?
That post seems fairly outdated, I would recommend using accepts_nested_attributes_for and fields_for in your form instead. That said, overlapping attributes should probably be set in your model's callbacks. Say you want a project's name to be automatically set to first task's name.
class Project < ActiveRecord::Base
has_many :tasks
accepts_nested_attributes_for :tasks
before_validation :set_name_from_task
private
def set_name_from_task
self.name = tasks.first.name
end
end
If your 2 models are completely unrelated, you can assign certain params to them directly in the controller.
def create
#foo = Foo.new(params[:foo])
#bar = Bar.new(params[:bar])
#bar.common_attr = params[:foo][:common_attr]
# validation/saving logic
end
Although this is not a great practice, this logic should ideally be moved into models.

Challenging Rails Question with Form Based on Existing Resources

You have a set of related models created through a scaffold e.g. a house, which has many rooms, which each have many windows, which each has a selection of locks.
These resources are already full of data i.e. someone has entered all the information, such as: a room called 'kitchen' has various windows associated with it and these windows each have five different locks associated with them.
Someone comes along and says:
Can you create a form that lets someone create a new project where they can select the different rooms, windows and then specify the locks that they would like for that project? (these are already in the system, nothing new to add, just the associations to a new project)
This sounds like a nested form but I have wasted a lot of time trying to solve this - there are many levels of nesting, which make this tricky. Any suggestions?
session based solution
With such deeply nested models select box on the front end wouldn't be enough...
Assuming this, you may want to create a current_house who's id live in a session (just like current_user works).
Once you have your current_house add different items by navigating to your list of items view and clicking on the add_to link :
# house_controller.rb
def add_to
current_house.polymorphic_items << Kitchen.find(params[:id])
redirect_to :back
end
But there are many approaches to this session based solution which sort of implements a cart/order system. You may want to add a current_item to add stuff in each leaf of your tree aka room of your house.
E.G after clicking on the kitchen you just added :
before_filter :set_current_item
def add_to
current_item.windows << Window.find(id)
end
current_item beeing polymorphic : a living room, a bathroom etc.
But how you implement that precisely depends on your Domain Model....
As a rule of thumb regarding nested forms I'd follow rails guidance for routes : don't go deeper than one level or you'll end up in a mess.
Yes this is a nested form. Railscasts nested forms is a great place to start.
If everything is already in the system you probably just want select boxes so they can select what they want. Also check out the .build method. If you have multiple levels of nesting you can also manually set the association by passing in the foreign key yourself.
I think you can model this with a single level of nested attributes, given the models below (based on Windows/Locks pre-existing and a room just needing to mix and match them into a set of windows with given locks):
class House < ActiveRecord::Base
has_many :rooms
end
class Room < ActiveRecord::Base
belongs_to :house
has_many :window_configs
end
class WindowConfig < ActiveRecord::Base
belongs_to :room
belongs_to :window
belongs_to :lock
end
class Lock < ActiveRecord::Base
has_many :window_configs
end
class Window < ActiveRecord::Base
has_many :window_configs
end
... based on that model setup, you could have a single house form that you dynamically add child 'room' definitions to that each have a name and a collection of window_configs which have two select boxes for each one (choose a window definition and then a lock definition). Because you're dynamically adding multiple rooms with multiple windows, you'd need some JS to populate new form elements, but it could all live in a single nested form.
form_for :house do |form|
# Dynamically add a Room form for each room you want with js
form.fields_for :room do |room_attributes|
room_attributes.text_field :name
# Dynamically add window_config forms on Room w/ JS
room_attributes.fields_for :window_config do |window_attributes|
window_attributes.select :window_id, Window.all
window_attributes.select :lock_id, Lock.all

Resources