Rails создает или обновляет магию?


95

У меня есть класс, в CachedObjectкотором хранятся общие сериализованные объекты, индексированные по ключу. Я хочу, чтобы этот класс реализовал create_or_updateметод. Если объект найден, он обновит его, в противном случае он создаст новый.

Есть ли способ сделать это в Rails или мне нужно написать свой собственный метод?

Ответы:


175

Рельсы 6

Рельсы 6 добавляет upsertи upsert_allметоды , которые обеспечивают эту функциональность.

Model.upsert(column_name: value)

[upsert] Он не создает экземпляров каких-либо моделей и не запускает обратные вызовы или проверки Active Record.

Рельсы 5, 4 и 3

Нет, если вы ищете оператор типа «upsert» (когда база данных выполняет обновление или оператор вставки в той же операции). По умолчанию Rails и ActiveRecord не имеют такой возможности. Однако вы можете использовать драгоценный камень upsert .

В противном случае вы можете использовать: find_or_initialize_byили find_or_create_by, которые предлагают аналогичную функциональность, хотя и за счет дополнительного обращения к базе данных, что в большинстве случаев вряд ли является проблемой. Поэтому, если у вас нет серьезных проблем с производительностью, я бы не стал использовать этот драгоценный камень.

Например, если не найдено ни одного пользователя с именем «Роджер», создается новый экземпляр пользователя с nameустановленным значением «Роджер».

user = User.where(name: "Roger").first_or_initialize
user.email = "email@example.com"
user.save

В качестве альтернативы вы можете использовать find_or_initialize_by.

user = User.find_or_initialize_by(name: "Roger")

В Rails 3.

user = User.find_or_initialize_by_name("Roger")
user.email = "email@example.com"
user.save

Вы можете использовать блок, но он запускается только в том случае, если запись новая .

User.where(name: "Roger").first_or_initialize do |user|
  # this won't run if a user with name "Roger" is found
  user.save 
end

User.find_or_initialize_by(name: "Roger") do |user|
  # this also won't run if a user with name "Roger" is found
  user.save
end

Если вы хотите использовать блок независимо от постоянства записи, используйте tapрезультат:

User.where(name: "Roger").first_or_initialize.tap do |user|
  user.email = "email@example.com"
  user.save
end


1
Исходный код, на который указывает документ, показывает, что это не работает так, как вы предполагаете - блок передается новому методу только в том случае, если соответствующая запись не существует. Похоже, что в Rails нет магии «upsert» - вы должны разделить его на два оператора Ruby, один для выбора объекта, а другой для обновления атрибута.
Sameers

@sameers Я не уверен, что понимаю, о чем вы. Как вы думаете, что я имею в виду?
Mohamad

1
О ... Теперь я понимаю, что вы имели в виду - что обе формы find_or_initialize_byи find_or_create_byпринимают блок. Я думал, вы имели в виду, что независимо от того, существует запись или нет, для выполнения обновления будет передан блок с объектом записи в качестве аргумента.
Sameers

3
Это немного вводит в заблуждение, не обязательно ответ, но API. Можно было бы ожидать, что блок будет пройден независимо, и, следовательно, его можно будет создать / обновить соответствующим образом. Вместо этого мы должны разбить его на отдельные утверждения. Бу. <3 Rails хотя :)
Volte

32

В Rails 4 вы можете добавить к конкретной модели:

def self.update_or_create(attributes)
  assign_or_new(attributes).save
end

def self.assign_or_new(attributes)
  obj = first || new
  obj.assign_attributes(attributes)
  obj
end

и используйте это как

User.where(email: "a@b.com").update_or_create(name: "Mr A Bbb")

Или, если вы предпочитаете добавить эти методы ко всем моделям, вставьте инициализатор:

module ActiveRecordExtras
  module Relation
    extend ActiveSupport::Concern

    module ClassMethods
      def update_or_create(attributes)
        assign_or_new(attributes).save
      end

      def update_or_create!(attributes)
        assign_or_new(attributes).save!
      end

      def assign_or_new(attributes)
        obj = first || new
        obj.assign_attributes(attributes)
        obj
      end
    end
  end
end

ActiveRecord::Base.send :include, ActiveRecordExtras::Relation

1
Не assign_or_newвернет первую строку в таблице, если она существует, а затем эта строка будет обновлена? Похоже, это для меня.
Стив Кляйн

User.where(email: "a@b.com").firstвернет nil, если не найден. Убедитесь, что у вас есть whereприцел
montrealmike

Просто примечание, чтобы сказать, что updated_atне будут затронуты, потому что assign_attributesиспользуется
lshepstone

Не будет, если вы используете assing_or_new, но будет, если вы используете update_or_create из-за сохранения
montrealmike

13

Добавьте это в свою модель:

def self.update_or_create_by(args, attributes)
  obj = self.find_or_create_by(args)
  obj.update(attributes)
  return obj
end

Благодаря этому вы можете:

User.update_or_create_by({name: 'Joe'}, attributes)

Со второй частью работать не буду. Невозможно обновить одну запись с уровня класса без идентификатора записи для обновления.
Aeramor

2
obj = self.find_or_create_by (аргументы); obj.update (атрибуты); return obj; будет работать.
veeresh yh

12

Магия вы искали в добавлен Rails 6 Теперь вы можете upsert (обновление или вставка). Для использования одной записи:

Model.upsert(column_name: value)

Для нескольких записей используйте upsert_all :

Model.upsert_all(column_name: value, unique_by: :column_name)

Примечание :

  • Оба метода не запускают обратные вызовы или проверки Active Record.
  • unique_by => Только PostgreSQL и SQLite

1

Старый вопрос, но я предлагаю свое решение для полноты картины. Мне это было нужно, когда мне нужна была конкретная находка, но другое создание, если его не существует.

def self.find_by_or_create_with(args, attributes) # READ CAREFULLY! args for finding, attributes for creating!
        obj = self.find_or_initialize_by(args)
        return obj if obj.persisted?
        return obj if obj.update_attributes(attributes) 
end

0

Вы можете сделать это одним оператором, например так:

CachedObject.where(key: "the given key").first_or_create! do |cached|
   cached.attribute1 = 'attribute value'
   cached.attribute2 = 'attribute value'
end

9
Это не сработает, так как будет возвращена только исходная запись, если она будет найдена. OP запрашивает решение, которое всегда меняет значение, даже если запись найдена.
JeanMertz 01

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.