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

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

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:

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

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

FWIW, no, it is not possible to do that without a LOT of sanitation of Rails/ActiveRecord code.

The problem is that the 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 that is pretty much unadaptable to your fancy concepts unless you want to sit down and rewrite large parts of it.

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

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.

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

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.

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

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

Hi,

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.

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.

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.