This project is archived and is in readonly mode.

#2909 ✓stale
Alex

Polymorphic many to many relationships...

Reported by Alex | July 15th, 2009 @ 04:51 AM | in 3.x

It would seem as though ActiveRecord is missing support for well designed polymorphic many to many relationships.

I have a model called "Task".
I have an abstract model called "Target".
I would like to relate multiple instances of subclasses of Target to Task.
I am not using single table inheritance.
I would like to query the polymorphic relationship to return a mixed result set.
I would like to query individual instances of subclasses of Target to obtain tasks that they are in a relationship with.
So, I figure a polymorphic many to many relationship between Tasks and subclasses of Targets is in order. In more detail, I will be able to do things like this in the console (and of course elsewhere):

task = Task.find(1)
task.targets
[...array of all the subclasses of Target here...] But! Assuming models "Store", "Software", "Office", "Vehicle", which are all subclasses of "Target" exist, it would be nice to also traverse the relationship in the other direction:

store = Store.find(1)
store.tasks
[...array of all the Tasks this Store is related to...] software = Software.find(18)
software.tasks
[...array of all the Tasks this Software is related to...] The database tables implied by polymorphic relationships appears to be capable of doing this traversal, but I see some recurring themes in trying to find an answer which to me defeat the spirit of polymorphic relationships. Using my example still, people appear to want to define Store, Software, Office, Vehicle in Task, which we can tell right away isn't a polymorphic relationship as it only returns one type of model.

The full text and a little discussion on my issue can be read over at stackoverflow:
http://stackoverflow.com/questions/1128308/rails-many-to-many-polym...

I apologize if there is a better place for this idea, do point me in the right direction.

Comments and changes to this ticket

  • Alex

    Alex July 16th, 2009 @ 01:16 AM

    I had a conversation with someone in #rubyonrails and he was surprised to discover that this functionality was not supported.

    After talking for a bit, we thought that a few assumptions could be made about a HABTM polymorphic relationship. Whether they are feasible or not is not in my realm of expertise as I am not familiar with the internals of ActiveRecord. I'm going to try and whip together a quick example case of how this might work and some of the thinking behind it:

    -=- class Task < ActiveRecord::Base

    has_and_belongs_to_many(

       :targets,
       :polymorphic => true
    

    )

    end
    -=- This establishes that Task will be relating to records via a table called "targets_tasks". This table will contain the familiar target_id and target_type columns. As well as a reference to the Task.

    -=- class Store < ActiveRecord::Base
    has_and_belongs_to_many(

       :tasks,
       :as => target
    

    ) end

    class Vehicle < ActiveRecord::Base
    has_and_belongs_to_many(

       :tasks,
       :as => target
    

    ) end

    ...and so on and so forth....

    -=-

    In an application's console:

    task = Task.find(69) task.targets [#<Vehicle...>,#<Vehicle...>,#<Store...>,#<Store...>.#<Store...>]

    store = Store.find(42) store.tasks [#<Task...>,#<Task...>,#<Task...>,#<Task...>,#<Task...>]

    -=-

    Trying my best not to make too much light of the complexity involved here, all the information needed is available. We can infer the class of the "target" from the relationship table - which I assume is already being done with the current concept of polymorphic relationships.
    This is a highly opinionated concept for implementation which can yield mixed lists with or without single table inheritance.

  • Nick

    Nick January 7th, 2010 @ 05:40 PM

    +1 on this - I've come across an instance in one of my applications (stock control) where this would be really useful.

    I'm going to try hacking at ActiveRecord to get it to support polymorphic has_and_belongs_to_many /somehow/ (including having one side of the association polymorphic, but the other side not, and both sides polymorphic) - if I get anything useful, I shall submit a patch here.

  • Matt Jones

    Matt Jones January 7th, 2010 @ 07:07 PM

    There's has_many_polymorphs, which does much of what you're describing. You'll also probably have much more luck with an explicit join model and has_many :through rather than habtm, which is something of a second-class citizen in current Rails.

  • Alex

    Alex January 7th, 2010 @ 08:14 PM

    I got some emails that there were updates to this issue...

    I ultimately never did get this working. I did also look at has_many_polymorphs, and I should note that it isn't really a solution either.

    What I was looking for was a way to define the relationship on the one side and at most the abstract class on the other (and have the subclasses inherit it). Although I figure it should be possible by only making the declaration on the owning side.

    (As a loose example...) Issue "has many" Targets
    Where Target is an abstract class that is subclassed by things like Cars, Stores, Computers, or whatever.

    The only declaration in this case would be in Issue where it says it has many Targets. Understandably, something might have to be in Target, so it would have a kind of "belongs to Issue" declaration.

    I know this is possible as I did get a rough ORM working in PHP to do this. I followed a lot of the Rails paradigms to get it going.

    has_many_polymorphs does little more than provide a shortcut to making declarations you can make in regular Rails anyway. I never saw it as a plugin/extension worth using and it didn't simplify my design.

  • Matt Jones

    Matt Jones January 7th, 2010 @ 09:25 PM

    The issue is that if you're not using STI on Target in the example above, the standard HABTM join record with just two ids isn't sufficient to identify the target record - what table should get the SELECT statement?

    This almost works:

    class Task < AR::Base
      has_many :target_links
      has_many :targets, :through => :target_links, :source => :targetable # => this gives a HasManyThroughAssociationPolymorphicError exception since :source_type isn't specified
    end


    class TargetLink < AR::Base belongs_to :task belongs_to :targetable, :polymorphic => true end


    class Target < AR::Base has_many :target_links, :as => :targetable has_many :tasks, :through => :target_links end

    The reason the error is thrown in the example above is that the generated SQL wouldn't make sense - what table should be joined against to retrieve records? That's the step that has_many_polymorphs makes possible, via the specification of :from => [:model1, :model2, :model3].

    As far as supporting this natively in has_and_belongs_to_many, it doesn't seem likely - 2.3 finally stripped out the (long-deprecated) "decorated join table" stuff with the explicit goal of encouraging more use of has_many :through for this kind of thing.

  • Alex

    Alex January 7th, 2010 @ 10:42 PM

    When I made my solution in PHP, I inferred the table name by the name of the subclass and maintained a table under the name of the "having" class and the abstract "belonging" class which stored the relationships.

    I had a table that looked like this:
    ] TaskTarget o TaskID o ID o Type

    I had no way of avoiding (my own MySQL knowledge may have limited this) creating the final graph with separate queries for each subclassed type. I always had to query TaskTarget, get the list sorted by Type and then run a query for each type:

    SELECT * FROM Store WHERE ID IN(?,?,?,?)...

    This didn't seem like a problem as far as I was concerned as I was querying them by ID (fairly light) and any caching mechanism could easily smooth that over. Considering that you are mixing data and program slightly to accomplish this (a necessary evil), the additional step seemed quite cheap for the capacity to have what I call "blind" relationships with subclasses of a known abstract class...

    Very fun to talk about :)

  • Jeremy Kemper

    Jeremy Kemper May 4th, 2010 @ 06:48 PM

    • Milestone changed from 2.x to 3.x
  • Rohit Arondekar

    Rohit Arondekar October 9th, 2010 @ 03:31 AM

    • State changed from “new” to “stale”
    • Importance changed from “” to “”

    Marking ticket as stale. If this is still an issue please leave a comment with suggested changes, creating a patch with tests, rebasing an existing patch or just confirming the issue on a latest release or master/branches.

Create your profile

Help contribute to this project by taking a few moments to create your personal profile. Create your profile »

<h2 style="font-size: 14px">Tickets have moved to Github</h2>

The new ticket tracker is available at <a href="https://github.com/rails/rails/issues">https://github.com/rails/rails/issues</a>

Pages