我试图在Rails设置中建立一些关系,并且对如何使用我配置的那些感到困惑。
我的情况是这样的:
我有一个名为Coaster
的模型。我希望每个过山车能够拥有0个或更多版本。我希望能够从它的实例中找到所有版本的过山车,也可以反过来。
我的模特和关系:
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数据文件导入Coasters时发生的代码:
# 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。
我只需要确保我真的得到了正确的结果。
任何评论都会受到高度赞赏,因为我已经多年来一直在研究这个问题并且没有走得太远。
只是因为任何人都会回复告诉我要看版本宝石。我最初走这条路线很棒,但问题是在我的情况下,过山车和过山车的版本都是彼此重要的,我不能做Coaster.all来得到所有过山车他们是否是版本与否。同一条线上的其他问题也出现了。
由于
答案 0 :(得分:1)
首先,欢迎来到精彩的历史追踪世界!正如您所发现的那样,跟踪关系数据库中数据的变化实际上并不容易。虽然有绝对的宝石可以跟踪历史记录以进行审计(例如auditable
),但听起来您希望您的历史记录仍然是一等公民。所以,让我首先分析您当前方法的问题,然后我提出一个更简单的解决方案,可以让您的生活更轻松。
没有特别的顺序,以下是您当前系统的一些难点:
必须维护is_latest
列,并且存在不同步的风险。你可能不会在测试中看到这一点,但在生产中,大规模,这是一个非常有效的风险。
您的incarnations
表创建了一个具有多个子版本的one-master-version版本,除了(类似于is_latest)版本的排序由{控制}之外,这很好。 {1}}列再次需要维护并且存在错误风险。您的导入脚本目前似乎无法设置它。
version_order
关系使得很难说B是A的版本;你可以通过一些更多的关系解决这个问题,但这也会使你的代码变得更复杂。
复杂性。很难跟踪历史记录的跟踪方式,正如您所发现的那样,很难管理插入新版本的细节(A和B都应该同意他们有2个版本,对吗?因为他们是同一个过山车?)
现在,我认为您的数据模型在技术上仍然有效 - 我认为您遇到的问题是您的脚本问题。但是,使用更简单的数据模型,您的脚本可以变得更简单,因此不容易出错。我只能使用一个表来实现这一目标:
incarnations
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
列可用作superseded_at
检查和订购版本的方式(尽管如下,我只是按is_latest
排序以简化)。有了这个结构,这就是我的Coaster类:
id
这使得添加新记录变得简单:
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
当然,它仍然是复杂的代码,使得更简单。我的方法肯定不是唯一的,也不一定是最好的。希望你能学到一些东西!