[Question] Is there any way to define class_name dynamically in has_many relation ?

857 views
Skip to first unread message

Takashi Nakagawa

unread,
Jan 17, 2014, 4:55:05 AM1/17/14
to rubyonra...@googlegroups.com

Walter Lee Davis

unread,
Jan 17, 2014, 9:25:24 AM1/17/14
to rubyonra...@googlegroups.com
Have you read anything about polymorphic relationships yet? That's a good place to start. Rails Guides, ActiveRecord Relations, read the whole page, but pay attention to the section on Polymorphism.

Walter

On Jan 17, 2014, at 4:55 AM, Takashi Nakagawa wrote:

>
>
> --
> You received this message because you are subscribed to the Google Groups "Ruby on Rails: Talk" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to rubyonrails-ta...@googlegroups.com.
> To post to this group, send email to rubyonra...@googlegroups.com.
> To view this discussion on the web visit https://groups.google.com/d/msgid/rubyonrails-talk/ecc2d9dd-fb18-4248-a50c-4fa9c36721b1%40googlegroups.com.
> For more options, visit https://groups.google.com/groups/opt_out.

Takashi Nakagawa

unread,
Jan 20, 2014, 9:43:00 AM1/20/14
to rubyonra...@googlegroups.com
Hi walter, thanks for answering question.

I've read about polymorphic relationships and I've tried it before, but in my case, I should use database which is already used in other project and I make new rails app with the database. so I can' t change database structure easily. 

2014年1月17日金曜日 23時25分24秒 UTC+9 Walter Lee Davis:

Walter Lee Davis

unread,
Jan 20, 2014, 9:56:19 AM1/20/14
to rubyonra...@googlegroups.com

On Jan 20, 2014, at 9:43 AM, Takashi Nakagawa wrote:

> Hi walter, thanks for answering question.
>
> I've read about polymorphic relationships and I've tried it before, but in my case, I should use database which is already used in other project and I make new rails app with the database. so I can' t change database structure easily.

Okay, well there are ways to change Rails' "opinionated" conventions about table naming and model naming. Have a look at the Rails Guides, I don't have time to look it up for you, but I know it's in there.

The only reason I can think of to not do what I recommend here is if you really really actually need some particular database feature to actually make your application work. Rails is mightily database-agnostic, but only if you don't need those features. If you do, then you really will find yourself fighting the conventions of the framework every step of the way.

What I recommend you do is clear off a section of whiteboard and write up a concordance between your existing table names and the models you want to create. So if you have a model named Person, which would want its table to be named people, yet your DBA insisted on naming that one ApplicationUsers or whatever, just draw a big circle and put both names in it. Continue until the *logical* structure of your Rails app is mapped out according to what you want to call your object (which drives the URL structure among other things).

Build your app using a different database -- maybe just a SQLite db -- with all of the tables named the way Rails expects them to be, for speed and flexibility (use migrations). Be sure to write your tests, and get things working the way you want the app to look. Then go back to your whiteboard, and your model files, and add the line of code that re-defines the table name (and primary key, if it isn't named id, or foreign key, if it isn't named singular_underscored_model_name_id) for each of your relationships. Change the database over, and see if it still works. Test and patch until it does.

Walter

>
> 2014年1月17日金曜日 23時25分24秒 UTC+9 Walter Lee Davis:
> Have you read anything about polymorphic relationships yet? That's a good place to start. Rails Guides, ActiveRecord Relations, read the whole page, but pay attention to the section on Polymorphism.
>
> Walter
>
> On Jan 17, 2014, at 4:55 AM, Takashi Nakagawa wrote:
>
> >
> >
> > --
> > You received this message because you are subscribed to the Google Groups "Ruby on Rails: Talk" group.
> > To unsubscribe from this group and stop receiving emails from it, send an email to rubyonrails-ta...@googlegroups.com.
> > To post to this group, send email to rubyonra...@googlegroups.com.
> > To view this discussion on the web visit https://groups.google.com/d/msgid/rubyonrails-talk/ecc2d9dd-fb18-4248-a50c-4fa9c36721b1%40googlegroups.com.
> > For more options, visit https://groups.google.com/groups/opt_out.
>
>
> --
> You received this message because you are subscribed to the Google Groups "Ruby on Rails: Talk" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to rubyonrails-ta...@googlegroups.com.
> To post to this group, send email to rubyonra...@googlegroups.com.
> To view this discussion on the web visit https://groups.google.com/d/msgid/rubyonrails-talk/6e224b17-820d-451e-be23-96b70cbeabf6%40googlegroups.com.

Matt Jones

unread,
Jan 22, 2014, 12:10:24 PM1/22/14
to rubyonra...@googlegroups.com


On Monday, 20 January 2014 09:43:00 UTC-5, Takashi Nakagawa wrote:
Hi walter, thanks for answering question.

I've read about polymorphic relationships and I've tried it before, but in my case, I should use database which is already used in other project and I make new rails app with the database. so I can' t change database structure easily. 


Are you attempting to make a Rails app that *shares* a DB with another application? In my experience, that's a phenomenally terrible idea - it sounds good at first, but you wind up having a very hard time debugging things or even creating a realistic test environment. If the old application must continue running, is there any way you can set up an "extract" process that copies + reformats data into the Rails app in a saner format?

If you absolutely can't do that, you'll need to be more specific about the problem you're trying to solve. Where is this "dynamic" class name supposed to be coming from?

--Matt Jones

ngo...@gmail.com

unread,
Apr 24, 2014, 10:53:37 AM4/24/14
to rubyonra...@googlegroups.com
FWIW, no, it is not possible to do that without a LOT of sanitation of Rails/ActiveRecord code.

The problem is that the <extremely competent and forward-looking> developers decided to do all the heavy lifting in reflections instead of the associations. Why is that a problem? Because reflections *have no information AT ALL* about the specific association they are being used for.

Example: you have a model "Dog" that has_many :bones.

Now: dog = Dog.first; dog.bones  #=> Array

Ignore the "Array" part, that's due to idiocy of the framework (basically AssociationProxy tries to be a BasicObject and fails *hard*). The framework at that point does the actual lookup through an "association proxy" which has a lot of information: the specific dog, ultimately all the bones, and additionally the reflection which has all information about the "has_many" call. Rails then goes on to basically throw away all the nice information from the association proxy, turns to the reflection, and ends up with two methods: Reflection#class_name and the derived #class_name, both of which boil down to either a value derived from the association name itself, or the :class_name option.

Now you might think, alright, I'll just do some minimal hacking and fix stuff so Reflection#class_name looks up the "left side" of the association call. That won't work, because (1) the reflection is really just a dumb memoisation of the has_many call, and (2) it's pretty much a singleton, so if you change it, you'll break your entire runtime.

You might also think that you could fix up the AssociationProxy. That's a better thought since it konws about the owner model and can just call aribtrary methods in there, but then you spend 4 hours on it, just to realise that later on the framework decides to use Reflection#chain and effectively bypasses all your fancy work when trying to build the scope for the final query.

All in all, it's another case of Rails/AR being a humongous pile of <organically-grown code> that is pretty much unadaptable to your fancy concepts unless you want to sit down and rewrite large parts of it.

Colin Law

unread,
Apr 24, 2014, 11:35:08 AM4/24/14
to rubyonra...@googlegroups.com
On 24 April 2014 15:53, ngo...@googlemail.com <ngo...@gmail.com> wrote:
> FWIW, no, it is not possible to do that without a LOT of sanitation of
> Rails/ActiveRecord code.

For those of us with less knowledge of how the internals of rails
works could you provide a simple example of what you are attempting to
achieve with dynamic class name in a association?

Thanks

Colin
> --
> You received this message because you are subscribed to the Google Groups
> "Ruby on Rails: Talk" group.
> To unsubscribe from this group and stop receiving emails from it, send an
> email to rubyonrails-ta...@googlegroups.com.
> To post to this group, send email to rubyonra...@googlegroups.com.
> To view this discussion on the web visit
> https://groups.google.com/d/msgid/rubyonrails-talk/8c7e2aa5-eeca-4469-9e22-6af7280a65cf%40googlegroups.com.
> For more options, visit https://groups.google.com/d/optout.

ngo...@gmail.com

unread,
Apr 25, 2014, 3:59:56 AM4/25/14
to rubyonra...@googlegroups.com
On Thursday, April 24, 2014 5:35:08 PM UTC+2, Colin Law wrote:
For those of us with less knowledge of how the internals of rails
works could you provide a simple example of what you are attempting to
achieve with dynamic class name in a association?

 I can't speak for the OP, but in my case I needed something like:

class Report < ActiveRecord::Base
  has_many :reports_subjects
  has_many :subjects, through: :reports_subjects, class_name: ->(report) { report.subjects_type }

  def subjects_type
    # divine required subject model class somehow
  end
end

Of course, that won't fly since the class_name is evaluated purely as a string deep down in the reflection and at that point, all knowledge of the specific instances involved in an association has been discarded.

Colin Law

unread,
Apr 25, 2014, 4:20:03 AM4/25/14
to rubyonra...@googlegroups.com
On 25 April 2014 08:59, ngo...@googlemail.com <ngo...@gmail.com> wrote:
> On Thursday, April 24, 2014 5:35:08 PM UTC+2, Colin Law wrote:
>>
>> For those of us with less knowledge of how the internals of rails
>> works could you provide a simple example of what you are attempting to
>> achieve with dynamic class name in a association?
>
>
> I can't speak for the OP, but in my case I needed something like:

Since you did not quote a previous message then the assumption is that
you are the OP.

>
> class Report < ActiveRecord::Base
> has_many :reports_subjects
> has_many :subjects, through: :reports_subjects, class_name: ->(report) {
> report.subjects_type }
>
> def subjects_type
> # divine required subject model class somehow
> end
> end
>
> Of course, that won't fly since the class_name is evaluated purely as a
> string deep down in the reflection and at that point, all knowledge of the
> specific instances involved in an association has been discarded.

What I don't understand is why you would want to do that.

Colin

ngo...@gmail.com

unread,
Apr 25, 2014, 5:42:32 AM4/25/14
to rubyonra...@googlegroups.com


On Friday, April 25, 2014 10:20:03 AM UTC+2, Colin Law wrote:

Since you did not quote a previous message then the assumption is that
you are the OP.

The problem in this case was an empty original message with all the semantics in the subject. 
 
What I don't understand is why you would want to do that.

Because in this case the polymorphic aspect of the subject side of the join table is entirely defined by the Report instance (a report only ever covers one type of subject, but potentially a lot of them), making it wasteful to put it in the join table (also, polymorphic mas-many-through tends to be a pain). Additionally report instances would be relatively short-lived so integrity over an extended time is not an issue.

Colin Law

unread,
Apr 25, 2014, 6:30:04 AM4/25/14
to rubyonra...@googlegroups.com
OK, thanks for the explanation. I think with any 'opinionated'
framework like Rails there comes a point where one either has to go
with the mechanisms that the framework provides or to use a different
framework. I don't think there is any point in making statements like
"All in all, it's another case of Rails/AR being a humongous pile of
<organically-grown code> that is pretty much unadaptable to your fancy
concepts unless you want to sit down and rewrite large parts of it".
If you don't like it then don't use it. Or don't use fancy concepts
that don't fit well with Rails.

Colin

Matt Jones

unread,
Apr 28, 2014, 3:31:06 PM4/28/14
to rubyonra...@googlegroups.com
This doesn't make any sense to me - if I request a ReportsSubject object from the database directly (via `find`, for instance), what do I get if I ask for its subject? What would the reports_subjects table even *store*? A bare `subject_id` would be insufficient since without a class_name it's unclear what table that ID refers to. And that's not even considering what should happen when this sort of code runs:

Report.joins(:subjects).where(name: 'hey wait WHAT TABLE IS THIS EVEN QUERYING')

--Matt Jones

ngo...@gmail.com

unread,
Apr 29, 2014, 3:50:39 AM4/29/14
to rubyonra...@googlegroups.com
Hi,


On Monday, April 28, 2014 9:31:06 PM UTC+2, Matt Jones wrote:

This doesn't make any sense to me - if I request a ReportsSubject object from the database directly (via `find`, for instance), what do I get if I ask for its subject? What would the reports_subjects table even *store*? A bare `subject_id` would be insufficient since without a class_name it's unclear what table that ID refers to.

A "report subject" (a row in the join table) doesn't carry any semantics without the report, which has further information about required details. A report also only ever has a single type of subject, so IMO it would not make sense to store a couple thousand repetitions of an STI name (although note that the subjects are not all part of a single STI hierarchy!) in a column that adds no information at all to the system.
 
And that's not even considering what should happen when this sort of code runs:

Report.joins(:subjects).where(name: 'hey wait WHAT TABLE IS THIS EVEN QUERYING')

This is not functionally different from having the polymorphic subject type specified in the join table.

Matt Jones

unread,
Apr 30, 2014, 9:23:19 AM4/30/14
to rubyonra...@googlegroups.com
There are a bunch of ways out of this situation, but the right choice depends on what exactly you need to do with the models. Starting from this use case, I'm going to discuss some tradeoffs and possible approaches. The code is more for illustration than anything else; details may be missing, etc.

  class Report < ActiveRecord::Base
    has_many :reports_subjects
    has_many :subjects, through: :reports_subjects, class_name: ->(report) { report.subjects_type }

    def subjects_type
      # divine required subject model class somehow
    end
  end

  class ReportsSubject < ActiveRecord::Base
    belongs_to :report
    belongs_to :subject # DOESN'T EXIST, class_name is based on parent Report instance
  end

Approaches:

* STI for subjects, with a validation to ensure all the subjects are the same type. I assume this is impractical in your use case. But if it works, it's ideal - eager-loading works, adding subjects with `@report.subjects << subject` works, joins work.

None of the approaches below will work with eager-loading.

* If the set of possible subject types is known to Report (likely, since it's deciding which one to use) a polymorphic association + some metaprogramming could work:

  class Report < ActiveRecord::Base
    POSSIBLE_TYPES = %w(Foo Bar Baz)
    has_many :reports_subjects

    POSSIBLE_TYPES.each do |type|
      has_many "#{type.underscore}_subjects", through: :reports_subjects, source: :subject, source_type: type
    end

    def subjects(reload=false)
      send("#{subjects_type.underscore}_subjects", reload)
    end

    def subjects_type
      # divine required subject model class somehow
      # return a *string* from POSSIBLE_TYPES
    end
  end

  class ReportsSubject < ActiveRecord::Base
    belongs_to :report
    belongs_to :subject, polymorphic: true
  end

This gives a "faux" association `subjects` that behaves mostly like a real association:

  @report.subjects.to_a            # works
  @report.subjects << new_subject  # works, raises if you try to push an object that isn't of subjects_type
  @report.subjects.count           # works

but not quite:

  Report.joins(:subjects)         # unknown association 'subjects'

This approach does require a `subject_type` column on reports_subjects. Worries about the performance impact of storing duplicate string values many times is (IMO) premature optimization, but your use case may be different.

* If a `subject_type` column is unfeasible for reasons, there's another way which drops more ActiveRecord machinery.

  class Report < ActiveRecord::Base

    has_many :reports_subjects

    def subjects
      subjects_type.joins(:reports_subject).where(reports_subject: { report_id: id })
    end

    def subjects_type
      # divine required subject model class somehow
      # prefer to return a real Class object here
    end
  end

  class ReportsSubject < ActiveRecord::Base
    belongs_to :report

    def subject
      report.subjects_type.find(subject_id)
    end
  end

  class SomeSubjectThingy < ActiveRecord::Base
    # this is needed in any class that Report can refer to; consider making it a concern
    has_many :reports_subjects, foreign_key: :subject_id
  end

You'll need to supply a method to actually build ReportsSubject objects for a given report, as << will not work on a report's `subjects` property. You'll also likely want to guard against inserting subjects of multiple types, as `ReportsSubject` no longer contains any information regarding what type its subject is.

Some even farther-afield approaches to think about:

* if a Report can only have one subject_type, are all Reports really instances of the same class? Perhaps Report is just a base class for more specific types (FooReport, etc) that have specific associations?

  class FooReport < Report
    has_many :foo_reports_subjects
    has_many :subjects, through: :foo_reports_subjects
  end

  class FooReportsSubject < ActiveRecord::Base # or ReportsSubject, YMMV
    self.table_name = 'reports_subjects'

    belongs_to :report, class_name: 'FooReport'
    belongs_to :subject, class_name: 'Foo'
  end

Given how similar these classes are, it might make sense to metaprogram them. Note that `reports_subjects` does *not* need a `type` column here.

* if you need eager-loading but don't want to do STI subjects, maybe consider a "wrapper" object that lets you eager-load what you want. For instance, suppose that you'd like to get the name of each subject (for UI, etc) when loading a bunch of reports.

  class Report < ActiveRecord::Base
    POSSIBLE_TYPES = %w(Foo Bar Baz)
    has_many :reports_subjects
    has_many :subject_shims, through: :reports_subjects

    def title
      "Report on #{subject_shims.map(&:name).join(', ')}"
    end

    # deal with :subjects somehow
  end

  class ReportsSubject < ActiveRecord::Base
    belongs_to :report
    belongs_to :subject_shim
  end

  class SubjectShim < ActiveRecord::Base
    # denormalize the `name` value from subject into subject_shims `name` column
    has_many :reports_subjects
    belongs_to :subject, polymorphic: true
  end

Then you can still do this:

  Report.includes(:subject_shims).each { |x| puts x.title }

without causing a SQL query per-Report. There are fewer `type` columns here (one per subject, instead of one per subject per report) but an extra database table.

---

Happy to help out if any of these look promising.

--Matt Jones

ngo...@gmail.com

unread,
May 2, 2014, 3:46:36 AM5/2/14
to rubyonra...@googlegroups.com
Hi,

Thanks for your work on that; right now, I have implemented some DIY setters and getters that fake the required functionality as far as I need it, and even does things like condensing a mass-assignment into a single INSERT…SELECT statement (using a lot of conjecture about the interaction between AR and Arel and definitely violating the law of Demeter something fierce), but of course that doesn't provide the whole battery of automagic.

Since the report mechanism will be mostly about shoveling a lot of IDs into and out of the join table, I feel that having more efficiency there is worth losing a bit of comfort in other places.
Reply all
Reply to author
Forward
0 new messages