Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails 5 - Object Relation Impedence and how to structure multiple inherited classes/tables

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'
like image 329
rmcsharry Avatar asked Sep 16 '16 05:09

rmcsharry


3 Answers

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

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

like image 137
Simon Avatar answered Oct 13 '22 10:10

Simon


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.

like image 21
rmcsharry Avatar answered Oct 13 '22 11:10

rmcsharry


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.

like image 27
engineerDave Avatar answered Oct 13 '22 12:10

engineerDave