Database relationships for polymorphic

Currently polymorphic-associations use the _id and _type

      t.bigint  :imageable_id
      t.string  :imageable_type

However it would be nice, for database constraint reasons, and data integrity at db level that it could be possible to have:

      t.references  :product
      t.references  :employee

and then have

class Picture < ApplicationRecord
  belongs_to :imageable, polymorphic: [:product, :employee]
  belongs_to :employee, optional: true
  belongs_to :product, optional: true
end
1 Like

I think the major reason, off the top of my head, that would be an issue is that without further code or database constraints you could have a Picture belong_to both an Employee and a Product, where as as currently designed, it enforces only a single imageable record that it belongs_to.

To describe a key goal of polymorphism, the polymorphic model should not know all the places it is being utilized. Let’s take your example of imageable and see what it would look like if it were applied to many additional models. Perhaps Driver, VehicleCondition, CustomerLogo, RMADescription, and Avatar, Now your Picture model ends up with these eight belongs_to lines:

  belongs_to :imageable, polymorphic: [:product, :employee, :driver, :vehicle_condition, :customer_logo, :rma_description, and :avatar]
  belongs_to :employee, optional: true
  belongs_to :product, optional: true
  belongs_to :driver, optional: true
  belongs_to :vehicle_condition, optional: true
  belongs_to :customer_logo, optional: true
  belongs_to :rma_description, optional: true
  belongs_to :avatar, optional: true

Probably many database people would be happier about the better relational integrity that this would offer, and of course Rails will let you set things up in this non-polymorphic way where all you lose is the ability to ask any Picture about its imageable and get back a variety of different types of object. This isn’t the goal of Rails’ polymorphism, to end up with a bunch of specific foreign keys.

Instead Rails polymorphic associations allow you to not know all the places in which your Picture model will end up being used. After all, it’s just a picture, and you shouldn’t have to know – no need to make any special accommodations for any piece of code using that picture. Use it where you will. (And for any model that does uses it, the inverse association is established with just: has_many :pictures, as: :imageable, and it just works.)

Definitely polymorphic associations are a unique beast, and some people love them, and some hate them, to the point that some teams prohibit the use of them. But for those that like them then it can make good sense.

On the topic of database integrity, it’s possible to enforce it transparently to Rails if your RDBMS supports generated columns that can be foreign keys:

testuser=# create table people
  (
    id integer primary key
  );
CREATE TABLE
testuser=# create table organisations
  (
    id integer primary key
  );
CREATE TABLE
testuser=# create table addresses
  (
    id               integer primary key,
    addressable_id   integer not null,
    addressable_type varchar not null,
    person_id        integer generated always as (case addressable_type when 'Person' then addressable_id end) stored,
    organisation_id  integer generated always as (case addressable_type when 'Organisation' then addressable_id end) stored    
  );
CREATE TABLE
testuser=# alter table addresses add constraint addresses_people foreign key (person_id) references people;
ALTER TABLE
testuser=# alter table addresses add constraint addresses_organisation foreign key (organisation_id) references organisations;
ALTER TABLE
testuser=# insert into people values (1);
INSERT 0 1
testuser=# insert into organisations values (2);
INSERT 0 1
testuser=# insert into addresses values(1, 1, 'Person');
INSERT 0 1
testuser=# insert into addresses values(2, 2, 'Organisation');
INSERT 0 1
testuser=# insert into addresses values(3, 2, 'Person');
ERROR:  insert or update on table "addresses" violates foreign key constraint "addresses_people"
DETAIL:  Key (person_id)=(2) is not present in table "people".
testuser=# select * from addresses;
 id | addressable_id | addressable_type | person_id | organisation_id 
----+----------------+------------------+-----------+-----------------
  1 |              1 | Person           |         1 |                
  2 |              2 | Organisation     |           |               2
(2 rows)

testuser=# delete from organisations;
ERROR:  update or delete on table "organisations" violates foreign key constraint "addresses_organisation" on table "addresses"
DETAIL:  Key (id)=(2) is still referenced from table "addresses".
testuser=# 

1 Like