1

I have a Leader that inherits from Person

class Leader < Person
  self.table_name = 'person'
  
  end
end

Person has_many :email_addresses

But when I look up

Leader.first.email_addresses.to_sql

I get

SELECT `email_address`.* FROM `email_address` WHERE `email_address`.`entity_id` = 4 AND `email_address`.`entity_type` = 'Leader' ORDER BY `email_address`.`position` ASC

AR is using Leader as the type, but my all my email_addresses have Person as the type. Is there a way to define this manually so that the Leader class's "type" is always the base class?

Addendum:

I have a workable solution as follows, that I'm not in love with, and that is to delegate the assoc to the base class instance:

class Person < ApplicationRecord
  # Allows subclasses to access/delegate to the superclass instance
  # especially necessary for polymorphic assocs where the classname needs to stay Person
  protected

  def person_object
    Person.find(id)
  end
end


class Leader < Person
  delegate :email_addresses, to: :person_object
end
pixelearth
  • 13,674
  • 10
  • 62
  • 110
  • you don't need to specify the table name for `Leader` it is inherited from `Person`. You mention that "email_addresses have Person as the type". What does your Email model look like? Does it have STI inheritance? – Les Nightingill Aug 16 '21 at 17:05
  • @LesNightingill the email_addresses model is very simple, and has a basic ` belongs_to :entity, polymorphic: true`. This is because many entities have email_addresses, like companies, hotels, people. In those cases the entity_type needs to be `Company`, `Hotel`, etc. But my people are subclassed into things like Leader, Employee, User, etc, and I want those `entity_type` to always be Person. I've technically come with a workable solution, that I'll add above ad an edit. – pixelearth Aug 16 '21 at 18:00
  • in leader.rb try `has_many :email_addresses, {where: "entity_type='Person'}` – Les Nightingill Aug 16 '21 at 18:39
  • @LesNightingill this is an interesting approach. Does is work for creating objects as well? My approach above handles `Leader.last.email_addresses.new => # `. And ideally the solution would be mostly contained in Person, so that all subclasses just inherit it, and not have to implement something. Unfortunately the association definitition is actually in a separate module `HasContactPoints`. – pixelearth Aug 16 '21 at 18:56
  • So I think the only solution that would be better than what I have above is some method that AR inherently calls to get the `_type` string, and override that on Person. I'm not sure if that exists, though – pixelearth Aug 16 '21 at 18:56
  • @pixelearth you can use [`inheritance_column=`](https://api.rubyonrails.org/classes/ActiveRecord/ModelSchema/ClassMethods.html#method-i-inheritance_column-3D) to set the value of `entity_type` For reference you can check this [post](https://stackoverflow.com/questions/20294879/change-activerecordbase-inheritance-column-in-a-rails-app) – Chandan Aug 23 '21 at 13:15
  • 1
    @Chandan it looks like this defines the column that stores the type. So in the traditional setup my situation ends up being column `type` is filled with `Leader` when I want it to be set to `Person`. Using `inheritance_column` will allow me to set the column `type` to something like `kind` (not the value). So then I'd end up with `kind` is `Leader`, when I want `type` `Person`. – pixelearth Aug 23 '21 at 13:51
  • @pixelearth there is a method named [`sti_name`](https://api.rubyonrails.org/classes/ActiveRecord/Inheritance/ClassMethods.html#method-i-sti_name) which is used for the value – Chandan Aug 24 '21 at 04:16
  • the STI in `ActiveRecord::Inheritance` has changed from version 5 and 6 due to improvement and bug fixes. – Chandan Aug 24 '21 at 04:50

2 Answers2

0

in my opinion you should not touch the type since it's the only thing could distinguish Leader from Person.

it' better to use rewhere to set what scope of emails you want to query such as: leader_emails, personal_emails and all_emails. so you could do nothing with type.

class Leader < Person
  self.table_name = 'person'
  
  def emails(scopes:)
   email_addresses.rewhere(entity_type: scopes) 
  end
end

Leader.first.emails scopes: 'Leader'
Leader.first.emails scopes: 'Person'
Leader.first.emails scopes: ['Person', 'Leader']
Lam Phan
  • 3,405
  • 2
  • 9
  • 20
  • Lam Phan: the `email_addresses` table, and all associations that live on the Person model will ALWAYS have `Person` as the type, so there's no need to send a configurable scope. I'm trying to make sure that interacting with the association on the subclass always knows to use the superclass type, both in reading and writing operations. – pixelearth Aug 16 '21 at 14:04
0

You haven't showed us all your (relevant) models, but this works (rails 6.1.4) as you would like it to:

# person.rb, table created with:
#    create_table :people do |t|
#      t.string :first_name
#      t.string :last_name
#      t.string :type

#      t.timestamps
#    end


class Person < ApplicationRecord
  has_many :email_addresses, as: :addressable
end

# leader.rb

class Leader < Person
  # no need to specify table_name, it's inherited
end

# email_address.rb, table created with
#    create_table :email_addresses do |t|
#      t.string :address
#      t.bigint :addressable_id
#      t.string :addressable_type

#      t.timestamps
#    end

class EmailAddress < ApplicationRecord
  belongs_to :addressable, polymorphic: true
end

And here are the queries from the rails console:

$> Person.first.email_addresses
  Person Load (0.3ms)  SELECT "people".* FROM "people" ORDER BY "people"."id" ASC LIMIT ?  [["LIMIT", 1]]
  EmailAddress Load (0.2ms)  SELECT "email_addresses".* FROM "email_addresses" WHERE "email_addresses"."addressable_id" = ? AND "email_addresses"."addressable_type" = ? /* loading for inspect */ LIMIT ?  [["addressable_id", 1], ["addressable_type", "Person"], ["LIMIT", 11]]

$> Leader.first.email_addresses
  Leader Load (0.3ms)  SELECT "people".* FROM "people" WHERE "people"."type" = ? ORDER BY "people"."id" ASC LIMIT ?  [["type", "Leader"], ["LIMIT", 1]]
  EmailAddress Load (0.4ms)  SELECT "email_addresses".* FROM "email_addresses" WHERE "email_addresses"."addressable_id" = ? AND "email_addresses"."addressable_type" = ? /* loading for inspect */ LIMIT ?  [["addressable_id", 2], ["addressable_type", "Person"], ["LIMIT", 11]]

Note that addressable_type is "Person" whether querying email addresses for an instance of Person or an instance of Leader

As a (very small) style comment, let me suggest that the model should be called Email, with address as one of its attributes. It feels redundant to query EmailAddress.first.address it should be Email.first.address.

Les Nightingill
  • 5,662
  • 1
  • 29
  • 32
  • Thanks for the suggestions, but your association is still using `Leader` as the type: `ORDER BY "people"."id" ASC LIMIT ? [["type", "Leader"]`. Also the association is called EmailAddress, because we have an Email model that deals with actual emails that are sent. – pixelearth Aug 22 '21 at 18:06
  • @pixelearth yes, of course it uses `Leader` when you call a class method on `Leader`, like `Leader.first`. But you are interested in the emails, no? That's the query you posted about in your OP. So `Leader.first.emails` creates a query on the emails table that uses `Person` as the addressable_type. Whereas in your OP, your query used `Leader` as the addressable type in the emails table query. – Les Nightingill Aug 23 '21 at 14:39
  • this must be a difference in Rails 6 and 5. Our app is currently on 5 – pixelearth Aug 23 '21 at 15:46
  • I just tried it in Rails 5.2.6... it works the same as in Rails 6 – Les Nightingill Aug 23 '21 at 18:07
  • I was mistaken, we're on Rails 6.0.3.4, but either way I still get the behavior I describe in my orig post. Not sure what accounts for the difference. Maybe a gem is affecting one of our apps? – pixelearth Aug 24 '21 at 15:05
  • Well you haven't showed us how the associations are specified in your models. So it may be different from the models I have showed, which works as expected. Also, you have specified the table in the Leader model, which is not necessary and which I didn't do. I have no special gems in the app that I describe in my response, it's just a basic rails app that I made to diagnose your problem. – Les Nightingill Aug 25 '21 at 00:57