Rails MTI with polymorphism - ruby-on-rails

Imagine the scenario:
I have a class with different types of students. All students have similar attributes, but each type of student has also unique atributes. So I used MTI to keep the common attributes in the table students and the individual ones in their respective table, and polimorphism to abstract the student type when handling them from the class perspective. I followed this tutorial: http://techspry.com/ruby_and_rails/multiple-table-inheritance-in-rails-3/.
From this, I got to these models:
class Clazz < ActiveRecord::Base
has_many :students
end
class Student < ActiveRecord::Base
belongs_to :stu, :polymorphic => true
belongs_to :clazz
end
class Student1 < ActiveRecord::Base
has_one :student, :as => :stu
end
class Student2 < ActiveRecord::Base
has_one :student, :as => :stu
end
My problem comes when I want to instantiate a specific student (indirectly associated to the class through student). I can't do it from the class, because it doesn't have a connection to the specific students and when I try to instantiate it directly, it says it doesn't recognize the ':class' field.
Student1.new(:clazz => #clazz, ... [other atributes]...)
unknown attribute: :class
Can anyone give me a hint on how to accomplish this? Tks

Basically what #Aaron is trying to ask is does this work:
class Student < ...
belongs_to :clazz
end
class Student1 < ...
has_one :student, :as => :stu
accepts_nested_attributes_for :stu
end
Student1.new(:stu => {:clazz => #clazz},...[other attributes])
ActiveRecord doesn't do you any favors by default when you need to initialize across trees of objects like this.

Check out the solution here:
http://mediumexposure.com/multiple-table-inheritance-active-record/
which is similar to
http://techspry.com/ruby_and_rails/multiple-table-inheritance-in-rails-3/.
but from my experience, the former is better. for one, it implements method_missing,
which the latter doesn't do.

Related

Passing a child to container of parent type

I have a parent class Individual and child classes Student and Professor in my rails application.
Inheritance is handled with a gem called 'acts_as_relation' which simulates multiple table inheritance.
In addition, I have an action within which a student instance is appended to a list of individuals. Normally I would have expected this to go through without any problems but I get this error:
ActiveRecord::AssociationTypeMismatch: Individual(#70220161296060) expected, got Student(#70220161349360)
Here is a glance at my model:
class Individual < ActiveRecord::Base
acts_as_superclass
end
class Student < ActiveRecord::Base
acts_as :individual
end
class Professor < ActiveRecord::Base
acts_as :individual
end
I've not used this gem, but to give you some help, here's what I've found and this:
They both mention that you're calling an object through your relation, which will have confusion over polymorphism or similar. The two posts could not fix the issue, and I presume that is because they could find the correct object for their relationship
Looking at this further, I found this tutorial on the gem homepage:
acts_as_relation uses a polymorphic has_one association to simulate
multiple-table inheritance. For the e-commerce example you would
declare the product as a supermodel and all types of it as acts_as
:product (if you prefer you can use their aliases is_a and
is_a_superclass)
class Product < ActiveRecord::Base
acts_as_superclass
end
class Pen < ActiveRecord::Base
acts_as :product
end
class Book < ActiveRecord::Base
acts_as :product
end
To make this work, you need to declare both a foreign key column and a
type column in the model that declares superclass. To do this you can
set :as_relation_superclass option to true on products create_table
(or pass it name of the association):
create_table :products, :as_relation_superclass => true do |t|
# ...
end
Or declare them as you do on a polymorphic belongs_to association, it
this case you must pass name to acts_as in :as option:
change_table :products do |t|
t.integer :producible_id
t.string :producible_type
end
class Pen < ActiveRecord::Base
acts_as :product, :as => :producible
end
class Book < ActiveRecord::Base
acts_as :product, :as => :producible
end
Are you sure you've got your datatables set up correctly?
The way I've solve this in my projects, is using instance_ofsome_class.individuals << student_instance.individual.
The thing here is that is not a real MTI, so your collection of individuals would accept only individuals instances. If you call some_student_instance.individual or some_professor_instance.individual, you'll get an individual instance which is related with your specific instance.
Then working with that collection, if you want a Student or Professor all you need to do is call individual_in_collection.specific. For example:
p = Professor.create
a_model.individuals << p.individual
puts "#{a_model.individuals.first.class.name}"
=> Individual
puts "#{a_model.individuals.first.specific.class.name}"
=> Professor

How to create an association that sets join table attributes automatically?

I am totally confused about how I should go about "the rails way" of effectively using my associations.
Here is an example model configuration from a Rails 4 app:
class Film < ActiveRecord::Base
# A movie, documentary, animated short, etc
has_many :roleships
has_many :participants, :through => :roleships
has_many :roles, :through => :roleships
# has_many :writers........ ?
end
class Participant < ActiveRecord::Base
# A human involved in making a movie
has_many :roleships
end
class Role < ActiveRecord::Base
# A person's role in a film. i.e. "Writer", "Actor", "Extra" etc
has_many :roleships
end
class Roleship < ActiveRecord::Base
# The join for connecting different people
# to the different roles they have had in
# different films
belongs_to :participant
belongs_to :film
belongs_to :role
end
Given the above model configuration, the code I wish I had would allow me to add writers directly to a film and in the end have the join setup correctly.
So for example, I'd love to be able to do something like this:
## The Code I WISH I Had
Film.create!(name: "Some film", writers: [Participant.first])
I'm not sure if I'm going about thinking about this totally wrong but it seems impossible. What is the right way to accomplish this? Nested resources? A custom setter + scope? Something else? Virtual attributes? thank you!
I created a sample app based on your question.
https://github.com/szines/hodor_filmdb
I think useful to setup in Participant and in Role model a through association as well, but without this will work. It depends how would you like to use later this database. Without through this query wouldn't work: Participant.find(1).films
class Participant < ActiveRecord::Base
has_many :roleships
has_many :films, through: :roleships
end
class Role < ActiveRecord::Base
has_many :roleships
has_many :films, through: :roleships
end
Don't forget to give permit for extra fields (strong_parameters) in your films_controller.rb
def film_params
params.require(:film).permit(:title, :participant_ids, :role_ids)
end
What is strange, that if you create a new film with a participant and a role, two records will be created in the join table.
Update:
You can create a kind of virtual attribute in your model. For example:
def writers=(participant)
#writer_role = Role.find(1)
self.roles << #writer_role
self.participants << participant
end
and you can use: Film.create(title: 'The Movie', writers: [Participant.first])
If you had a normal has_and_belongs_to_many relationship i.e. beween a film and a participant, then you can create a film together with your examples.
As your joining model is more complex, you have to build the roleships separately:
writer= Roleship.create(
participant: Participant.find_by_name('Spielberg'),
role: Role.find_by_name('Director')
)
main_actor= Roleship.create(
participant: Participant.find_by_name('Willis'),
role: Role.find_by_name('Actor')
)
Film.create!(name: "Some film", roleships: [writer, main_actor])
for that, all attributes you use to build roleships and films must be mass assignable, so in a Rails 3.2 you would have to write:
class Roleship < ActiveRecord::Base
attr_accessible :participant, :role
...
end
class Film < ActiveRecord::Base
attr_accessible :name, :roleships
...
end
If you want to user roleship_ids, you have to write
class Film < ActiveRecord::Base
attr_accessible :name, :roleship_ids
...
end
Addendum:
Of cause you could write a setter method
class Film ...
def writers=(part_ids)
writer_role=Role.find_by_name('Writer')
# skiped code to delete existing writers
part_ids.each do |part_id|
self.roleships << Roleship.new(role: writer_role, participant_id: part_id)
end
end
end
but that makes your code depending on the data in your DB (contents of table roles) which is a bad idea.

Can a nested set have duplicate child objects or multiple parent_id/root/nodes?

Can a nested set have duplicate child objects or multiple parent_id/root/nodes?
For instance, I want to create an application that can manage parts and equipment. However, a specific equipment can have the same parts from other equipment as well.
Any thoughts on the best approach for this?
Thank you!!!
I think what you need here is an association class to help model the many-to-many relationship. In rails, this might look something like this:
class Equipment < ActiveRecord::Base
has_many :part_relationships
has_many :parts, :through => :part_relationships
end
class Part < ActiveRecord::Base
has_many :part_relationships
has_many :equipment, :through => :part_relationships
end
class PartRelationship < ActiveRecord::Base
belongs_to :equipment
belongs_to :part
end
There are other ways of modelling this (e.g. using a tree type structure), but if a 'set' is what you want, then this is the way I'd go.
Once this is done, you can do things like:
e = Equipment.find(:first)
e.parts # Returns all the parts on this equipment, including shared
p = Part.find(:first)
p.equipment # Returns all equipment this part features in.
# Create a new relationship between e and p
PartRelationship.create(:equipment => e, :part => p)

Rails query using HMTH and multiple join models

I am using Rails 3 and wanted to get the classes a student has access to based upon the model below
class Student
has_many :students_levels
has_many :levels, :through => :students_levels
end
class Class
has_many :classes_levels
has_many :levels, :through => :classes_levels
end
class Level
has_many :students_levels
has_many :classes_levels
end
class StudentsLevel
belongs_to :students
belongs_to :levels
end
class ClassesLevel
belongs_to :classes
belongs_to :levels
end
I came up with the query below but didn't think it seemed like the best Rails way to do things and wanted to get additional suggestions. Thx
Class.where(:id => (ClassesLevel.where(:level_id => Student.find(1).levels)))
I want to add this as an instance method to Student and was thinking there would be a better way doing something with has many through.
I quite did not understand the whole logic behind your class structure. Why you are not connecting students directly into a class? And how a class can have many levels. I mean if you have Math1 and Math2, those are different classes, right? Or do you have Math1,2,3?
Well, anyway, here's the solution if you want to use current assosiations, I hope it suites your needs:
Class Student
...
def available_classes
Class.find(:all,
:include => {:levels => {:students_levels => :student}},
:conditions => ["students.id = ?", self.id])
end
And sorry, this is still in Rails 2.x format...

How do I model this multi-inheritance relationship w/ Ruby ActiveRecord?

Assuming I have 5 tables. Can ActiveRecord handle this? How would you set it up?
The hierarchy:
Account (Abstract)
CorporateCustomer (Abstract)
PrivateCustomer
PublicCustomer
GovernmentCustomer
Edit: In nhibernate and castle activerecord the method needed to enable this scenario is called "joined-subclasses".
You could try something along the following lines.
class Account < ActiveRecord::Base
belongs_to :corp_or_gov_customer, :polymorphic => true
def account_id
self.id
end
end
class GovernmentCustomer < ActiveRecord::Base
has_one :account, :as => :corp_or_gov_customer, :dependent => :destroy
def method_missing( symbol, *args )
self.account.send( symbol, *args )
end
end
class CorporateCustomer < ActiveRecord::Base
has_one :account, :as => :corp_or_gov_customer, :dependent => :destroy
belongs_to :priv_or_pub_customer, :polymorphic => true
def method_missing( symbol, *args )
self.account.send( symbol, *args )
end
end
class PrivateCustomer < ActiveRecord::Base
has_one :corporate_customer, :as => :priv_or_pub_customer, :dependent => :destroy
def method_missing( symbol, *args )
self.corporate_customer.send( symbol, *args )
end
end
class PublicCustomer < ActiveRecord::Base
has_one :corporate_customer, :as => :priv_or_pub_customer, :dependent => :destroy
def method_missing( symbol, *args )
self.corporate_customer.send( symbol, *args )
end
end
I've not tested this code (or even checked it for syntax). Rather it's intended just to point you in the direction of polymorphic relations.
Overriding method_missing to call nested objects saves writing code like
my_public_customer.corporate_customer.account.some_attribute
instead you can just write
my_public_customer.some_attribute
In response to the comment:
The problem is that concepts like "is a", "has many" and "belongs to" are all implemented by foreign key relationships in the relational model. The concept of inheritance is completely alien to RDB systems. The semantics of those relationships has to be mapped onto the relational model by your chosen ORM technology.
But Rails' ActiveRecord library doesn't implement "is_a" as a relationship between models.
There are several ways to model your class hierarchy in an RDB.
A single table for all accounts but with redundant attributes - this is supported by ActiveRecord simply by adding a "type" column to your table. and then creating your class hierarchy like this:
class Account < ActiveRecord::Base
class GovernmentCustomer < Account
class CorporateCustomer < Account
class PublicCustomer < CorporateCustomer
class PrivateCustomer < CorporateCustomer
Then if you call PrivateCustomer.new the type field will automatically be set to "PrivateCustomer" and when you call Account.find the returned objects will be of the correct class.
This is the approach I would recommend because it's by far the simplest way to do what you want.
One table for each concrete class - As far as I know there is no mapping provided for this in ActiveRecord. The main problem with this method is that to get a list of all accounts you have to join three tables. What is needed is some kind of master index, which leads to the next model.
One table for each class - You can think of tables that represent the abstract classes as a kind of uniform index, or catalogue of objects that are stored in the tables for the concrete classes. By thinking about it this way you are changing the is_a relationship to a has_a relationship e.g. the object has_a index_entry and the index_entry belongs_to the object. This can be mapped by ActiveRecord using polymorphic relationships.
There is a very good discussion of this problem in the book "Agile Web Development with Rails" (starting on page 341 in the 2nd edition)
Search for ActiveRecord Single Table Inheritance feature.
Unfortunately I can't find a more detailed reference online to link to. The most detailed explanation I read was from "The Rails Way" book.
This is one way (the simplest) of doing it:
class Account < ActiveRecord::Base
self.abstract_class = true
has_many :corporate_customers
has_many :government_customers
end
class CorporateCustomer < ActiveRecord::Base
self.abstract_class = true
belongs_to :account
has_many :private_customers
has_many :public_customers
end
class PrivateCustomer < ActiveRecord::Base
belongs_to :corporate_customer
end
class PublicCustomer < ActiveRecord::Base
belongs_to :corporate_customer
end
class GovernmentCustomer < ActiveRecord::Base
belongs_to :account
end
NOTE: Abstract models are the models which cannot have objects ( cannot be instantiated ) and hence they don’t have associated table as well. If you want to have tables, then I fail to understand why it needs to an abstract class.
Assuming most of the data is shared, you only need one table: accounts. This will just work, assuming accounts has a string type column.
class Account < ActiveRecord::Base
self.abstract_class = true
end
class CorporateCustomer < Account
self.abstract_class = true
has_many financial_statements
end
class PrivateCustomer < CorporateCustomer
end
class PublicCustomer < CorporateCustomer
end
class GovernmentCustomer < Account
end
Google for Rails STI, and in particular Rails STI abstract, to get some more useful info.

Resources