【问题标题】:Rails: use existing model validation rules against a collection instead of the database tableRails:对集合而不是数据库表使用现有的模型验证规则
【发布时间】:2014-01-27 12:20:12
【问题描述】:

Rails 4,Mongoid 而不是 ActiveRecord(但是为了这个问题,这应该改变任何东西)。

假设我有一个带有一些验证规则的 MyModel 域类:

class MyModel
  include Mongoid::Document

  field :text, type: String
  field :type, type: String

  belongs_to :parent

  validates :text, presence: true
  validates :type, inclusion: %w(A B C)
  validates_uniqueness_of :text, scope: :parent # important validation rule for the purpose of the question
end

其中Parent 是另一个域类:

class Parent
    include Mongoid::Document

    field :name, type: String

    has_many my_models
end

此外,我在数据库中的相关表中填充了一些有效数据。

现在,我想从 CSV 文件中导入一些数据,这可能与数据库中的现有数据发生冲突。简单的做法是为 CSV 中的每一行创建一个 MyModel 实例并验证它是否有效,然后将其保存到数据库中(或丢弃它)。

类似这样的:

csv_rows.each |data| # simplified 
  my_model = MyModel.new(data) # data is the hash with the values taken from the CSV row

  if my_model.valid?
    my_model.save validate: false
  else
    # do something useful, but not interesting for the question's purpose
    # just know that I need to separate validation from saving
  end
end

现在,这对于有限的数据量来说非常顺利。但是当 CSV 包含数十万行时,这会变得很慢,因为(最坏的情况)每一行都有一个写操作。

我想做的是存储有效项目的列表并在文件解析过程结束时将它们全部保存。所以,没什么复杂的:

valids = []
csv_rows.each |data|
  my_model = MyModel.new(data)

  if my_model.valid?  # THE INTERESTING LINE this "if" checks only against the database, what happens if it conflicts with some other my_models not saved yet?
    valids << my_model
  else
    # ...
  end
end

if valids.size > 0
  # bulk insert of all data
end

如果我能确定 CSV 中的数据不包含重复行或违反 MyModel 验证规则的数据,那将是完美的。


我的问题是:如何根据数据库和valids 数组检查每一行,而不必重复MyModel 中定义的验证规则(避免重复)?

我没有考虑其他(更有效)的方法吗?

【问题讨论】:

标签: ruby-on-rails validation ruby-on-rails-4 mongoid bulkinsert


【解决方案1】:

您可以做的是验证模型,将属性保存在哈希中,推送到 valids 数组,然后使用 mongodb 的 insert 批量插入值:

valids = []
csv_rows.each |data|
  my_model = MyModel.new(data)

  if my_model.valid?
    valids << my_model.attributes
  end
end

MyModel.collection.insert(valids, continue_on_error: true)

但这不会阻止新的重复...因为您可以使用哈希和复合键执行以下操作:

valids = {}
csv_rows.each |data|
  my_model = MyModel.new(data)

  if my_model.valid?
    valids["#{my_model.text}_#{my_model.parent}"] = my_model.as_document
  end
end

那么以下任何一个都可以工作,DB Agnostic:

MyModel.create(valids.values)

或 MongoDB'ish:

MyModel.collection.insert(valids.values, continue_on_error: true)

甚至更好

确保您在集合上有一个 uniq 索引:

class MyModel
  ...
  index({ text: 1, parent: 1 }, { unique: true, dropDups: true })
  ...
end

然后只需执行以下操作:

MyModel.collection.insert(csv_rows, continue_on_error: true)

http://api.mongodb.org/ruby/current/Mongo/Collection.html#insert-instance_method http://mongoid.org/en/mongoid/docs/indexing.html

提示:如果您预计有数千行以大约 500 条左右的批次执行此操作,我建议您这样做。

【讨论】:

  • 这一切听起来都非常好。 AFAYK,有没有办法跟踪MyModel 的哪些项目由于错误而被跳过?
  • 是的,通过添加以下参数collect_on_error: true,它将返回错误::collect_on_error (Boolean) — default: +false+ — if true, then collects invalid documents as an array. Note that this option changes the result format. - 通过文档api.mongodb.org/ruby/current/Mongo/…
  • 这种方法很有趣,即使对于数据库选择来说有点太紧了。如果我更改了数据库,我需要解决这个问题。记住这一点,这可能是一个很好的解决方案。如果有人提出更多应用程序级的解决方案,我会再等一会儿,否则我会接受你的回答。谢谢队友:)
  • 这是真的!关于验证和批量插入,我发现我总是不得不深入到数据库层才能获得最佳性能。然后你必须问自己......独特性重要吗?尤其是当您可以执行“组”之类的操作时,或者按时间戳(或其他任何方式等)排序的限制 1 时?但我也很想看到其他答案。我 100% 同意您的观点,即最好采用更解耦的方法。
  • 根据您的 cmets 重新考虑我的答案。更新了第二个选项,使其与数据库无关...使用attributes 方法而不是as_document 并使用Model.create 作为替代方法。
猜你喜欢
  • 2013-03-29
  • 1970-01-01
  • 1970-01-01
  • 2015-05-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多