Лучший способ найти 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
(при условии, что это новая запись).