Rails:多对多的多态关系

时间:2009-07-14 22:01:42

标签: ruby-on-rails polymorphic-associations

查看评论以了解更新。

我一直在努力在这个问题上得到明确而直截了当的答案,我希望这次我能得到它! :d 我肯定还有很多东西需要学习Rails,但是我确实理解我面临的问题,并且非常感谢你的帮助。

  • 我有一个名为“任务”的模型。
  • 我有一个名为“Target”的抽象模型。
  • 我想将Target的子类的多个实例与Task相关联。
  • 我没有使用单表继承。
  • 我想查询多态关系以返回Target的子类的混合结果集。
  • 我想查询Target子类的各个实例,以获取与之关系的任务。

所以,我认为任务与子目标的子类之间的多对多关系是多态的。 更详细地说,我将能够在控制台(当然还有其他地方)做这样的事情:

task = Task.find(1)
task.targets
[...array of all the subclasses of Target here...]

但是!假设存在“存储”,“软件”,“办公室”,“车辆”等模型,它们都是“目标”的子类,那么在另一个方向上遍历关系也会很好:

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...]

多态关系隐含的数据库表似乎能够进行这种遍历,但我在尝试寻找一个能够打败多态关系精神的答案时看到一些反复出现的主题:

  • 仍然使用我的例子,人们似乎想在任务中定义Store,Software,Office,Vehicle,我们可以立即告诉他们不是多态关系,因为它只返回一种类型的模型
  • 与最后一点类似,人们仍然希望以单向形式或形式定义任务中的商店,软件,办公室和车辆。这里重要的一点是这种关系对子类化是盲目的。我的多态性最初只会与目标进行交互,而不是作为其各自的子类类型。再次定义Task中的每个子类会开始消耗多态关系的目的。
  • 我看到连接表的模型可能是有序的,这对我来说似乎有些正确,除了它增加了我认为Rails愿意废除的一些复杂性。我对这一点感到缺乏经验。

这似乎是导轨功能或集体社区知识中的一个小漏洞。所以希望stackoverflow可以记录我的搜索答案!

感谢所有帮助过的人!

6 个答案:

答案 0 :(得分:55)

您可以结合使用多态和has_many :through来获得灵活的映射:

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

......等等。

答案 1 :(得分:1)

你提到的has_many_polymorphs解决方案并不是那么糟糕。

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

似乎可以做你想做的一切。

它提供了以下方法:

到任务:

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

到软件,商店,办公室,车辆:

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

如果我正确地关注了这些评论,那么唯一剩下的问题就是您不希望每次创建新类型的子目标时都必须修改app / models / task.rb。 Rails方式似乎要求您修改两个文件以创建双向关联。 has_many_polymorphs只需要您更改Tasks文件。对我来说似乎是一场胜利。或者至少,如果您不必编辑新的模型文件,那么至少也是如此。

有一些方法可以解决这个问题,但是它们看起来似乎太过分了,以避免每隔一段时间更换一个文件。但是,如果你自己修改了Task来增加多态关系,那么这就是我的建议:

保留一个子目标列表,我将在lib / subtargets中建议每行格式化一个条目,实际上是table_name.underscore。 (大写字母带有下划线前缀,然后一切都是小写的)

store
software
office
vehicle

创建config / initializers / subtargets.rb并填写:

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

接下来,您将要创建自定义生成器或新的rake任务。生成新的子目标并将模型名称添加到上面定义的子目标列表文件中。你可能最终会做一些简单的事情来做出改变并将参数传递给标准生成器。

很抱歉,我现在不想让您完成这项工作,但这里有some resources

最后用hastargetList

替换has_many_polymorphs声明中的列表
class Task < ActiveRecord::Base
  has_many_polymorphs :targets, :from => SubtargetList
end

从此时起,您可以使用

添加新的子目标
$ script/generate subtarget_model home

一旦重新加载控制台或重新启动生产服务器,这将自动更新您的多态列表。

正如我所说,自动更新子目标列表需要做很多工作。但是,如果你选择这条路线,你可以调整自定义生成器,确保在生成子目标模型时,所有必需的部分都在那里。

答案 2 :(得分:1)

使用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

在您的数据库中,您需要: Task type:stringTask target_id:integer

优势在于,现在每个任务类型都有一个直通模型,可以是特定的。

另见STI and polymorphic model together

干杯!

答案 3 :(得分:0)

这可能不是一个特别有用的答案,但简单地说,我认为没有一种简单或自动的方式来做到这一点。至少,不像简单的一对一或多对话那么容易。

我认为为连接表创建ActiveRecord模型是解决问题的正确方法。正常的has_and_belongs_to_many关系假定两个指定表之间的连接,而在您的情况下,您似乎想要在tasksstoressoftwares中的任何一个之间加入{ {1}},或者offices(顺便说一下,有没有理由不在这里使用STI?看起来它有助于通过限制你拥有的表数来降低复杂性)。因此,在您的情况下,连接表还需要知道所涉及的vehicles子类的名称。像

这样的东西
Target

然后,在您的create_table :targets_tasks do |t| t.integer :target_id t.string :target_type t.integer :task_id end 课程,Task子类和Target课程中,您可以使用TargetsTask关键字设置has_many关联记录在ActiveRecord::Associations::ClassMethods rdoc pages

但是,这仍然只能帮助您解决问题,因为:through无法使用:through字段作为target_type子类名称。为此,您可以编写一些自定义选择/查找器SQL片段,也在ActiveRecord::Associations::ClassMethods中进行了记录。

希望这能让你朝着正确的方向前进。如果你找到了完整的解决方案,我很乐意看到它!

答案 4 :(得分:0)

我同意其他人,我会寻求使用STI混合的解决方案,委托会更容易实现。

问题的核心是存储Target的所有子类的记录。 ActiveRecord通过STI模型选择数据库。

您可以将它们存储在Target中的类变量中,并使用继承的回调向其中添加新的回调。然后,您可以从该数组的内容动态生成您需要的代码,并利用method_missing。

答案 5 :(得分:0)

你有没有追求那种蛮力方法:

class Task 
  has_many :stores
  has_many :softwares
  has_many :offices
  has_many :vehicles

  def targets
    stores + softwares + offices + vehicles
  end
  ...

它可能不那么优雅,但说实话,它并不是那么冗长,而且代码本身没有任何效率。