如何实现has_many:通过与Mongoid和mongodb的关系?

Posted

技术标签:

【中文标题】如何实现has_many:通过与Mongoid和mongodb的关系?【英文标题】:How to implement has_many :through relationships with Mongoid and mongodb? 【发布时间】:2011-10-23 10:58:13 【问题描述】:

使用the Rails guides 中的这个修改示例,如何使用 mongoid 对关系“has_many :through”关联进行建模?

挑战在于 mongoid 不像 ActiveRecord 那样支持 has_many :through。

# doctor checking out patient
class Physician < ActiveRecord::Base
  has_many :appointments
  has_many :patients, :through => :appointments
  has_many :meeting_notes, :through => :appointments
end

# notes taken during the appointment
class MeetingNote < ActiveRecord::Base
  has_many :appointments
  has_many :patients, :through => :appointments
  has_many :physicians, :through => :appointments
end

# the patient
class Patient < ActiveRecord::Base
  has_many :appointments
  has_many :physicians, :through => :appointments
  has_many :meeting_notes, :through => :appointments
end

# the appointment
class Appointment < ActiveRecord::Base
  belongs_to :physician
  belongs_to :patient
  belongs_to :meeting_note
  # has timestamp attribute
end

【问题讨论】:

【参考方案1】:

Mongoid 没有 has_many :through 或等效功能。它对 MongoDB 没有那么有用,因为它不支持连接查询,因此即使您可以通过另一个集合引用相关集合,它仍然需要多个查询。

https://github.com/mongoid/mongoid/issues/544

通常,如果您在 RDBMS 中存在多对多关系,您将在 MongoDB 中使用包含“外”键数组的字段以不同方式进行建模。例如:

class Physician
  include Mongoid::Document
  has_and_belongs_to_many :patients
end

class Patient
  include Mongoid::Document
  has_and_belongs_to_many :physicians
end

换句话说,您将消除连接表,它会产生与 has_many 类似的效果:在访问“另一边”方面。但在您的情况下,这可能不合适,因为您的联接表是一个 Appointment 类,其中包含一些额外信息,而不仅仅是关联。

如何对此进行建模在某种程度上取决于您需要运行的查询,但您似乎需要添加 Appointment 模型并定义与 Patient 和 Physician 的关联,如下所示:

class Physician
  include Mongoid::Document
  has_many :appointments
end

class Appointment
  include Mongoid::Document
  belongs_to :physician
  belongs_to :patient
end

class Patient
  include Mongoid::Document
  has_many :appointments
end

对于 MongoDB 中的关系,您始终必须在嵌入文档或关联文档之间做出选择。在您的模型中,我猜 MeetingNotes 是嵌入关系的良好候选者。

class Appointment
  include Mongoid::Document
  embeds_many :meeting_notes
end

class MeetingNote
  include Mongoid::Document
  embedded_in :appointment
end

这意味着您可以一起检索笔记和约会,而如果这是一个关联,则需要多个查询。您只需要记住单个文档的 16MB 大小限制,如果您有大量会议记录,这可能会发挥作用。

【讨论】:

+1 非常好的答案,仅供参考,mongodb 大小限制已增加到 16 MB。 出于好奇(抱歉查询迟了),我也是 Mongoid 的新手,我想知道当它是使用单独的集合存储关联的 nn 关系时,您将如何查询数据,它和 ActiveRecord 一样吗?【参考方案2】:

只是为了对此进行扩展,这里的模型扩展了与 has_many 非常相似的方法:从 ActiveRecord 通过返回查询代理而不是记录数组:

class Physician
  include Mongoid::Document
  has_many :appointments

  def patients
    Patient.in(id: appointments.pluck(:patient_id))
  end
end

class Appointment
  include Mongoid::Document
  belongs_to :physician
  belongs_to :patient
end

class Patient
  include Mongoid::Document
  has_many :appointments

  def physicians
    Physician.in(id: appointments.pluck(:physician_id))
  end
end

【讨论】:

这肯定有助于导致我的检索方法返回一个搞乱分页的数组。 没有魔法。 @CyrilDD,你指的是什么? map(&:physician_id) 是 map|appointment| 的简写约会.physician.id 我想知道,考虑到文档不是嵌入的而是使用外部模型关联的,这种方法是否会减少对 16MB 文档大小限制的潜在挫败感? (对不起,如果这是一个菜鸟问题!) 正如弗朗西斯解释的那样,使用.pluck() 而不是.map 要快得多。你能为未来的读者更新你的答案吗? 我收到undefined method 'pluck' for #&lt;Array:...&gt;【参考方案3】:

Steven Soroka 的解决方案真的很棒!我没有评论答案的声誉(这就是我添加新答案的原因:P)但我认为使用 map 来建立关系是昂贵的(特别是如果你的 has_many 关系有数百条|数千条记录),因为它得到从数据库中获取数据,构建每条记录,生成原始数组,然后迭代原始数组以使用给定块中的值构建一个新数组。

使用 pluck 更快,也许是最快的选择。

class Physician
  include Mongoid::Document
  has_many :appointments

  def patients
    Patient.in(id: appointments.pluck(:patient_id))
  end
end

class Appointment
  include Mongoid::Document
  belongs_to :physician
  belongs_to :patient 
end

class Patient
  include Mongoid::Document
  has_many :appointments 

  def physicians
    Physician.in(id: appointments.pluck(:physician_id))
  end
end

这里有一些使用 Benchmark.measure 的统计数据:

> Benchmark.measure  physician.appointments.map(&:patient_id) 
 => #<Benchmark::Tms:0xb671654 @label="", @real=0.114643818, @cstime=0.0, @cutime=0.0, @stime=0.010000000000000009, @utime=0.06999999999999984, @total=0.07999999999999985> 

> Benchmark.measure  physician.appointments.pluck(:patient_id) 
 => #<Benchmark::Tms:0xb6f4054 @label="", @real=0.033517774, @cstime=0.0, @cutime=0.0, @stime=0.0, @utime=0.0, @total=0.0> 

我只使用了 250 个约会。 不要忘记在约会文档中为 :patient_id 和 :physician_id 添加索引!

希望对你有帮助 感谢阅读!

【讨论】:

我收到undefined method 'pluck' for #&lt;Array:...&gt;【参考方案4】:

我想从自引用关联的角度来回答这个问题,而不仅仅是 has_many :through 的角度。

假设我们有一个包含联系人的 CRM。联系人将与其他联系人有关系,但不是在两个不同模型之间创建关系,而是在同一模型的两个实例之间创建关系。一个联系人可以有很多朋友,并且可以与很多其他联系人成为朋友,因此我们将不得不创建多对多的关系。

如果我们使用 RDBMS 和 ActiveRecord,我们将使用 has_many :through。因此我们需要创建一个连接模型,比如 Friendship。该模型将有两个字段,一个表示正在添加朋友的当前联系人的contact_id,一个表示正在成为好友的用户的friend_id。

但我们使用的是 MongoDB 和 Mongoid。如上所述,Mongoid 没有 has_many :through 或等效功能。它对 MongoDB 没有那么有用,因为它不支持连接查询。因此,为了在像 MongoDB 这样的非 RDBMS 数据库中建模多对多关系,您需要使用一个字段,该字段在任一侧都包含一个“外”键数组。

class Contact
  include Mongoid::Document
  has_and_belongs_to_many :practices
end

class Practice
  include Mongoid::Document
  has_and_belongs_to_many :contacts
end

如文档所述:

多对多关系,其中反向文档存储在一个 使用 Mongoid 定义与基础文档分开的集合 has_and_belongs_to_many 宏。这表现出与 Active Record,但不需要加入集合, 外键 ID 以数组形式存储在 关系。

在定义这种性质的关系时,每个文档都存储在 它各自的集合,每个文档都包含一个“外键” 以数组的形式引用另一个。

# the contact document

  "_id" : ObjectId("4d3ed089fb60ab534684b7e9"),
  "practice_ids" : [ ObjectId("4d3ed089fb60ab534684b7f2") ]


# the practice document

  "_id" : ObjectId("4d3ed089fb60ab534684b7e9"),
  "contact_ids" : [ ObjectId("4d3ed089fb60ab534684b7f2") ]

现在对于 MongoDB 中的自引用关联,您有几个选择。

has_many :related_contacts, :class_name => 'Contact', :inverse_of => :parent_contact
belongs_to :parent_contact, :class_name => 'Contact', :inverse_of => :related_contacts

相关联系人与联系人多且属于多实践有什么区别?巨大的差异!一种是两个实体之间的关系。其他是自引用。

【讨论】:

示例文档好像是一样的?

以上是关于如何实现has_many:通过与Mongoid和mongodb的关系?的主要内容,如果未能解决你的问题,请参考以下文章

将 RABL 与 mongoid 一起使用时减少 DB 调用

Mongoid(Rails)中的两个 1 - N 关系

使用 Mongoid 的 MongoDB 对话/私人消息模式

Rails has_many,如何实现嵌套关联?

如何跳过 Mongoid 文档的回调?

不会触发Mongoid after / before_remove回调