EDIT I have edited this from the original to make it easier to understand.
I understand the Object Relationship Impedance problem. I understand Rails STI and Polymorphism (the Rails way and that it's not true OO Polymorphism). I've read a TON of blogs and questions on this, and still cannot find the answer to this problem.
class Person < ApplicationRecord (ie what was ActiveRecord::Base)
end
class Employee < Person
end
class Customer < Person
end
... multiple other types of Person
Now lets say the 'client' asks to extend the system, and create some new stuff. Let's call it a Project, to which we can assign Employees:
Ok so let's create a Many-to-Many using Third Normal Form:
class Project < ApplicationRecord
has_many :assignments
has_many :employees, through: :assignments
end
class Employee < Person
has_many :assignments
has_many :projects, through: :assignments
end
class Assignment < ApplicationRecord
belongs_to :employee
belongs_to :project
end
This won't work. The migration will fail, since there is no table called Employee to create the foreign-key constraints on. STI means that the 'base class' is the People table.
TWO QUESTIONS:
1 How do you solve this? (for this you may also want to chip in here)
2 How do you create serialised data properly for Projects (which should include employees, but not People or other sub-types of Person)?
ProjectSerializer < ActiveModelSerializers
has_many :employees
has_many :employees, through: :assignments
end
won't work, so you would have to serialise People.
UPDATE
In the migration I created Project and Assignment tables (Person already exists).
So now the database has these tables:
Project
Person
Assignment
Assignment has two foreign keys (referencing Person, since that is the table that exists, not Employee):
person_id
project_id
Every time I try to create an assignment, this error is thrown, which I expected of course:
ActiveModel::UnknownAttributeError (unknown attribute 'employee_id' for Assignment.)
The solution according to Rails documentation (section 4.1.2.5) and every other answer I can find anywhere for this situation is to tell Rails what the foreign_key is. Like this:
class Assignment < ApplicationRecord
belongs_to :employee, foreign_key: "person_id"
belongs_to :project
end
But every example I find (even in the documentation) all assume that there is no inheritance - all the models are inheriting from ActiveRecord:Base (or ApplicationRecord in Rails 5).
Even though I am explicitly telling Rails that the assignment table has a foreign_key called 'person_id' that holds the id for the employee, it still cannot find it.
And finally I tried this (thanks to the answer from @camonz)
class Assignment < ApplicationRecord
belongs_to :person, foreign_key: "person_id", foreign_type: "employee"
belongs_to :project
end
Same error.
Is this really a model setup that Rails cannot handle?
Here is the Rails log:
I, [2016-09-22T22:54:55.088466 #12182] INFO -- : Started POST "/assignments" for ::1 at 2016-09-22 22:54:55 +0200
I, [2016-09-22T22:54:55.095768 #12182] INFO -- : Processing by AssignmentsController#create as JSON
I, [2016-09-22T22:54:55.096007 #12182] INFO -- : Parameters: {"data"=>{"attributes"=>{"status"=>"pending"}, "relationships"=>{"project"=>{"data"=>{"type"=>"projects", "id"=>"601"}}, "employee"=>{"data"=>{"type"=>"employees", "id"=>"143"}}}, "type"=>"assignments"}, "assignment"=>{}}
I, [2016-09-22T22:54:55.098032 #12182] INFO -- : {:status=>"pending", :project_id=>"601", :employee_id=>"143"}
I, [2016-09-22T22:54:55.117411 #12182] INFO -- : Completed 500 Internal Server Error in 21ms (ActiveRecord: 8.8ms)
F, [2016-09-22T22:54:55.119116 #12182] FATAL -- :
F, [2016-09-22T22:54:55.119246 #12182] FATAL -- : ActiveModel::UnknownAttributeError (unknown attribute 'employee_id' for Assignment.):
F, [2016-09-22T22:54:55.119283 #12182] FATAL -- :
F, [2016-09-22T22:54:55.119313 #12182] FATAL -- : app/controllers/assignments_controller.rb:18:in `create'
On the migration, drop the FK constraints on the addressses
table. On the child classes redefine the has_many
relation and specify :foreign_key
& :foreign_type
.
On your Assignment serializer, specify the belongs_to :employee
, AMS should handle it correctly.
Also take a look at the :source
and :source_type
options of the has_many
association.
I found a solution to my first Question. Leaving the models setup as they are in the question, I change the database to see if it would work.
I changed the Assignment table so that the foreign_key is now called 'employee_id' since Rails seemed to want to insist on that.
Then I changed the constraint, which now reads:
ALTER TABLE public.assignments
ADD CONSTRAINT fk_rails_52f37556f9 FOREIGN KEY (employee_id)
REFERENCES public.people (id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION;
Then the code works - but of course I would now have to write a migration script to create such a constraint.
The original migration does not work, as it creates 'person_id':
t.references :person, foreign_key: true
This answer made it clear that Rails does not properly support foreign keys. SIGH
The other problem with this solution is that if other subtypes of Person
at some point in the future need to also be assigned to Projects
, they can't be. So not a great solution really. Better to leave assignments related directly to Person.
I'll answer your first question as it to me is the main question. To me there is a clue when its asking for an employees table on migration.
This won't work. The migration will fail, since there is no table called Employee to create the foreign-key constraints on. STI means that the 'base class' is the People table.
This would indicate to me your schema on the people table is missing the "type" column name, which will store the class name of the Object and also trigger the STI behavior of Rails.
STI means that the 'base class' is the People table.
Sort of but your error indicates that all that is happening is normal ruby inheritance. So you essentially have the I in STI but not the ST part.
The point of STI isn't just inheritance of a parent class, this is normal ruby Inheritance, the point is multiple classes using a single table in the database because they're essentially the same schema. However it requires the seemingly invisible "type" column name to trigger the Rails convention of STI in the database. Without this its normal ruby inheritance.
Note: Having gone down this road in the past I'll warn you that this way lies madness. Especially when the table is self referential as you're looking to do here with your mentioning of foreign keys, i.e. foreign keys to the same table.
Be careful when you get to step #2 because you're setting up an infinite reference possibility (race condition) or at the least Big O lookups if you start calling the associations too eagerly. For example, JSON may build out the associations on employees for each Person. So it will have to call the same table "people" multiple times to build out each employee's employees, i.e. it becomes employees all the way down. This will be slow.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With