Каков самый быстрый способ создания массовых ассоциаций HABTM в Rails?

У меня есть две таблицы с отношениями HABTM в Rails. Что-то вроде следующего:

class Foo &lt ActiveRecord::Base
  has_and_belongs_to_many :bars
end

class Bar &lt ActiveRecord::Base
  has_and_belongs_to_many :foos
end

Теперь у меня есть новый объект Foo и вы хотите массово назначить ему тысячи баров, которые я предварительно загрузил:

@foo = Foo.create
@bars = Bar.find_all_by_some_attribute(:a)

Какой самый быстрый способ сделать это? Я пробовал:

@foo.bars = @bars
@foo.bars &lt&lt @bars

И оба выполняются очень медленно, причем для каждого bar для записи bar требуется следующая запись:

bars_foos Столбцы (1.1ms) SHOW ПОЛЯ ОТ bars_foos SQL (0.6ms) INSERT INTO bars_foos (bar_id, foo_id) ЗНАЧЕНИЯ (100, 117200)

Я просмотрел ар-расширения, но функция import, похоже, не работает без модели (Model.import), которая исключает ее использование для таблицы соединений.

Нужно ли мне писать SQL, или у Rails более красивый способ?

Ответ 1

Я думаю, что наилучшим вариантом будет использование SQL, а массовая вставка нескольких строк для каждого запроса. Если вы можете создать инструкцию INSERT, которая делает что-то вроде:

INSERT INTO foos_bars (foo_id,bar_id) VALUES (1,1),(1,2),(1,3)....

Вы должны иметь возможность вставлять тысячи строк в один запрос. Я не пробовал ваш метод mass_habtm, но, похоже, вы могли бы что-то вроде:

bars = Bar.find_all_by_some_attribute(:a)
foo = Foo.create
values = bars.map {|bar| "(#{foo.id},#{bar.id})"}.join(",")
connection.execute("INSERT INTO foos_bars (foo_id, bar_id) VALUES #{values}")

Кроме того, если вы ищете панель "some_attribute", убедитесь, что вы указали это поле в своей базе данных.

Ответ 2

Вы все еще можете посмотреть activerecord-import. Правильно, что он не работает без модели, но вы можете создать модель только для импорта.

class FooBar < ActiveRecord::Base; end

FooBar.import [:foo_id, :bar_id], [[1,2], [1,3]]

Вы можете обернуть это в транзакцию, чтобы гарантировать полное заполнение HABTM, как здесь:

ActiveRecord::Base.transaction do
  imported_foo = Foo.import( foo_names, foo_values )
  imported_bar = Bar.import( bar_names, bar_values )
  FooBar.import( [:foo_id, :bar_id], imported_foo.ids.zip(imported_bar.ids)
end

Ответ 3

Это было быстрее, чем эквивалентный собственный код rails в 7 раз:

class &lt&lt Foo
  def mass_habtm(attr_array)
    attr_str = attr_array.map{|a| %Q{'#{a}'} }.uniq.join(",")
    self.connection.execute(%Q{insert into foos_bars (foo_id,bar_id) 
                     select distinct foos.id,bars.id from foos,bars 
                     where foos.id = #{self.id} 
                     and bars.some_attribute in (#{attr_str})})
  end
end

Мне кажется, что это достаточно простая операция, которая должна быть эффективно поддержана в Rails, я бы хотел услышать, есть ли у кого-то более чистый способ.

Я запускаю 2.2.2, , возможно, он более эффективно реализован в 3.x? и нашел то же самое в версии 3.0.2.

Ответ 4

Честно говоря, has_and_belongs_to_many - очень устаревший способ делать вещи. Вероятно, вам стоит взглянуть на has_many :through, что является новым способом создания таблиц соединений и уже довольно давно.

class Foo < ActiveRecord::Base
  has_many :foobars
  has_many :bars, :through => :foobars

  def add_many_bars(bars)
    bars.each do |bar|
      self.bars << bar
    end
  end
end

class Bar < ActiveRecord::Base
  has_many :foobars
  has_many :foos, :through => :foobars
end

class FooBar < ActiveRecord::Base
  belongs_to :foo
  belongs_to :bar
end

Кроме того, вы должны попробовать запустить то же самое в процессе производства и посмотреть, какую производительность вы получаете, поскольку в процессе производства происходит много кеширования, которое не обязательно происходит в разработке.