Mongoid 批量更新/插入的替代方案是什么?

7
我知道Mongoid v3+支持通过Model.collection.insert()进行批量插入。但是,我不认为它支持批量更新,其中每个记录的属性都不同(因此我不认为update_all会起作用)。有没有一种方法可以批量更新/插入而不是单个记录查找和更新?
这是一个简化的示例,其中我有2个模型:
class Product
  ...
  has_and_belongs_to_many :lists
end

class List
  ...
  has_and_belongs_to_many :products
end

当创建一个新的Product时,我会将其与一个或多个List相关联。然而,我还需要每天更新Product属性,而不会丢失List参考信息(我可以接受在Product上的验证不运行)。
一种方法是,在不使用批处理的情况下,在Product上调用find_or_initialize_by并更新属性。但对于10K-1M+条记录来说,这样做非常耗时。
另一种使用批量插入的方法是执行Product.delete_all,然后执行Product.collection.insert(...),但这会创建新的product_ids,并且与List的关系不再维护。
在这个例子中是否有一种批量更新或upsert的方法?

你能分享一下你的文档模式吗?虽然我不太常使用mongoid嵌入式文档,但如果我对你的文档结构和你想要实现的目标有一个比较清晰的了解,我可能能够通过moped(mongoid驱动程序)来帮助你。 - amit_saxena
我猜这不一定是一个嵌入式文档的特定问题。如果关系是“has_many/belongs_to”,那么使用moped的解决方案是否存在? - netwire
如果您能编辑您的问题并让我知道实际模式(文档的外观)以及您在数据方面想要实现什么,那么我可能能够提供帮助。关系(has_many/belongs_to)是由mongoid构建的抽象,而不是mongo固有的。 - amit_saxena
2个回答

11

MongoDB 2.6支持update命令,请参见http://docs.mongodb.org/manual/reference/command/update/

Mongoid.default_session使您在MongoDB数据库级别上获得了对command方法的相应访问。 以下示例显示如何构造和发出批量更新命令以回答您的问题。 输出显示了10个列表, 单个与批量更新运行时间的比较, Product集合的包含更新后库存计数的转储, 以及批量更新命令的结构。 即使在这个简单的演示中,批量更新与单个更新相比性能有了显着提高。

Ruby 'mongo' 1.x驱动程序支持用于批量写入的流畅API, 新的Ruby 'mongo' 2.0驱动程序也将支持此功能,最终将进入Mongoid。 Mongoid v3+目前使用的是Moped驱动程序,它没有用于批量写入的流畅API。 但正如本文所示, 通过Session#command方法,在Mongoid/Moped中构建并直接发出批量写(批量更新)命令是相当简单的。

希望这可以帮助您。

app/models/product.rb

class Product
  include Mongoid::Document
  field :name, type: String
  field :stock, type: Integer, default: 0
  has_and_belongs_to_many :lists, index: true
end

应用程序/模型/列表.rb

class List
  include Mongoid::Document
  field :name, type: String
  has_and_belongs_to_many :products
end

测试/单元/product_test.rb

require 'test_helper'
require 'benchmark'
require 'pp'

class ProductTest < ActiveSupport::TestCase
  def setup
    @session = Mongoid.default_session
    @session.drop
  end
  test '0. mongoid version' do
    puts "\nMongoid::VERSION:#{Mongoid::VERSION}\nMoped::VERSION:#{Moped::VERSION}"
  end
  def individual_updates(list_ids, repetitions)
    repetitions.times do
      list_ids.each_with_index do |list_id, index|
        Product.where(:list_ids => list_id).update_all({'$inc' => {'stock' => index}})
      end
    end
  end
  def bulk_command(list_ids, repetitions)
    updates = []
    repetitions.times do
      list_ids.each_with_index do |list_id, index|
        updates << {'q' => {'list_ids' => list_id}, 'u' => {'$inc' => {'stock' => index}}, 'multi' => true}
      end
    end
    { update: Product.collection_name.to_s, updates: updates, ordered: false }
  end
  def bulk_updates(list_ids, repetitions)
    @session.command(bulk_command(list_ids, repetitions))
  end
  test 'x' do
    puts
    [
      ['ASUS MeMO Pad FHD 10', ['ASUS', 'tablet', 'Android']],
      ['Apple iPad Air Wi-Fi + Cellular 128GB - Silver', ['Apple', 'tablet', 'iOS']],
      ['Apple iPad mini with Retina display Wi-Fi + Cellular 128GB - Silver', ['Apple', 'tablet', 'iOS']],
      ['Apple iPhone 5c 32GB Green', ['Apple', 'phone', 'iOS']],
      ['Apple iPhone 5s 64GB Space Gray', ['Apple', 'phone', 'iOS']],
      ['LG G Pad 8.3 Tablet', ['LG', 'tablet', 'Android']],
      ['LG Google Nexus 5 White', ['LG', 'phone', 'Android']],
      ['Microsoft Surface 7ZR-00001', ['Microsoft', 'tablet', 'Windows 8 RT']],
      ['Samsung Galaxy S4 I9500', ['Samsung', 'phone', 'Android']],
      ['Samsung Galaxy Tab S 8.4', ['Samsung', 'tablet', 'Android']]
    ] .each do |product_name, list_names|
      product = Product.create(name: product_name)
      list_names.each do |list_name|
        list = List.where(name: list_name).first_or_create
        list.products << product
      end
    end
    list_names = List.all.to_a.collect(&:name).sort.uniq
    p list_names
    list_ids = list_names.collect{|list_name| List.where(name: list_name).first.id}
    assert(list_ids.count > 0)

    Benchmark.bm(20) do |x|
      x.report('individual updates') { individual_updates(list_ids, 100) }
      x.report('bulk updates') { bulk_updates(list_ids, 100) }
    end

    pp Product.all.to_a

    db_command = bulk_command(list_ids, 1)
    assert(db_command[:updates].size > 0)
    pp db_command
  end
end

rake test

Run options:

# Running tests:

[1/2] ProductTest#test_0._mongoid_version
Mongoid::VERSION:3.1.6
Moped::VERSION:1.5.2
[2/2] ProductTest#test_x
["ASUS", "Android", "Apple", "LG", "Microsoft", "Samsung", "Windows 8 RT", "iOS", "phone", "tablet"]
                           user     system      total        real
individual updates     0.420000   0.070000   0.490000 (  0.489579)
bulk updates           0.060000   0.000000   0.060000 (  0.180926)
[#<Product _id: 5408b72b7f11bad1ca000001, name: "ASUS MeMO Pad FHD 10", stock: 2000, list_ids: ["5408b72c7f11bad1ca000002", "5408b72c7f11bad1ca000003", "5408b72c7f11bad1ca000004"]>,
 #<Product _id: 5408b72c7f11bad1ca000005, name: "Apple iPad Air Wi-Fi + Cellular 128GB - Silver", stock: 3600, list_ids: ["5408b72c7f11bad1ca000006", "5408b72c7f11bad1ca000003", "5408b72c7f11bad1ca000007"]>,
 #<Product _id: 5408b72c7f11bad1ca000008, name: "Apple iPad mini with Retina display Wi-Fi + Cellular 128GB - Silver", stock: 3600, list_ids: ["5408b72c7f11bad1ca000006", "5408b72c7f11bad1ca000003", "5408b72c7f11bad1ca000007"]>,
 #<Product _id: 5408b72c7f11bad1ca000009, name: "Apple iPhone 5c 32GB Green", stock: 3400, list_ids: ["5408b72c7f11bad1ca000006", "5408b72c7f11bad1ca00000a", "5408b72c7f11bad1ca000007"]>,
 #<Product _id: 5408b72c7f11bad1ca00000b, name: "Apple iPhone 5s 64GB Space Gray", stock: 3400, list_ids: ["5408b72c7f11bad1ca000006", "5408b72c7f11bad1ca00000a", "5408b72c7f11bad1ca000007"]>,
 #<Product _id: 5408b72c7f11bad1ca00000c, name: "LG G Pad 8.3 Tablet", stock: 2600, list_ids: ["5408b72c7f11bad1ca00000d", "5408b72c7f11bad1ca000003", "5408b72c7f11bad1ca000004"]>,
 #<Product _id: 5408b72c7f11bad1ca00000e, name: "LG Google Nexus 5 White", stock: 2400, list_ids: ["5408b72c7f11bad1ca00000d", "5408b72c7f11bad1ca00000a", "5408b72c7f11bad1ca000004"]>,
 #<Product _id: 5408b72c7f11bad1ca00000f, name: "Microsoft Surface 7ZR-00001", stock: 3800, list_ids: ["5408b72c7f11bad1ca000010", "5408b72c7f11bad1ca000003", "5408b72c7f11bad1ca000011"]>,
 #<Product _id: 5408b72c7f11bad1ca000012, name: "Samsung Galaxy S4 I9500", stock: 2800, list_ids: ["5408b72c7f11bad1ca000013", "5408b72c7f11bad1ca00000a", "5408b72c7f11bad1ca000004"]>,
 #<Product _id: 5408b72c7f11bad1ca000014, name: "Samsung Galaxy Tab S 8.4", stock: 3000, list_ids: ["5408b72c7f11bad1ca000013", "5408b72c7f11bad1ca000003", "5408b72c7f11bad1ca000004"]>]
{:update=>"products",
 :updates=>
  [{"q"=>{"list_ids"=>"5408b72c7f11bad1ca000002"},
    "u"=>{"$inc"=>{"stock"=>0}},
    "multi"=>true},
   {"q"=>{"list_ids"=>"5408b72c7f11bad1ca000004"},
    "u"=>{"$inc"=>{"stock"=>1}},
    "multi"=>true},
   {"q"=>{"list_ids"=>"5408b72c7f11bad1ca000006"},
    "u"=>{"$inc"=>{"stock"=>2}},
    "multi"=>true},
   {"q"=>{"list_ids"=>"5408b72c7f11bad1ca00000d"},
    "u"=>{"$inc"=>{"stock"=>3}},
    "multi"=>true},
   {"q"=>{"list_ids"=>"5408b72c7f11bad1ca000010"},
    "u"=>{"$inc"=>{"stock"=>4}},
    "multi"=>true},
   {"q"=>{"list_ids"=>"5408b72c7f11bad1ca000013"},
    "u"=>{"$inc"=>{"stock"=>5}},
    "multi"=>true},
   {"q"=>{"list_ids"=>"5408b72c7f11bad1ca000011"},
    "u"=>{"$inc"=>{"stock"=>6}},
    "multi"=>true},
   {"q"=>{"list_ids"=>"5408b72c7f11bad1ca000007"},
    "u"=>{"$inc"=>{"stock"=>7}},
    "multi"=>true},
   {"q"=>{"list_ids"=>"5408b72c7f11bad1ca00000a"},
    "u"=>{"$inc"=>{"stock"=>8}},
    "multi"=>true},
   {"q"=>{"list_ids"=>"5408b72c7f11bad1ca000003"},
    "u"=>{"$inc"=>{"stock"=>9}},
    "multi"=>true}],
 :ordered=>false}
Finished tests in 1.334821s, 1.4983 tests/s, 1.4983 assertions/s.
2 tests, 2 assertions, 0 failures, 0 errors, 0 skips

请注意,BSON :: ObjectId对象在检查时返回字符串,但实际上值是BSON :: ObjectID的实例,而不是字符串。 - Gary Murakami
谢谢Gary,让我试试然后回来。 - netwire
没有批量 upsert 命令,对吧?我在考虑通过这个流程模拟批量 upsert,你觉得怎么样?流程如下:1. 使用 find_or_initialize_by 在内存中创建模型,2. 使用批量更新进行保存。 - netwire
抱歉,为了澄清一下,我尝试了这个代码:products.each do |product| updates << {'q' => {'external_id' => product.external_id}, 'u' => product.as_document, 'multi' => false, 'upsert' => true} end 然而,在更新时会出现问题,因为文档的 _id 已经存在且无法更改。有没有办法使用 as_document 但不包括 _id,或者我需要手动创建哈希表? - netwire
db.collection.update() 是Mongo shell用户界面方法,用于在MongoDB中进行更新(http://docs.mongodb.org/manual/reference/method/db.collection.update/)。这对应于支持批量更新的2.6服务器上的“新”更新命令(http://docs.mongodb.org/manual/reference/command/update/),或者是单个请求的2.4或更早版本服务器上的“旧”OP_UPDATE wire协议操作(http://docs.mongodb.org/meta-driver/latest/legacy/mongodb-wire-protocol/)。 - Gary Murakami
显示剩余12条评论

1

可能Gary的答案有效。但是我发现在使用标准mongodb ruby驱动程序的mongoid 5.x和6.x中,可以使用Model.mongo_client.bulk_write([op1, op2, ...], options)。请参见

需要这两个文档来理解如何构建调用。例如:

  update = TestCase.collection.bulk_write(
    [
      replace_one: {
        filter: {},
        update: {},
        upsert: true
      },
      update_one: { ... },
      ...
    ],
    ordered: false
  )

1
谢谢,我一直在尝试使用驼峰操作符updateOne,直到你让我意识到驱动程序将它们转换成了蛇形大小写。 - Cyril Duchon-Doris
这个答案是可行的,同时在过滤器中,你必须使用'a.b.c' => 1的表示法,而不是{a: {b: {c: 1}}} - Shiyason
@Shiyason,我认为这个差异并不是针对这个操作特定的,而是取决于当你使用标准的Ruby驱动时想要实现什么目的。 - akostadinov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接