Рейки створюють або оновлюють магію?


93

У мене є клас, що називається CachedObjectзагальними серіалізованими об’єктами, проіндексованими ключем. Я хочу, щоб цей клас реалізував create_or_updateметод. Якщо об’єкт знайдений, він оновить його, інакше створить новий.

Чи є спосіб зробити це в Rails, чи я повинен написати свій власний метод?

Відповіді:


172

Рейки 6

Rails 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")

У рейках 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 немає магії "підняття" - вам потрібно розділити її на два оператори Ruby, один для вибору об'єкта та один для оновлення атрибутів.
sameers

@sameers Я не впевнений, що розумію, що ти маєш на увазі. Що, на вашу думку, я маю на увазі?
Мохамад,

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поверне нуль, якщо не знайдено. Переконайтеся, що у вас є 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

1
obj = self.find_or_create_by (аргументи); obj.update (атрибути); return obj; спрацює.
veeresh yh

12

Магія, яку ви шукали, була додана в Rails 6 Тепер ви можете вставити (оновити або вставити). Для використання одного запису:

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.