Лучший способ найти find_or_create_by_id, но обновить атрибуты, если запись найдена

Я ищу чистый способ создать запись с набором атрибутов, если запись не существует и - если запись существует - обновить ее атрибуты. Мне нравится синтаксис блока в вызове find_or_create_by_id. Вот мой код:

@categories = Highrise::DealCategory.find(:all)

@categories.each do |category|
  puts "Category: #{category.name}"

  Category.find_or_create_by_id(category.id) do |c|
    c.name = category.name
  end
end

Проблема здесь в том, что если запись существует, но имя изменилось, она не обновляется.

Ищете чистое решение этой проблемы...

6 ответов

Решение

Вы можете написать свой собственный метод:

class ActiveRecord::Base
  def self.find_by_id_or_create(id, &block)
    obj = self.find_by_id( id ) || self.new
    yield obj
    obj.save
  end
end

использование

 Category.find_by_id_or_create(10) do |c|
   c.name = "My new name"
 end

Конечно, таким образом вы должны расширить method missing метод и реализовать этот метод так же, как другие find_by_something методы. Но для краткости этого будет достаточно.

Я использовал этот паттен для семян:

Category.find_or_initialize_by(id: category.id).update |c|
  c.name = category.name
end

Он работает так же, как ответы Дейла Вийнанда и Теуласа (сохраняет экземпляр только один раз), но использует блок, как в вашем вопросе.

Я думаю, что самый простой способ - использовать метод Tap Ruby, например:

Category.find_or_initialize_by(id: category.id).tap do |c|
  c.name = category.name
  c.save
end

Также я изменил find_or_create_by_id к find_or_initialize_by, иначе он дважды попадет в базу данных.

find_or_initialize_byнаходит или инициализирует запись, а затем возвращает ее. Затем мы подключаемся к нему, вносим обновления и сохраняем их в базе данных.

Я кодировал эти искатели, которые можно использовать для разных сценариев.

Самое главное, что он удаляет параметр :id на создание и обновление.

Создание модели с :id может вызвать проблемы с MySql или же PostgreSQL потому что Rails использует автоматический порядковый номер базы данных. Если вы создаете новые экземпляры модели с :id Вы можете получить UniqueViolation: ERROR: duplicate key value violates unique constraint,

# config/initializers/model_finders.rb
class ActiveRecord::Base

  def self.find_by_or_create(attributes, &block)
    self.find_by(attributes) || self.create(attributes.except(:id), &block)
  end


  def self.find_by_or_create!(attributes, &block)
    self.find_by(attributes) || self.create!(attributes.except(:id), &block)
  end


  def self.find_or_create_update_by(attributes, &block)
    self.find_by(attributes).try(:update, attributes.except(:id), &block) || self.create(attributes.except(:id), &block)
  end


  def self.find_or_create_update_by!(attributes, &block)
    self.find_by(attributes).try(:update!, attributes.except(:id), &block) || self.create!(attributes.except(:id), &block)
  end


  def self.find_by_or_initialize(attributes, &block)
    self.find_by(attributes) || new(attributes.except(:id), &block)
  end

end

Я сделал это вчера, желая, чтобы был способ сделать это в одну строчку.

Закончилось (с использованием вашего кода):

c = Category.find_or_initialize_by_id(category.id)
c.name = category.name
c.save

Возможно, есть более хороший способ, но это то, что я использовал.

[Редактировать: использовать инициализацию вместо создания, чтобы избежать двойного попадания в БД)

Мне понравился ответ fl00r. Но почему мы должны каждый раз сохранять объект? Мы можем проверить, есть ли это уже в записях, или сохранить его

def self.find_or_create_by_id(id, &block)
    obj = self.find_by_id(id) 
    unless obj
      obj = self.create(id: id)
    end
    obj
end

Попробуй это:

c = Category.find_or_initialize_by_id(category.id)
c.name = category.name
c.save!

Таким образом, вы сохраняете экземпляр только один раз, а не дважды, если вы вызывали find_or_create_by_id (при условии, что это новая запись).

Другие вопросы по тегам