Rails 中的复杂多对多关系

Complex many-to-many relation in Rails

我有一个模型Place。 比如地方可以是一个城市,一个地区,一个地区,一个国家,但是我觉得一个模型就够了,因为对于我来说,城市和地区之间没有太大的区别。

而且地方可以彼此属于,例如一个地区可以有很多城市,一个国家可以有很多地区等等

但我的问题是它是一种历史工程,一个城市可以属于多个地区。例如,从 1800 年到 1900 年,一个城市属于一个历史区域(现在不存在),从 1900 年到现在 - 属于另一个历史区域。重要的是将这些区域存储为不同的地方,尽管它们可以具有相似的地理边界,但只有名称不同。

我猜是多对多,但也许有人可以给出更好的主意?

如果它是多对多的,我怎样才能在一个查询中得到一系列的父地点来制作像 "Place, Parent place 1, Parent place 2, Parent place 3" 这样的简单字符串,例如"San Francisco, California, USA"?

这是我的代码:

create_table :places do |t|
  t.string :name

  t.timestamps null: false
end

create_table :place_relations, id: false do |t|
  t.integer :sub_place_id
  t.integer :parent_place_id

  t.timestamps null: false
end

class Place < ActiveRecord::Base
  has_and_belongs_to_many :parent_places,
    class_name: "Place",
    join_table: "place_relations",
    association_foreign_key: "parent_place_id"

  has_and_belongs_to_many :sub_places,
    class_name: "Place",
    join_table: "place_relations",
    association_foreign_key: 'sub_place_id'
end

请不要犹豫,给我一些想法!

这会在不干预该模型的情况下将直接多对多关系与另一个模型关联起来。但是如果你想要像 Polymorphic Association 这样的东西,你可以使用更高级的东西。

有关更多信息,请访问 Rails 指南:Polymorphic Association

这是我想到的第一个解决方案,可能还有很多其他方法可以做到,但我相信这可能是最干净的。

您的总体思路是正确的,但您只需要对连接稍作修改 table。本质上,您将改用 has_many... through 关系,以便您可以附加某种时间范围鉴别器。

在我的示例中,我使用日期时间字段来指示关联从什么时候开始相关。结合默认范围按时间鉴别器(在我的示例中称为 effective_from)对结果进行排序,您可以轻松地 select "current" parents 和 children 无需额外努力的地方,或 select 在 where 子句中使用单个日期比较的历史数据。 请注意,您不需要像我一样处理时间框架歧视,这只是为了演示概念。根据需要修改。

class PlaceRelation < ActiveRecord::Base
  default_scope { order "effective_from DESC" }

  belongs_to :parent, class_name: "Place"
  belongs_to :child, class_name: "Place"
end

class Place < ActiveRecord::Base
  has_many :parent_places, class_name: "PlaceRelation", foreign_key: "child_id"
  has_many :child_places, class_name: "PlaceRelation", foreign_key: "parent_id"

  has_many :parents, through: :parent_places, source: :parent
  has_many :children, through: :child_places, source: :child
end

place_relations table 的迁移应该如下所示:

class CreatePlaceRelations < ActiveRecord::Migration
  def change
    create_table :place_relations do |t|
      t.integer :parent_id
      t.integer :child_id
      t.datetime :effective_from
      t.timestamps
    end
  end
end

所以如果我们创建一对 "top level" country-places:

country1 = Place.create(name: "USA")
country2 = Place.create(name: "New California Republic")

和州府

state = Place.create("California")

还有一座城市

city = Place.create("San Francisco")

最后把它们绑在一起:

state.parent_places.create(parent_id: country1.id, effective_from: DateTime.now - 1.year)
state.parent_places.create(parent_id: country2.id, effective_from: DateTime.now)

city.parent_places(parent_id: state.id, effective_from: DateTime.now)

那么您将拥有一个属于州 "California" 的城市 ("San Francisco"),历史上 属于国家 "USA" , 后来 "New California Republic".

此外,如果您想构建一个包含地点名称及其所有内容的字符串 "parents",您可以 "recursively" 这样做:

def full_name(name_string = [])
  name_string << self.name
  parent = self.parents.first
  if parent.present?
    return parent.full_name name_string
  else
    return name_string.join(", ")
  end
end

对于我们的城市 "San Francisco",根据 effective_from 字段的顺序,结果应该是 "San Francisco, California, New California Republic"