Как вы предотвращаете откат изменений базы данных внутри фильтра Rails ActiveRecord before_create, когда он возвращает false?

Я добавил фильтр before_create в одну из моих моделей Rails ActiveRecord, и внутри этого фильтра я выполняю некоторые обновления базы данных.

Иногда я возвращаю false из фильтра, чтобы предотвратить создание целевой модели, но это приводит к тому, что все другие изменения базы данных, которые я сделал (находясь внутри фильтра), свернуты назад.

Как я могу предотвратить это?

Обновление №1: вот псевдокод, объясняющий мою проблему:

class Widget < ActiveRecord::Base
  before_create :update_instead

  def update_instead
    if some_condition?
      update_some_record_in_same_model # this is getting rolled back
      return false # don't create a new record
    else
      return true # let it continue
    end
  end
end

Обновление №2. Несколько хороших ответов ниже, но у каждого есть свои недостатки. В итоге я переопределил метод create следующим образом:

def create
  super unless update_instead? # yes I reversed the return values from above
end

person Teflon Ted    schedule 20.10.2009    source источник


Ответы (5)


Я просто должен был сделать это недавно. Вам нужно специально запросить другое подключение от AR. Затем выполните свои изменения в этом соединении. Таким образом, если создание завершается ошибкой и транзакция откатывается, изменения вашего обратного вызова уже были зафиксированы в другой транзакции.

Не обращайте внимания на мой ответ выше. Пример кода, который вы только что привели, действительно прояснил ситуацию.

class Foo < ActiveRecord::Base
  before_create :update_instead

  def update_instead
    dbconn = self.class.connection_pool.checkout
    dbconn.transaction do
      dbconn.execute("update foos set name = 'updated'")
    end
    self.class.connection_pool.checkin(dbconn)
    false
  end
end


>> Foo.create(:name => 'sam')
=> #<Foo id: nil, name: "sam", created_at: nil, updated_at: nil>
>> Foo.all
=> [#<Foo id: 2, name: "updated", created_at: "2009-10-21 15:12:55", updated_at: "2009-10-21 15:12:55">]
person Rich Cavanaugh    schedule 20.10.2009
comment
Вам нужно специально запросить другое подключение от AR — Fantastic; как это сделать? Спасибо. - person Teflon Ted; 21.10.2009
comment
хех, это могло бы помочь. Я вернулся и проверил, и, к сожалению, то, что я делал, может немного отличаться. Изменения, которые мне нужно было сохранить, были в другой модели. Поэтому я просто сказал другой модели использовать собственное соединение. Итак, предположим, что основная транзакция была на модели Foo, а в before_create она выполняла Bar.create. Даже если работа над Foo провалится, я хотел, чтобы Бар остался. Поэтому я использовал install_connection в теле Bar, например: class Bar ‹ ActiveRecord::Base create_connection end - person Rich Cavanaugh; 21.10.2009
comment
Да мне это не пойдет. Здесь я работаю с одной базой данных, и, как вы видите, мои комментарии к другому ответу, вложенные транзакции также не работают. Это выглядит не очень хорошо. - person Teflon Ted; 21.10.2009
comment
Я также работаю с одной базой данных. Мое решение просто использует отдельное соединение, которое приносит с собой совершенно другую транзакцию для конкретной модели. - person Rich Cavanaugh; 21.10.2009
comment
Вот пирожок, демонстрирующий то, что я очень плохо выражаю словами. pastie.textmate.org/private/9qc8ckyqvyuc8s8fbnqnng - person Rich Cavanaugh; 21.10.2009
comment
Спасибо, но это не работает для меня. Пожалуйста, смотрите мое обновление вопроса с примером кода. - person Teflon Ted; 21.10.2009
comment
Я попробовал эту технику с Rails 3.2, используя объекты AR вместо выполнения (SQL), и не смог заставить ее работать. Транзакция все еще откатывается - что, я думаю, может означать, что она была вложена в другую транзакцию? Все еще не уверен, понимаю ли я этот цикл, поэтому я перенес логику в свой контроллер. - person schwabsauce; 17.10.2013

Используйте транзакцию в фильтре.

person Jim Deville    schedule 20.10.2009
comment
Спасибо. Не могли бы вы быть немного более явным? Возможно, какой-то образец/псевдокод? - person Teflon Ted; 21.10.2009
comment
Это не работает. Обернутая транзакция откатывается вместе с обертывающей транзакцией. Из документации: Большинство баз данных не поддерживают настоящие вложенные транзакции. На момент написания единственной известной нам базой данных, поддерживающей настоящие вложенные транзакции, была MS-SQL. По этой причине Active Record эмулирует вложенные транзакции с помощью точек сохранения. - person Teflon Ted; 21.10.2009

Вы пробовали перезаписывать файлы create/save и их деструктивные версии? ActiveRecord::Base.create, ActiveRecord::Base.save и их деструктивные версии завернуты в транзакцию, они также запускают обратные вызовы и проверки. Если вы переопределяете его, только то, что сделано super, будет частью транзакции. Если вам нужно выполнить проверки раньше, вы можете явно вызвать valid, чтобы запустить их все.

Пример:

before_create :before_create_actions_that_can_be_rolled_back

def create
  if valid? && before_create_actions_that_wont_be_rolled_back
    super
  end
end

def before_create_actions_that_wont_be_rolled_back
 # exactly what it sounds like
end

def before_create_actions_that_can_be_rolled_back
 # exactly what it sounds like
end

Предостережение: с этими изменениями методы будут вызываться в следующем порядке:

  1. перед проверкой (on_create)
  2. подтверждать
  3. после проверки (on_create)
  4. before_create_actions_that_wont_be_rolled_back
  5. перед проверкой (on_create)
  6. подтверждать
  7. после проверки (on_create)
  8. перед сохранением обратных вызовов
  9. перед созданием обратных вызовов
  10. запись создана
  11. после создания обратных вызовов
  12. после сохранения обратных вызовов

Если какая-либо проверка завершится неудачно или если какой-либо обратный вызов вернет false на шагах 5–12, база данных будет возвращена в состояние, в котором она находилась до шага 5.

Если действительный? терпит неудачу или before_create_actions_that_wont_be_rolled_back терпит неудачу, то вся цепочка будет остановлена.

person EmFi    schedule 21.10.2009
comment
Это умно и может стать отличным планом Б, но мне действительно нужно применить обновления на шаге before_create (№ 6 в вашем списке) после проверки. Спасибо. - person Teflon Ted; 21.10.2009
comment
Нет причин, по которым вы не можете назвать действительным? явно. Это обеспечит прохождение всех проверок перед выполнением ваших действий, которые не следует откатывать. Конечно, это означает, что проверки будут выполняться дважды. Независимо от того, решение было обновлено, чтобы решить вашу проблему. - person EmFi; 21.10.2009

Можно ли вместо этого внести эти изменения в after_create?

person JRL    schedule 20.10.2009
comment
Нет, я должен предотвратить создание в некоторых случаях. - person Teflon Ted; 21.10.2009

Здесь используется та же концепция, что и в ответе Рича Кавано. Я добавил родительскую модель, чтобы было понятнее, что делает фильтр. Суть в том, чтобы использовать поток + автоматическую проверку/регистрацию отдельного соединения. Примечание: вы должны убедиться, что значение :pool установлено как минимум на 2 в вашей спецификации соединения, в зависимости от того, сколько параллельных потоков вы будете запускать. Я думаю, что по умолчанию это 5.

class Gadget < ActiveRecord::Base
  has_many :widgets
end

class Widget < ActiveRecord::Base
  belongs_to :gadget
  before_create :update_instead

  def update_some_record_in_same_model
    # the thread forces a new connection to be checked out
    Thread.new do
      ActiveRecord::Base.connection_pool.with_connection do |conn|
        # try this part without the 2 surrounding blocks and it will be rolled back
        gadget.touch_count += 1
        gadget.save!
      end
    end.join
  end
  def some_condition?
    true
  end

  def update_instead
    if some_condition?
      update_some_record_in_same_model # this is getting rolled back
      p [:touch_count_in_filter, gadget.reload.touch_count]
      return false # don't create a new record
    else
      return true # let it continue
    end
  end
end

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

  g = Gadget.create(:name => 'g1')
  puts "before:"
  p [:touch_count, g.reload.touch_count]
  p [:widget_count, Widget.count]

  g.widgets.create(:name => 'w1')
  puts "after:"
  # Success means the count stays incremented
  p [:touch_count, g.reload.touch_count]
  p [:widget_count, Widget.count]

Дополнительная литература: http://bibwild.wordpress.com/2011/11/14/multi-threading-in-rails-activerecord-3-0-3-1/

person Kelvin    schedule 17.02.2012