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 公民。所以,让我先分析一下你目前的做法存在的问题,然后我会提出一个更简单的解决方案,可能会让你的生活更轻松。
排名不分先后,以下是您当前系统的一些痛点:
必须维护 is_latest
列,并且存在不同步的风险。您可能不会在测试中看到这一点,但在大规模生产中,这是一个非常有效的风险。
您的 incarnations
table 创建了一个主版本与多个子版本的结构,除了(类似于 is_latest) 版本的顺序由 version_order
列控制,该列再次需要维护并且存在不正确的风险。目前您的导入脚本似乎没有设置它。
incarnations
关系让人很难判断B是A的一个版本;你可以用更多的关系来解决这个问题,但这也会让你的代码更复杂。
复杂性。很难了解历史是如何跟踪的,而且正如您所发现的,很难管理插入新版本的细节(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
诚然,它仍然是复杂的代码,如果能简化一下就好了。我的方法当然不是唯一的,也不一定是最好的。不过,希望你学到了一些东西!
我正在尝试在 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 公民。所以,让我先分析一下你目前的做法存在的问题,然后我会提出一个更简单的解决方案,可能会让你的生活更轻松。
排名不分先后,以下是您当前系统的一些痛点:
必须维护
is_latest
列,并且存在不同步的风险。您可能不会在测试中看到这一点,但在大规模生产中,这是一个非常有效的风险。您的
incarnations
table 创建了一个主版本与多个子版本的结构,除了(类似于 is_latest) 版本的顺序由version_order
列控制,该列再次需要维护并且存在不正确的风险。目前您的导入脚本似乎没有设置它。incarnations
关系让人很难判断B是A的一个版本;你可以用更多的关系来解决这个问题,但这也会让你的代码更复杂。复杂性。很难了解历史是如何跟踪的,而且正如您所发现的,很难管理插入新版本的细节(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
诚然,它仍然是复杂的代码,如果能简化一下就好了。我的方法当然不是唯一的,也不一定是最好的。不过,希望你学到了一些东西!