Rails 关系混乱

Rails relationship confusion

我正在尝试在 Rails 中设置一些关系,但对如何使用我配置的关系有些困惑。

我的场景是这样的: 我有一个名为 Coaster 的模型。我希望每个考斯特都能有 0 个或更多的版本。我希望能够从它的实例中找到 Coaster 的所有版本,也希望能够从它的实例中找到它的所有版本。

我目前的模型和关系:

coaster.rb:

has_many :incarnations
has_many :coaster_versions,
  through: :incarnations

incarnation.rb:

belongs_to :coaster
belongs_to :coaster_version,
           class_name: "Coaster",
           foreign_key: "coaster_id"

Incarnations 的数据库架构:

create_table "incarnations", force: :cascade do |t|
  t.integer "coaster_id"
  t.integer "is_version_of_id"
  t.boolean "is_latest"
  t.integer "version_order"
end

以及从我的 CSV 数据文件导入杯垫时发生的代码:

 # Versions
 # Now determine if this is a new version of existing coaster or not
 if Coaster.where(order_ridden: row[:order_ridden]).count == 1

   # Create Coaster Version that equals itself.
   coaster.incarnations.create!({is_version_of_id: coaster.id, is_latest: true})
 else

   # Set original and/or previous incarnations of this coaster to not be latest
   Coaster.where(order_ridden: row[:order_ridden]).each do |c|
     c.incarnations.each do |i|
       i.update({is_latest: false})
     end
   end

   # Add new incarnation by finding original version
   original_coaster = Coaster.unscoped.where(order_ridden: row[:order_ridden]).order(version_number: :asc).first
   coaster.incarnations.create!({is_version_of_id: original_coaster.id, is_latest: true})

现在我的所有数据库表都已填满,但我不确定如何确保一切按我希望的方式运行。

例如我有两个杯垫(A 和 B),B 是 A 的一个版本。当我得到 A 并要求计算它的数量时 coaster_versions,结果我只返回 1?我当然应该得到 2 还是正确的?

在同一行中,如果我得到 B 并调用 coaster_versions,我也会得到 1。

我只需要确保我真正得到正确的结果。

任何意见都将不胜感激,因为我已经为此工作了很长时间,但进展并不顺利。

以防万一有人回复告诉我查看版本控制 gems。我最初走的是这条路,它很棒,但问题是,在我的情况下,过山车和过山车的一个版本彼此一样重要,我无法 Coaster.all 获得所有过山车,无论它们是否是不是版本。同样的其他问题也出现了。

谢谢

首先,欢迎来到精彩的历史追踪世界!正如您所发现的,跟踪关系数据库中数据的变化实际上并不那么容易。虽然肯定有一些 gems 可以仅出于审计目的跟踪历史记录(例如 auditable),但听起来您希望您的历史记录仍然是第一个 class 公民。所以,让我先分析一下你目前的做法存在的问题,然后我会提出一个更简单的解决方案,可能会让你的生活更轻松。

排名不分先后,以下是您当前系统的一些痛点:

  1. 必须维护 is_latest 列,并且存在不同步的风险。您可能不会在测试中看到这一点,但在大规模生产中,这是一个非常有效的风险。

  2. 您的 incarnations table 创建了一个主版本与多个子版本的结构,除了(类似于 is_latest) 版本的顺序由 version_order 列控制,该列再次需要维护并且存在不正确的风险。目前您的导入脚本似乎没有设置它。

  3. incarnations关系让人很难判断B是A的一个版本;你可以用更多的关系来解决这个问题,但这也会让你的代码更复杂。

  4. 复杂性。很难了解历史是如何跟踪的,而且正如您所发现的,很难管理插入新版本的细节(A 和 B 应该都同意他们有 2 个版本,对吧?因为他们是同一个过山车? )

现在,我认为您的数据模型在技术上仍然有效 -- 我认为您看到的问题是您的脚本问题。但是,使用更简单的数据模型,您的脚本会变得更简单,从而更不容易出错。这是我的做法,只使用一个 table:

create_table "coasters", force: :cascade do |t|
  t.string   "name"
  t.integer  "original_version_id"
  t.datetime "superseded_at"
  t.datetime "created_at",          null: false
  t.datetime "updated_at",          null: false
end

original_version_id 与您的 incarnations table 的作用相同,link 一个版本回到原始记录。 superseded_at 列既可用作 is_latest 检查,也可用作订购版本的方式(尽管在下面,为了简单起见,我只是按 id 订购)。有了这个结构,这就是我的过山车 class:

class Coaster < ActiveRecord::Base
  belongs_to :original_version, class_name: "Coaster"

  scope :latest,   -> { where(superseded_at: nil) }
  scope :original, -> { where('original_version_id = id') }

  # Create a new record, linked properly in history.
  def self.insert(attrs)
    # Find the current latest version.
    if previous_coaster = Coaster.latest.find_by(name: attrs[:name])
      # At the same time, create the new version (linked back to the original version)
      # and deprecate the current latest version. A transaction ensures either both
      # happen, or neither do.
      transaction do
        create!(attrs.merge(original_version_id: previous_coaster.original_version_id))
        previous_coaster.update_column(:superseded_at, Time.now)
      end
    else
      # Create the first version. Set its original version id to itself, to simplify
      # our logic.
      transaction do
        new_record = create!(attrs)
        new_record.update_column(:original_version_id, new_record.id)
      end
    end
  end

  # Retrieve all records linked to the same original version. This will return the
  # same result for any of the versions.
  def versions
    self.class.where(original_version_id: original_version_id)
  end

  # Return our version as an ordinal, e.g. 1 for the very first version.
  def version
    versions.where(['id <= ?', id]).count
  end
end

这使得添加新记录变得简单:

irb> 5.times { Coaster.insert(name: "Coaster A") }
irb> 4.times { Coaster.insert(name: "Coaster B") }
irb> Coaster.latest.find_by(name: "Coaster A").version
   (2.2ms)  SELECT COUNT(*) FROM "coasters" WHERE "coasters"."original_version_id" =  AND (id <= 11)  [["original_version_id", 7]]
=> 5
irb> Coaster.original.find_by(name: "Coaster A").version
   (2.3ms)  SELECT COUNT(*) FROM "coasters" WHERE "coasters"."original_version_id" =  AND (id <= 7)  [["original_version_id", 7]]
=> 1

诚然,它仍然是复杂的代码,如果能简化一下就好了。我的方法当然不是唯一的,也不一定是最好的。不过,希望你学到了一些东西!