【问题标题】:How to eager loading in a method called from rake task如何急切加载从 rake 任务调用的方法
【发布时间】:2015-02-05 18:16:39
【问题描述】:

我有这样的任务:

namespace :company do
  task :update, [:code] => :environment do |t, args|
    company = Company.find_or_create_by(code: args[:code])
    company.update_from_local_data
  end
end

这是Company 类。

class Company < ActiveRecord::Base
  has_many :items
  def update_from_local_data
    data = YAML.load(File.read(ENV['COMPANY_DATA_FILE']))
    update_items(data)
  end
  def update_items(item_array)
    item_array.each do |value|
      item = items.find_or_initialize_by(name: value[:name])
      item.update_attributes(value)
    end
  end
end

我确认此代码有很多SELECT SQL 查询。

在控制器中我可以处理它,但我如何从 rake 任务中使用预先加载?

编辑

感谢 Uri 的 cmets 我了解了如何提高将多个数据保存到数据库的性能,但我仍然无法为多个项目调用 find_or_initialize_by

我找到ActiveRecord.import:on_duplicate_key_update 选项,但它只能与MySQL 一起使用,而我正在使用PostgreSQL

编辑 2

为了解释问题是什么,我创建了一个example project

这是Company#update_from_local_data 的结果。 我不想SELECT 查询每个Items。

我怎样才能更有效地编写它?

c = Company.first
c.update_from_local_data
  Item Load (0.2ms)  SELECT  "items".* FROM "items"  WHERE "items"."company_id" = ? AND "items"."name" = 'item0' LIMIT 1  [["company_id", 1]]
   (0.1ms)  begin transaction
   (0.0ms)  commit transaction
  Item Load (0.1ms)  SELECT  "items".* FROM "items"  WHERE "items"."company_id" = ? AND "items"."name" = 'item1' LIMIT 1  [["company_id", 1]]
   (0.0ms)  begin transaction
   (0.0ms)  commit transaction
  Item Load (0.1ms)  SELECT  "items".* FROM "items"  WHERE "items"."company_id" = ? AND "items"."name" = 'item2' LIMIT 1  [["company_id", 1]]
   (0.1ms)  begin transaction
   (0.0ms)  commit transaction
  Item Load (0.1ms)  SELECT  "items".* FROM "items"  WHERE "items"."company_id" = ? AND "items"."name" = 'item3' LIMIT 1  [["company_id", 1]]
   (0.0ms)  begin transaction
   (0.0ms)  commit transaction
  Item Load (0.1ms)  SELECT  "items".* FROM "items"  WHERE "items"."company_id" = ? AND "items"."name" = 'item4' LIMIT 1  [["company_id", 1]]
   (0.0ms)  begin transaction
   (0.0ms)  commit transaction
  Item Load (0.1ms)  SELECT  "items".* FROM "items"  WHERE "items"."company_id" = ? AND "items"."name" = 'item5' LIMIT 1  [["company_id", 1]]
   (0.0ms)  begin transaction
   (0.0ms)  commit transaction
  Item Load (0.1ms)  SELECT  "items".* FROM "items"  WHERE "items"."company_id" = ? AND "items"."name" = 'item6' LIMIT 1  [["company_id", 1]]
   (0.0ms)  begin transaction
   (0.0ms)  commit transaction
  Item Load (0.1ms)  SELECT  "items".* FROM "items"  WHERE "items"."company_id" = ? AND "items"."name" = 'item7' LIMIT 1  [["company_id", 1]]
   (0.0ms)  begin transaction
   (0.0ms)  commit transaction
  Item Load (0.1ms)  SELECT  "items".* FROM "items"  WHERE "items"."company_id" = ? AND "items"."name" = 'item8' LIMIT 1  [["company_id", 1]]
   (0.0ms)  begin transaction
   (0.0ms)  commit transaction
  Item Load (0.1ms)  SELECT  "items".* FROM "items"  WHERE "items"."company_id" = ? AND "items"."name" = 'item9' LIMIT 1  [["company_id", 1]]
   (0.0ms)  begin transaction
   (0.0ms)  commit transaction
=> [{:name=>"item0"}, {:name=>"item1"}, {:name=>"item2"}, {:name=>"item3"}, {:name=>"item4"}, {:name=>"item5"}, {:name=>"item6"}, {:name=>"item7"}, {:name=>"item8"}, {:name=>"item9"}]

【问题讨论】:

标签: ruby-on-rails ruby eager-loading


【解决方案1】:

您说要加快 find_or_initialize_by 步骤。

def update_items( items ) 
  # 'items' is an array of attributes hashes

  ActiveRecord::Base.transaction do

    names_array = items.map{ |attributes| attributes[:name] }
    existing_records = Company.where(name: names_array)

    records_by_name = existing_record.each_with_object({}) do |record, hash|
      name = record.name
      hash[name] = record
    end

    items.each do |attributes|
      name = attributes[:name]
      record = records_by_name[name] || Company.new

      # with validations and callbacks:
      #   record.update_attributes(attributes)

      # without validations:
      #   attributes.each{ |k, v| record[k] = v }
      #   record.save(validate: false)


      # without validations or callbacks:
      #   If you're using an older version of Rails,
      #   you can use record.save(:update_without_callbacks)
      #   For recent versions, you'll need to either write SQL-
      #   or disable all callbacks with skip_callbacks and then re-enable-
      #   them with set_callbacks
    end
  end
end

基本上,您可以一次性找到所有现有记录,而不是为每个名称执行单独的搜索查询。

【讨论】:

    【解决方案2】:
    1. 您可以使用update_all,它将返回更新的条目数。如果更新了 0 个条目,则创建新记录

      def update_items(item_array)
        item_array.each do |value|
          entries_updated = items.where(name: value[:name]).update_all(value)
          if entries_updated == 0
            items.create!(value)
          end
      end
      

      请注意,如果create! 无法创建记录,它将引发错误。您可能希望仅使用 create 并自行处理验证错误。

    2. 根据您在聊天items = load_all(item_array); items.update_all 上建议的界面,您可以采取的另一种方法是

      def update_items(item_array)
        grouped = item_array.group_by {|i| i[:name] }
        items.where(name: grouped.keys).each do |item|
          data = grouped[item.name]
          item.assign_attributes(data)
          item.save! if item.changed? 
        end
      end
      

      如果不是所有项目都经常更改,这将减少查询,但如果Company 有数千个项目,则可能会很慢,但您可以将item_array 分成更小的组,然后执行该操作。请注意,无法生成一个更新语句来根据不同的条件更改多条记录。

    【讨论】:

    • 感谢您的回答,但 name 列具有独特的价值,不幸的是,您的代码在我的情况下并没有提高性能。
    • 是的,名称具有唯一值。所以将会发生的事情是Company.where.update_all 将始终只更新具有该给定名称的Company,与您执行item.update_attributes(value) 的行相遇。然后如果没有更新,它返回 0,意味着没有该名称的公司,因此它继续使用 value 中的数据创建此类公司。
    • 这样,代码将始终运行 1 或 2 个不需要从 db 加载信息的查询,因为 update_all 不从 db 加载数据,只运行 update,在您的情况下,它基于索引。您的代码将始终运行选择,然后加载或不加载数据,然后运行更新。
    • 我现在注意到,您的代码正在更新 Company 类,而我想更新或初始化 Item 类。我无法将您的答案调整为我的原始代码。我错过了什么吗?
    • 是的,你需要使用items,更新了答案。我提到的所有条件仍然适用。
    猜你喜欢
    • 2016-04-09
    • 2013-09-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-03-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多