Как я могу избежать выполнения обратных вызовов ActiveRecord?


143

У меня есть модели с обратными вызовами after_save. Обычно это нормально, но в некоторых ситуациях, например при создании данных для разработки, я хочу сохранить модели без выполнения обратных вызовов. Есть простой способ сделать это? Что-то вроде ...

Person#save( :run_callbacks => false )

или

Person#save_without_callbacks

Я просмотрел документацию Rails и ничего не нашел. Однако, по моему опыту, документы Rails не всегда рассказывают всю историю.

ОБНОВИТЬ

Я нашел сообщение в блоге, в котором объясняется, как можно удалить обратные вызовы из такой модели:

Foo.after_save.clear

Я не смог найти, где задокументирован этот метод, но, похоже, он работает.


8
Если вы делаете что-то разрушительное или дорогостоящее (например, отправляете электронные письма) в обратном вызове, я рекомендую убрать это и запустить его отдельно от контроллера или где-то еще. Таким образом, вы не «случайно» активируете его в процессе разработки и т. Д.
ryanb

2
принятое вами решение не работает для меня. Я использую рельсы 3. Я получаю сообщение об ошибке , как это: - неопределенный метод `update_without_callbacks' для # <Пользователя: 0x10ae9b848>
Мохит джайн

дааа, эта запись в блоге сработала ....
Mohit Jain

1
Связанный вопрос: stackoverflow.com/questions/19449019/…
Allerin

Не стал бы Foo.after_save.clear удалял обратные вызовы для всей модели? И как тогда вы предлагаете их восстановить?
Джошуа Пинтер,

Ответы:


72

Это решение только для Rails 2.

Я только что исследовал это и думаю, что у меня есть решение. Вы можете использовать два частных метода ActiveRecord:

update_without_callbacks
create_without_callbacks

Вам нужно будет использовать send для вызова этих методов. Примеры:

p = Person.new(:name => 'foo')
p.send(:create_without_callbacks)

p = Person.find(1)
p.send(:update_without_callbacks)

Это определенно то, что вы действительно захотите использовать только в консоли или при выполнении некоторых случайных тестов. Надеюсь это поможет!


7
это не работает для меня. Я использую рельсы 3. Я получаю сообщение об ошибке , как это: - неопределенный метод `update_without_callbacks' для # <Пользователь: 0x10ae9b848>
Мохит Jain

Ваше предложение не работает, но сообщение в блоге, упомянутое в части обновления, работает ..
Mohit Jain

Это также пропустит проверки.
Daniel Pietzsch

У меня есть другое решение для любой версии Rails. У нас это хорошо работает. Проверьте это в моем сообщении в блоге: railsguides.net/2014/03/25/skip-callbacks-in-tests
ka8725

228

Используйте update_column(Rails> = v3.1) или update_columns(Rails> = 4.0), чтобы пропустить обратные вызовы и проверки. Кроме того, с помощью этих методов, updated_atявляется не обновляется.

#Rails >= v3.1 only
@person.update_column(:some_attribute, 'value')
#Rails >= v4.0 only
@person.update_columns(attributes)

http://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-update_column

# 2: пропуск обратных вызовов, который также работает при создании объекта

class Person < ActiveRecord::Base
  attr_accessor :skip_some_callbacks

  before_validation :do_something
  after_validation :do_something_else

  skip_callback :validation, :before, :do_something, if: :skip_some_callbacks
  skip_callback :validation, :after, :do_something_else, if: :skip_some_callbacks
end

person = Person.new(person_params)
person.skip_some_callbacks = true
person.save

2
похоже, что он работает и с 2.x, и есть множество других методов, которые работают аналогично: guides.rubyonrails.org/…
rogerdpack

16
Это не касается :create_without_callbacks:( Как я могу запустить что-то подобное? (Работает в Rails2, удалено в Rails3).
nzifnab

Предполагая @person, что где-то в контроллере есть переменная, это решение означает, что люди, читающие ваш класс модели, не смогут понять обратные вызовы. Они увидят after_create :something_coolи подумают: «Отлично, после создания происходит что-то классное!». Чтобы на самом деле понять ваш модельный класс, им придется перебирать все ваши контроллеры, ища все маленькие места, где вы решили внедрить логику. Мне это не нравится> o <;;
Зигги

1
замените skip_callback ..., if: :skip_some_callbacksна, after_create ..., unless: :skip_some_callbacksчтобы правильно запустить это с помощью after_create.
sakurashinken

28

Обновлено:

Решение @Vikrant Chaudhary кажется лучше:

#Rails >= v3.1 only
@person.update_column(:some_attribute, 'value')
#Rails >= v4.0 only
@person.update_columns(attributes)

Мой первоначальный ответ:

см. эту ссылку: Как пропустить обратные вызовы ActiveRecord?

в Rails3,

Предположим, у нас есть определение класса:

class User < ActiveRecord::Base
  after_save :generate_nick_name
end 

Подход 1:

User.send(:create_without_callbacks)
User.send(:update_without_callbacks)

Подход 2: если вы хотите пропустить их в своих файлах rspec или чем-то еще, попробуйте следующее:

User.skip_callback(:save, :after, :generate_nick_name)
User.create!()

ПРИМЕЧАНИЕ: как только это будет сделано, если вы не находитесь в среде rspec, вам следует сбросить обратные вызовы:

User.set_callback(:save, :after, :generate_nick_name)

у меня отлично работает на рельсах 3.0.5


22

Если цель состоит в том, чтобы просто вставить запись без обратных вызовов или проверок, и вы хотели бы сделать это, не прибегая к дополнительным драгоценным камням, добавляя условные проверки, используя RAW SQL или каким-либо образом изменяя существующий код, рассмотрите возможность использования «тени объект ", указывающий на существующую таблицу БД. Вот так:

class ImportedPerson < ActiveRecord::Base
  self.table_name = 'people'
end

Это работает со всеми версиями Rails, является потокобезопасным и полностью исключает все проверки и обратные вызовы без каких-либо изменений в существующем коде. Вы можете просто добавить это объявление класса прямо перед фактическим импортом, и все будет в порядке. Просто не забудьте использовать свой новый класс для вставки объекта, например:

ImportedPerson.new( person_attributes )

4
Лучшее решение КОГДА-ЛИБО. Элегантно и просто!
Рафаэль Оливейра,

1
Это сработало для меня очень хорошо, потому что это было то, что я хотел сделать только в тесте, чтобы имитировать состояние базы данных «до», не загрязняя мой объект производственной модели оборудованием, чтобы опционально пропустить обратные вызовы.
Дуглас Ловелл

1
Безусловно, лучший ответ
robomc

1
Проголосовали за, потому что он показывает, как обойти существующие ограничения рельсов, и помог мне понять, как на самом деле работает весь объект MVC. Так просто и чисто.
Майкл Шмитц,

20

рельсы 3:

MyModel.send("_#{symbol}_callbacks") # list  
MyModel.reset_callbacks symbol # reset

11
Ницца. Также MyModel.skip_callback (: create,: after,: my_callback) для точного управления .. см. ActiveSupport :: Callbacks :: ClassMethods docs для всего лобанга
позднее

4
Полезная информация: «символа» в нем reset_callbacksнет :after_save, а скорее :save. apidock.com/rails/v3.0.9/ActiveSupport/Callbacks/ClassMethods/…
nessur

17

Вы можете попробовать что-то подобное в своей модели Person:

after_save :something_cool, :unless => :skip_callbacks

def skip_callbacks
  ENV[RAILS_ENV] == 'development' # or something more complicated
end

РЕДАКТИРОВАТЬ: after_save не является символом, но, по крайней мере, в тысячный раз я пытался сделать его одним.


1
Я действительно думаю, что это лучший ответ здесь. Таким образом, логика, которая определяет, когда обратный вызов пропускается, доступна в модели, и у вас не будет повсюду сумасшедших фрагментов кода, откладывающих бизнес-логику или обходящих инкапсуляцию с помощью send. KOODOS
Ziggy

11

Вы можете использовать update_columns:

User.first.update_columns({:name => "sebastian", :age => 25})

Обновляет указанные атрибуты объекта без вызова save, тем самым пропуская проверки и обратные вызовы.


7

Единственный способ предотвратить все обратные вызовы after_save - сделать так, чтобы первый возвращал false.

Возможно, вы могли бы попробовать что-то вроде (непроверенного):

class MyModel < ActiveRecord::Base
  attr_accessor :skip_after_save

  def after_save
    return false if @skip_after_save
    ... blah blah ...
  end
end

...

m = MyModel.new # ... etc etc
m.skip_after_save = true
m.save

1
Я люблю пробовать (непроверено). Увлекательная поездка.
Adamantish

Протестировано и все работает. Думаю, это очень хорошее и чистое решение, спасибо!
кернификация

6

Похоже, что одним из способов справиться с этим в Rails 2.3 (поскольку update_without_callbacks больше нет и т. Д.) Было бы использование update_all, который является одним из методов, который пропускает обратные вызовы в соответствии с разделом 12 руководства Rails по проверкам и обратным вызовам .

Также обратите внимание, что если вы что-то делаете в своем обратном вызове after_, который выполняет расчет на основе многих ассоциаций (например, has_many assoc, где вы также принимаете accept_nested_attributes_for), вам нужно будет перезагрузить ассоциацию, в случае, если как часть сохранения , один из ее участников был удален.


4

В up-votedнекоторых случаях ответ может показаться запутанным.

Вы можете использовать простую ifпроверку, если хотите пропустить обратный вызов, например:

after_save :set_title, if: -> { !new_record? && self.name_changed? }


3

Решение, которое должно работать во всех версиях Rails без использования гема или плагина, - это просто напрямую выдавать операторы обновления. например

ActiveRecord::Base.connection.execute "update table set foo = bar where id = #{self.id}"

Это может (а может и не быть) вариант в зависимости от сложности вашего обновления. Это хорошо работает , например , для флагов корректировки на запись из в качестве after_save обратного вызова (без обратного вызова перезапуска).


Не уверен, почему голосование против, но я все же думаю, что приведенный выше ответ является законным. Иногда лучший способ избежать проблем с поведением ActiveRecord - избегать использования ActiveRecord.
Дэйв Смайли

За принцип проголосовали против -1. У нас просто возникла производственная проблема (с длинной историей), которая потребовала от нас создания новой записи (а не обновления), и запуск обратных вызовов был бы катастрофическим. Все приведенные выше ответы - хаки, признают они это или нет, и переход к БД был лучшим решением. Для этого ЕСТЬ законные условия. Хотя следует остерегаться SQL-инъекций с расширением #{...}.
зловещий

2

Мне нужно было решение для Rails 4, поэтому я придумал следующее:

приложение / модели / проблемы / save_without_callbacks.rb

module SaveWithoutCallbacks

  def self.included(base)
    base.const_set(:WithoutCallbacks,
      Class.new(ActiveRecord::Base) do
        self.table_name = base.table_name
      end
      )
  end

  def save_without_callbacks
    new_record? ? create_without_callbacks : update_without_callbacks
  end

  def create_without_callbacks
    plain_model = self.class.const_get(:WithoutCallbacks)
    plain_record = plain_model.create(self.attributes)
    self.id = plain_record.id
    self.created_at = Time.zone.now
    self.updated_at = Time.zone.now
    @new_record = false
    true
  end

  def update_without_callbacks
    update_attributes = attributes.except(self.class.primary_key)
    update_attributes['created_at'] = Time.zone.now
    update_attributes['updated_at'] = Time.zone.now
    update_columns update_attributes
  end

end

в любой модели:

include SaveWithoutCallbacks

тогда ты можешь:

record.save_without_callbacks

или

Model::WithoutCallbacks.create(attributes)

1
# for rails 3
  if !ActiveRecord::Base.private_method_defined? :update_without_callbacks
    def update_without_callbacks
      attributes_with_values = arel_attributes_values(false, false, attribute_names)
      return false if attributes_with_values.empty?
      self.class.unscoped.where(self.class.arel_table[self.class.primary_key].eq(id)).arel.update(attributes_with_values)
    end
  end

1

Ничто из этого не указывает на without_callbacksплагин, который просто делает то, что вам нужно ...

class MyModel < ActiveRecord::Base
  before_save :do_something_before_save

  def after_save
    raise RuntimeError, "after_save called"
  end

  def do_something_before_save
    raise RuntimeError, "do_something_before_save called"
  end
end

o = MyModel.new
MyModel.without_callbacks(:before_save, :after_save) do
  o.save # no exceptions raised
end

http://github.com/cjbottaro/without_callbacks работает с Rails 2.x


1

Я написал плагин, который реализует update_without_callbacks в Rails 3:

http://github.com/dball/skip_activerecord_callbacks

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


1

Если вы используете Rails 2. Вы можете использовать SQL-запрос для обновления столбца без выполнения обратных вызовов и проверок.

YourModel.connection.execute("UPDATE your_models SET your_models.column_name=#{value} WHERE your_models.id=#{ym.id}")

Думаю, он должен работать в любых версиях рельсов.


1

Когда мне нужен полный контроль над обратным вызовом, я создаю другой атрибут, который используется в качестве переключателя. Просто и эффективно:

Модель:

class MyModel < ActiveRecord::Base
  before_save :do_stuff, unless: :skip_do_stuff_callback
  attr_accessor :skip_do_stuff_callback

  def do_stuff
    puts 'do stuff callback'
  end
end

Контрольная работа:

m = MyModel.new()

# Fire callbacks
m.save

# Without firing callbacks
m.skip_do_stuff_callback = true
m.save

# Fire callbacks again
m.skip_do_stuff_callback = false
m.save


1

Вы можете использовать драгоценный камень скрытого сохранения: https://rubygems.org/gems/sneaky-save .

Обратите внимание, что это не может помочь в сохранении ассоциаций без проверок. Он выдает ошибку created_at cannot be null, поскольку он напрямую вставляет запрос sql в отличие от модели. Чтобы реализовать это, нам нужно обновить все автоматически сгенерированные столбцы db.


0

Почему вы хотите иметь возможность делать это в процессе разработки? Конечно, это будет означать, что вы создаете свое приложение с недопустимыми данными, и поэтому оно будет вести себя странно, а не так, как вы ожидаете в производственной среде.

Если вы хотите заполнить свой dev db данными, лучшим подходом будет создание rake-задачи, которая использовала faker gem для создания достоверных данных и импорта их в db, создавая столько или несколько записей, сколько вы хотите, но если вы на пятке Я склонен к этому, и у меня есть веская причина, я предполагаю, что update_without_callbacks и create_without_callbacks будут работать нормально, но когда вы пытаетесь согнуть рельсы по своей воле, спросите себя, у вас есть веская причина и действительно ли то, что вы делаете, хорошая идея.


Я не пытаюсь сохранять без проверок, просто без обратных вызовов. Мое приложение использует обратные вызовы для записи статического HTML-кода в файловую систему (вроде как CMS). Я не хочу этого делать при загрузке данных разработчика.
Итан,

Это была просто мысль, я думаю, когда в прошлом я встречал такой вопрос, он пытается обойтись без всяких на то причин.
nitecoder

0

Один из вариантов - иметь отдельную модель для таких манипуляций, используя ту же таблицу:

class NoCallbacksModel < ActiveRecord::Base
  set_table_name 'table_name_of_model_that_has_callbacks'

  include CommonModelMethods # if there are
  :
  :

end

(Тот же подход может облегчить обход проверок)

Стефан


0

Другой способ - использовать хуки проверки вместо обратных вызовов. Например:

class Person < ActiveRecord::Base
  validate_on_create :do_something
  def do_something
    "something clever goes here"
  end
end

Таким образом, вы можете получить do_something по умолчанию, но вы можете легко переопределить его с помощью:

@person = Person.new
@person.save(false)

3
Это кажется плохой идеей - вы должны использовать вещи по прямому назначению. Меньше всего вам нужно, чтобы ваши проверки имели побочные эффекты.
chug2k

0

То, что должно работать со всеми версиями, ActiveRecordвне зависимости от опций или методов activerecord, которые могут существовать или не существовать.

module PlainModel
  def self.included(base)
    plainclass = Class.new(ActiveRecord::Base) do
      self.table_name = base.table_name
    end
    base.const_set(:Plain, plainclass)
  end
end


# usage
class User < ActiveRecord::Base
  include PlainModel

  validates_presence_of :email
end

User.create(email: "")        # fail due to validation
User::Plain.create(email: "") # success. no validation, no callbacks

user = User::Plain.find(1)
user.email = ""
user.save

TL; DR: используйте "другую модель активной записи" для той же таблицы


0

Для настраиваемых обратных вызовов используйте attr_accessorи unlessв обратном вызове.

Определите свою модель следующим образом:

class Person << ActiveRecord::Base

  attr_accessor :skip_after_save_callbacks

  after_save :do_something, unless: :skip_after_save_callbacks

end

А затем, если вам нужно сохранить запись, не after_saveзатрагивая заданные вами обратные вызовы, установите для skip_after_save_callbacksвиртуального атрибута значение true.

person.skip_after_save_callbacks #=> nil
person.save # By default, this *will* call `do_something` after saving.

person.skip_after_save_callbacks = true
person.save # This *will not* call `do_something` after saving.

person.skip_after_save_callbacks = nil # Always good to return this value back to its default so you don't accidentally skip callbacks.

0

Я столкнулся с той же проблемой, когда хотел запустить задачу Rake, но без выполнения обратных вызовов для каждой сохраняемой записи. Это сработало для меня (Rails 5), и должно работать почти для каждой версии Rails:

class MyModel < ApplicationRecord
  attr_accessor :skip_callbacks
  before_create :callback1
  before_update :callback2
  before_destroy :callback3

  private
  def callback1
    return true if @skip_callbacks
    puts "Runs callback1"
    # Your code
  end
  def callback2
    return true if @skip_callbacks
    puts "Runs callback2"
    # Your code
  end
  # Same for callback3 and so on....
end

Это работает так, что он просто возвращает true в первой строке метода, skip_callbacks имеет значение true, поэтому остальная часть кода метода не выполняется. Чтобы пропустить обратные вызовы, вам просто нужно установить skip_callbacks в true перед сохранением, созданием, уничтожением:

rec = MyModel.new() # Or Mymodel.find()
rec.skip_callbacks = true
rec.save

-6

Не самый чистый способ, но вы можете обернуть код обратного вызова в условие, которое проверяет среду Rails.

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