Rails: Many to many polymorphic relationships

前端 未结 7 2057
孤城傲影
孤城傲影 2021-01-30 00:17

See comments for updates.

I\'ve been struggling to get a clear and straight-forward answer on this one, I\'m hoping this time I\'ll get it! :D I definitely have

相关标签:
7条回答
  • 2021-01-30 00:41

    This may not be an especially helpful answer, but stated simply, I don't think there is an easy or automagic way to do this. At least, not as easy as with simpler to-one or to-many associations.

    I think that creating an ActiveRecord model for the join table is the right way to approach the problem. A normal has_and_belongs_to_many relationship assumes a join between two specified tables, whereas in your case it sounds like you want to join between tasks and any one of stores, softwares, offices, or vehicles (by the way, is there a reason not to use STI here? It seems like it would help reduce complexity by limiting the number of tables you have). So in your case, the join table would also need to know the name of the Target subclass involved. Something like

    create_table :targets_tasks do |t|
      t.integer :target_id
      t.string :target_type
      t.integer :task_id
    end
    

    Then, in your Task class, your Target subclasses, and the TargetsTask class, you could set up has_many associations using the :through keyword as documented on the ActiveRecord::Associations::ClassMethods rdoc pages.

    But still, that only gets you part of the way, because :through won't know to use the target_type field as the Target subclass name. For that, you might be able to write some custom select/finder SQL fragments, also documented in ActiveRecord::Associations::ClassMethods.

    Hopefully this gets you moving in the right direction. If you find a complete solution, I'd love to see it!

    0 讨论(0)
  • 2021-01-30 00:53

    I agree with the others I would go for a solution that uses a mixture of STI and delegation would be much easier to implement.

    At the heart of your problem is where to store a record of all the subclasses of Target. ActiveRecord chooses the database via the STI model.

    You could store them in a class variable in the Target and use the inherited callback to add new ones to it. Then you can dynamically generate the code you'll need from the contents of that array and leverage method_missing.

    0 讨论(0)
  • 2021-01-30 00:56

    Have you pursued that brute force approach:

    class Task 
      has_many :stores
      has_many :softwares
      has_many :offices
      has_many :vehicles
    
      def targets
        stores + softwares + offices + vehicles
      end
      ...
    

    It may not be that elegant, but to be honest it's not that verbose, and there is nothing inherently inefficient about the code.

    0 讨论(0)
  • 2021-01-30 00:56

    The has_many_polymorphs solution you mention isn't that bad.

    class Task < ActiveRecord::Base
      has_many_polymorphs :targets, :from => [:store, :software, :office, :vehicle]
    end
    

    Seems to do everything you want.

    It provides the following methods:

    to Task:

    t = Task.first
    t.targets   # Mixed collection of all targets associated with task t
    t.stores    # Collection of stores associated with task t
    t.softwares # same but for software
    t.offices   # same but for office
    t.vehicles  # same but for vehicles
    

    to Software, Store, Office, Vehicle:

    s = Software.first    # works for any of the subtargets.
    s.tasks               # lists tasks associated with s
    

    If I'm following the comments correctly, the only remaining problem is that you don't want to have to modify app/models/task.rb every time you create a new type of Subtarget. The Rails way seems to require you to modify two files to create a bidirectional association. has_many_polymorphs only requires you to change the Tasks file. Seems like a win to me. Or at least it would if you didn't have to edit the new Model file anyway.

    There are a few ways around this, but they seem like way too much work to avoid changing one file every once in a while. But if you're that dead set against modifying Task yourself to add to the polymorphic relationship, here's my suggestion:

    Keep a list of subtargets, I'm going to suggest in lib/subtargets formatted one entry per line that is essentially the table_name.underscore. (Capital letters have an underscore prefixed and then everything is made lowercase)

    store
    software
    office
    vehicle
    

    Create config/initializers/subtargets.rb and fill it with this:

    SubtargetList = File.open("#{RAILS_ROOT}/lib/subtargets").read.split.reject(&:match(/#/)).map(&:to_sym)
    

    Next you're going to want to either create a custom generator or a new rake task. To generate your new subtarget and add the model name to the subtarget list file, defined above. You'll probably end up doing something bare bones that makes the change and passes the arguments to the standard generator.

    Sorry, I don't really feel like walking you through that right now, but here are some resources

    Finally replace the list in the has_many_polymorphs declaration with SubtargetList

    class Task < ActiveRecord::Base
      has_many_polymorphs :targets, :from => SubtargetList
    end
    

    From this point on you could add a new subtarget with

    $ script/generate subtarget_model home
    

    And this will automatically update your polymorphic list once you reload your console or restart the production server.

    As I said it's a lot of work to automatically update the subtargets list. However, if you do go this route you can tweak the custom generator ensure all the required parts of the subtarget model are there when you generate it.

    0 讨论(0)
  • 2021-01-30 00:59

    You can combine polymorphism and has_many :through to get a flexible mapping:

    class Assignment < ActiveRecord::Base
      belongs_to :task
      belongs_to :target, :polymorphic => true
    end
    
    class Task < ActiveRecord::Base
      has_many :targets, :through => :assignment
    end
    
    class Store < ActiveRecord::Base
      has_many :tasks, :through => :assignment, :as => :target
    end
    
    class Vehicle < ActiveRecord::Base
      has_many :tasks, :through => :assignment, :as => :target
    end
    

    ...And so forth.

    0 讨论(0)
  • 2021-01-30 00:59

    Using STI:

    class Task < ActiveRecord::Base
    end
    
    class StoreTask < Task
      belongs_to :store, :foreign_key => "target_id"
    end
    
    class VehicleTask < Task
      belongs_to :vehicle, :foreign_key => "target_id"
    end
    
    class Store < ActiveRecord::Base
      has_many :tasks, :class_name => "StoreTask", :foreign_key => "target_id"
    end
    
    class Vehicle < ActiveRecord::Base
      has_many :tasks, :class_name => "VehicleTask", :foreign_key => "target_id"
    end
    

    In your databse you'll need: Task type:string and Task target_id:integer

    The advantage is that now you have a through model for each task type which can be specific.

    See also STI and polymorphic model together

    Cheers!

    0 讨论(0)
提交回复
热议问题