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

95

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

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

kid_drew
источник

Ответы:

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
Aeramor
источник
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
0

Сиквел камень добавляет update_or_create метод , который , кажется, делать то , что вы ищете.

Курт Престон
источник
11
Речь идет об Active Record.
ja