6

Rails 3.2.13 アプリ (Ruby 2.0.0 + Heroku 上の Postgres) では、API から大量の注文データを取得することが多く、データベース内の各注文を更新または作成する必要があります。協会。1 回の注文で、それ自体が作成/更新されます。10 ~ 15 個の関連オブジェクトがあり、一度に最大 500 個の注文をインポートしています。

以下のコードは機能しますが、問題は速度の点でまったく効率的ではないことです。500 件のレコードを作成/更新するには、約 10 分かかります。1 分で 6500 以上の db クエリが生成されます!

def add_details(shop, shopify_orders)
  shopify_orders.each do |shopify_order|
    order = Order.where(:order_id => shopify_order.id.to_s, :shop_id => shop.id).first_or_create
    order.update_details(order,shopify_order,shop)  #This calls update_attributes for the Order
    ShippingLine.add_details(order, shopify_order.shipping_lines)
    LineItem.add_details(order, shopify_order.line_items)
    Taxline.add_details(order, shopify_order.tax_lines)
    Fulfillment.add_details(order, shopify_order.fulfillments)
    Note.add_details(order, shopify_order.note_attributes)
    Discount.add_details(order, shopify_order.discount_codes)
    billing_address = shopify_order.billing_address rescue nil
    if !billing_address.blank?
      BillingAddress.add_details(order, billing_address)
    end
    shipping_address = shopify_order.shipping_address rescue nil
    if !shipping_address.blank?
      ShippingAddress.add_details(order, shipping_address)
    end
    payment_details = shopify_order.payment_details rescue nil
    if !payment_details.blank?
      PaymentDetail.add_details(order, payment_details)
    end
  end
end

  def update_details(order,shopify_order,shop)
    order.update_attributes(
      :order_name => shopify_order.name,
      :order_created_at => shopify_order.created_at,
      :order_updated_at => shopify_order.updated_at,
      :status => Order.get_status(shopify_order),
      :payment_status => shopify_order.financial_status,
      :fulfillment_status => Order.get_fulfillment_status(shopify_order),
      :payment_method => shopify_order.processing_method,
      :gateway => shopify_order.gateway,
      :currency => shopify_order.currency,
      :subtotal_price => shopify_order.subtotal_price,
      :subtotal_tax => shopify_order.total_tax,
      :total_discounts => shopify_order.total_discounts,
      :total_line_items_price => shopify_order.total_line_items_price,
      :total_price => shopify_order.total_price,
      :total_tax => shopify_order.total_tax,
      :total_weight => shopify_order.total_weight,
      :taxes_included => shopify_order.taxes_included,
      :shop_id => shop.id,
      :email => shopify_order.email,
      :order_note => shopify_order.note
    )
  end

ご覧のとおり、各注文をループして、存在するかどうかを確認し (次に、既存の注文をロードするか、新しい注文を作成します)、update_attributes を呼び出して注文の詳細を渡します。その後、各関連付けを作成または更新します。関連付けられた各モデルは、次のように非常によく似ています。

  class << self
    def add_details(order, tax_lines)
      tax_lines.each do |shopify_tax_line|
        taxline = Taxline.find_or_create_by_order_id(:order_id => order.id)
        taxline.update_details(shopify_tax_line)
      end
    end
  end
  def update_details(tax_line)
    self.update_attributes(:price => tax_line.price, :rate => tax_line.rate, :title => tax_line.title)
  end

私はactiverecord-import gemを調べましたが、残念ながら、必要に応じて更新するのではなく、レコードを一括で作成することを目的としているようです。

これをパフォーマンスのために改善できる最善の方法は何ですか?

よろしくお願いします。

アップデート:

このわずかな改善を思いつきました。これにより、新しく作成された注文を更新するための呼び出しが本質的に削除されます (注文ごとに 1 つのクエリが少なくなります)。

 def add_details(shop, shopify_orders)
      shopify_orders.each do |shopify_order|
      values = {:order_id => shopify_order.id.to_s, :shop_id => shop.id,
        :order_name => shopify_order.name,
            :order_created_at => shopify_order.created_at,
            :order_updated_at => shopify_order.updated_at,
            :status => Order.get_status(shopify_order),
            :payment_status => shopify_order.financial_status,
            :fulfillment_status => Order.get_fulfillment_status(shopify_order),
            :payment_method => shopify_order.processing_method,
            :gateway => shopify_order.gateway,
            :currency => shopify_order.currency,
            :subtotal_price => shopify_order.subtotal_price,
            :subtotal_tax => shopify_order.total_tax,
            :total_discounts => shopify_order.total_discounts,
            :total_line_items_price => shopify_order.total_line_items_price,
            :total_price => shopify_order.total_price,
            :total_tax => shopify_order.total_tax,
            :total_weight => shopify_order.total_weight,
            :taxes_included => shopify_order.taxes_included,
            :email => shopify_order.email,
            :order_note => shopify_order.note}
        get_order = Order.where(:order_id => shopify_order.id.to_s, :shop_id => shop.id)
        if get_order.blank?
            order = Order.create(values)
        else
        order = get_order.first  
            order.update_attributes(values)
        end
        ShippingLine.add_details(order, shopify_order.shipping_lines)
        LineItem.add_details(order, shopify_order.line_items)
        Taxline.add_details(order, shopify_order.tax_lines)
        Fulfillment.add_details(order, shopify_order.fulfillments)
        Note.add_details(order, shopify_order.note_attributes)
        Discount.add_details(order, shopify_order.discount_codes)
        billing_address = shopify_order.billing_address rescue nil
        if !billing_address.blank?
          BillingAddress.add_details(order, billing_address)
        end
        shipping_address = shopify_order.shipping_address rescue nil
        if !shipping_address.blank?
          ShippingAddress.add_details(order, shipping_address)
        end
        payment_details = shopify_order.payment_details rescue nil
        if !payment_details.blank?
          PaymentDetail.add_details(order, payment_details)
        end
      end
 end

関連するオブジェクトの場合:

  class << self
    def add_details(order, tax_lines)
      tax_lines.each do |shopify_tax_line|
        values = {:order_id => order.id,
            :price => tax_line.price,
            :rate => tax_line.rate,
            :title => tax_line.title}
        get_taxline = Taxline.where(:order_id => order.id)
        if get_taxline.blank?
            taxline = Taxline.create(values)
        else
            taxline = get_taxline.first  
            taxline.update_attributes(values)
        end
      end
    end
  end

より良い提案はありますか?

4

3 に答える 3

7

コード全体を 1 つのデータベース トランザクションにラップしてみてください。Heroku を使用しているため、Postgres のボトムエンドになります。多くの update ステートメントを使用すると、それらを一度に処理することでおそらく大きなメリットが得られるため、コードはより速く実行され、基本的にサーバーがそれらをデキューできるため、Postgres 側で実行する 6500 個のステートメントの「キュー」を残すだけです。ボトムエンドによっては、トランザクションをより小さなチャンクに分割する必要がある場合がありますが、一度に 100 個のトランザクションを実行しても (その後、トランザクションをクローズして再度オープンすると)、Pg へのスループットが大幅に向上します。

http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html http://www.postgresql.org/docs/9.2/static/sql-set-transaction.html

したがって、2 行目の前に次のようなものを追加します。

def add_details(shop, shopify_orders)
  Order.transaction do
    shopify_orders.each do |shopify_order|

そして、メソッドの最後に別の端を追加します。

      if !payment_details.blank?
        PaymentDetail.add_details(order, payment_details)
      end
    end //shopify_orders.each..
  end //Order.transaction..
end //method
于 2013-09-26T20:14:45.200 に答える
1

次のように ActiveRecord にモンキー パッチを適用できます。

class ActiveRecord::Base

  #http://stackoverflow.com/questions/15317837/bulk-insert-records-into-active-record-table?lq=1
  #https://gist.github.com/jackrg/76ade1724bd816292e4e
  #  "UPDATE THIS SET <list_of_column_assignments>  FROM <table_name> THIS  JOIN (VALUES (<csv1>, <csv2>,...) VALS ( <column_names> ) ON <list_of_primary_keys_comparison>"
  def self.bulk_update(record_list)
      pk = self.primary_key
      raise "primary_key not found" unless pk.present?

      raise "record_list not an Array of Hashes" unless record_list.is_a?(Array) && record_list.all? {|rec| rec.is_a? Hash }
      return nil if record_list.empty?

      result = nil

      #test if every hash has primary keys, so we can JOIN
      record_list.each { |r|  raise "Primary Keys '#{self.primary_key.to_s}' not found on record: #{r}" unless hasAllPKs?(r) }


      #list of primary keys comparison
      pk_comparison_array = []
      if (pk).is_a?(Array)
          pk.each {|thiskey| pk_comparison_array << "THIS.#{thiskey} = VALS.#{thiskey}" }
      else
          pk_comparison_array << "THIS.#{pk} = VALS.#{pk}"
      end
      pk_comparison = pk_comparison_array.join(' AND ')

      #SQL
      (1..record_list.count).step(1000).each do |start|
        key_list, value_list = convert_record_list(record_list[start-1..start+999])
        #csv values
        csv_vals = value_list.map {|v| "(#{v.join(", ")})" }.join(", ")
        #column names
        column_names = key_list.join(", ")
        #list of columns assignments
        columns_assign_array = []
        key_list.each {|col|
          unless inPK?(col)
            columns_assign_array << "THIS.#{col} = VALS.#{col}"
          end }
        columns_assign = columns_assign_array.join(', ')

        sql = "UPDATE THIS SET #{columns_assign}  FROM #{self.table_name} THIS  JOIN ( VALUES #{csv_vals} ) VALS ( #{column_names} ) ON ( #{pk_comparison} )"
        result = self.connection.execute(sql)

        return result if result<0
      end

      return result

  end

  def self.inPK?(str)
      pk = self.primary_key

      test = str.to_s
      if pk.is_a?(Array)
            (pk.include?(test))
      else
            (pk==test)
      end
  end

  #test if given hash has primary keys included as hash keys and those keys are not empty
  def self.hasAllPKs?(hash)
      h = hash.stringify_keys
      pk = self.primary_key

      if pk.is_a?(Array)
           (pk.all? {|k| h.key?(k) and h[k].present? })
      else
           h.key?(pk) and h[pk].present?
      end
  end

  def self.convert_record_list(record_list)
    # Build the list of keys
    key_list = record_list.map(&:keys).flatten.map(&:to_s).uniq.sort

    value_list = record_list.map do |rec|
      list = []
      key_list.each {|key| list <<  ActiveRecord::Base.connection.quote(rec[key] || rec[key.to_sym]) }
      list
    end

    # If table has standard timestamps and they're not in the record list then add them to the record list
    time = ActiveRecord::Base.connection.quote(Time.now)
    for field_name in %w(created_at updated_at)
      if self.column_names.include?(field_name) && !(key_list.include?(field_name))
        key_list << field_name
        value_list.each {|rec| rec << time }
      end
    end

    return [key_list, value_list]
  end
end

次に、モデルの属性 (主キーを含む) を含むハッシュの配列を生成し、次のようにすることができます。

ActiveRecord::Base.transaction do
   Model.bulk_update [ {attr1: val1, attr2: val2,...},  {attr1: val1, attr2: val2,...},   ... ]
end

これは、Rails のコールバックと検証のない単一の SQL コマンドになります。

于 2014-08-21T15:20:48.603 に答える
0

PostgreSQL の場合、上記のアプローチでは対処できない問題がいくつかあります。

  1. 更新対象テーブルには、エイリアスだけでなく、実際のテーブルを指定する必要があります。
  2. FROM 句でターゲット テーブルを繰り返すことはできません。ターゲット テーブルを VALUES テーブルに結合しているため (FROM 句にはテーブルが 1 つしかないため、JOIN を使用できず、代わりに "WHERE " を使用する必要があります。
  3. 単純な「UPDATE」コマンドで行うのと同じ「無料」キャストを VALUES テーブルで取得できないため、日付/タイムスタンプ値をそのままキャストする必要があります (#val_cast がこれを行います)。

    class ActiveRecord::Base
    
      def self.update!(record_list)
        raise ArgumentError "record_list not an Array of Hashes" unless record_list.is_a?(Array) && record_list.all? {|rec| rec.is_a? Hash }
        return record_list if record_list.empty?
    
        (1..record_list.count).step(1000).each do |start|
          field_list, value_list = convert_record_list(record_list[start-1..start+999])
          key_field = self.primary_key
          non_key_fields = field_list - [%Q["#{self.primary_key}"], %Q["created_at"]]
          columns_assign = non_key_fields.map {|field| "#{field} = #{val_cast(field)}"}.join(",")
          value_table = value_list.map {|row| "(#{row.join(", ")})" }.join(", ")
          sql = "UPDATE #{table_name} AS this SET #{columns_assign} FROM (VALUES #{value_table}) vals (#{field_list.join(", ")}) WHERE this.#{key_field} = vals.#{key_field}"
          self.connection.update_sql(sql)
        end
    
        return record_list
      end
    
      def self.val_cast(field)
        field = field.gsub('"', '')
        if (column = columns.find{|c| c.name == field }).sql_type =~ /time|date/
          "cast (vals.#{field} as #{column.sql_type})"
        else
          "vals.#{field}"
        end
      end
    
      def self.convert_record_list(record_list)
        # Build the list of fields
        field_list = record_list.map(&:keys).flatten.map(&:to_s).uniq.sort
    
        value_list = record_list.map do |rec|
          list = []
          field_list.each {|field| list <<  ActiveRecord::Base.connection.quote(rec[field] || rec[field.to_sym]) }
          list
        end
    
        # If table has standard timestamps and they're not in the record list then add them to the record list
        time = ActiveRecord::Base.connection.quote(Time.now)
        for field_name in %w(created_at updated_at)
          if self.column_names.include?(field_name) && !(field_list.include?(field_name))
            field_list << field_name
            value_list.each {|rec| rec << time }
          end
        end
    
        field_list.map! {|field| %Q["#{field}"] }
    
        return [field_list, value_list]
      end
    end
    
于 2014-11-22T19:01:53.243 に答える