Como evitar a execução de retornos de chamada do ActiveRecord?

140

Eu tenho alguns modelos que têm retornos de chamada after_save. Normalmente, isso é bom, mas em algumas situações, como ao criar dados de desenvolvimento, quero salvar os modelos sem que os retornos de chamada sejam executados. Existe uma maneira simples de fazer isso? Algo parecido com ...

Person#save( :run_callbacks => false )

ou

Person#save_without_callbacks

Procurei nos documentos do Rails e não encontrei nada. No entanto, na minha experiência, os documentos do Rails nem sempre contam toda a história.

ATUALIZAR

Encontrei uma postagem no blog que explica como você pode remover retornos de chamada de um modelo como este:

Foo.after_save.clear

Não consegui encontrar onde esse método está documentado, mas parece funcionar.

Ethan
fonte
8
Se você estiver fazendo algo destrutivo ou caro (como enviar e-mails) em um retorno de chamada, recomendo movê-lo e acioná-lo separadamente do controlador ou de outro lugar. Desta forma, você não vai "acidentalmente" gatilho lo no desenvolvimento, etc.
ryanb
2
solução que você aceitou não está funcionando para mim. Estou usando o rails 3. Estou recebendo um erro como este: - método indefinido `update_without_callbacks 'para # <User: 0x10ae9b848>
Mohit Jain
yaa que o post do blog funcionou ....
Mohit Jain
1
Pergunta relacionada: stackoverflow.com/questions/19449019/…
Allerin
Não Foo.after_save.clearremoveria retornos de chamada para o modelo inteiro? E então, como você propõe restaurá-los?
Joshua Pinter

Respostas:

72

Esta solução é apenas o Rails 2.

Acabei de investigar isso e acho que tenho uma solução. Existem dois métodos particulares do ActiveRecord que você pode usar:

update_without_callbacks
create_without_callbacks

Você precisará usar send para chamar esses métodos. exemplos:

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

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

Definitivamente, isso é algo que você realmente deseja usar no console ou durante alguns testes aleatórios. Espero que isto ajude!

efalcao
fonte
7
Não está funcionando para mim. Estou usando o rails 3. Estou recebendo um erro como este: - método indefinido `update_without_callbacks 'para # <User: 0x10ae9b848>
Mohit Jain
Sua sugestão não está funcionando, mas o post mencionados na parte atualização está funcionando ..
Mohit Jain
Isso também ignorará as validações.
Daniel Pietzsch 14/09/11
Eu tenho outra solução para qualquer versão do Rails. Funciona bem para nós. Verifique-o no meu blog: railsguides.net/2014/03/25/skip-callbacks-in-tests
ka8725:
224

Use update_column(Rails> = v3.1) ou update_columns(Rails> = 4.0) para ignorar retornos de chamada e validações. Também com esses métodos, nãoupdated_at é atualizado.

#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: Ignorando retornos de chamada que também funcionam ao criar um objeto

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
Vikrant Chaudhary
fonte
2
Parece que ele trabalha com 2.x, bem como, e há uma série de outros métodos que funcionam de forma semelhante: guides.rubyonrails.org/...
rogerdpack
15
Esta não aborda :create_without_callbacks:( Como posso executar algo semelhante a isso (Trabalhou em Rails2, removido em Rails3)?.
nzifnab
Supondo que @personseja uma variável em um controlador em algum lugar, essa solução significa que as pessoas que estão lendo sua classe de modelo não serão capazes de entender os retornos de chamada. Eles verão after_create :something_coole pensarão "ótimo, algo legal acontece após a criação!". Para realmente entender sua classe de modelo, eles terão que passar por todos os seus controladores, procurando todos os pequenos lugares onde você decidiu injetar lógica. Eu não gosto disso> o <;;
Ziggy
1
substitua skip_callback ..., if: :skip_some_callbackspor after_create ..., unless: :skip_some_callbackspara executar isso corretamente com after_create.
sakurashinken
28

Atualizada:

A solução de @Vikrant Chaudhary parece melhor:

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

Minha resposta original:

consulte este link: Como pular retornos de chamada do ActiveRecord?

no Rails3,

suponha que temos uma definição de classe:

class User < ActiveRecord::Base
  after_save :generate_nick_name
end 

Abordagem 1:

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

Abordagem 2: Quando você quiser ignorá-los em seus arquivos rspec ou qualquer outra coisa, tente o seguinte:

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

NOTA: quando isso for feito, se você não estiver no ambiente rspec, deverá redefinir os retornos de chamada:

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

funciona bem para mim nos trilhos 3.0.5

Siwei Shen 申思维
fonte
20

trilhos 3:

MyModel.send("_#{symbol}_callbacks") # list  
MyModel.reset_callbacks symbol # reset
guai
fonte
11
Agradável. Também MyModel.skip_callback (: create,: after,: my_callback) para um controle preciso. Consulte ActiveSupport :: Callbacks :: ClassMethods para todos os lobang
atrasado
4
Informações úteis: o 'símbolo' reset_callbacksnão é :after_save, mas sim :save. apidock.com/rails/v3.0.9/ActiveSupport/Callbacks/ClassMethods/…
nessur
19

Se o objetivo é simplesmente inserir um registro sem retornos de chamada ou validações, e você gostaria de fazê-lo sem recorrer a gemas adicionais, adicionando verificações condicionais, usando RAW SQL ou futzing com o código existente, considere usar uma "sombra" objeto "apontando para sua tabela db existente. Igual a:

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

Isso funciona com todas as versões do Rails, é seguro para threads e elimina completamente todas as validações e retornos de chamada sem modificações no código existente. Você pode simplesmente lançar essa declaração de classe antes da importação real e deve estar pronto. Lembre-se de usar sua nova classe para inserir o objeto, como:

ImportedPerson.new( person_attributes )
Brad Werth
fonte
4
Melhor solução NUNCA. Elegante e simples!
Rafael Oliveira
1
Isso funcionou muito bem para mim porque era algo que eu queria fazer apenas em teste, para simular o estado "antes" do banco de dados, sem poluir meu objeto de modelo de produção com máquinas para opcionalmente ignorar retornos de chamada.
Douglas Lovell
1
De longe a melhor resposta
robomc 7/03/19
1
Promovido porque mostra como solucionar as restrições de trilhos existentes e me ajudou a entender como todo o objeto MVC realmente funciona. Tão simples e limpo.
Michael Schmitz
17

Você pode tentar algo assim no seu modelo de pessoa:

after_save :something_cool, :unless => :skip_callbacks

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

EDIT: after_save não é um símbolo, mas é pelo menos a milésima vez que tentei torná-lo um.

Sarah Mei
fonte
1
Eu realmente acho que esta é a melhor resposta aqui. Dessa forma, a lógica que determina quando o retorno de chamada é ignorado está disponível no modelo, e você não tem fragmentos de código malucos em todo lugar, revidando a lógica de negócios ou contornando o encapsulamento send. KOODOS
Ziggy
10

Você pode usar update_columns:

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

Atualiza os atributos fornecidos de um objeto, sem chamar save, ignorando validações e retornos de chamada.

Luís Ramalho
fonte
7

A única maneira de impedir todos os retornos de chamada after_save é fazer com que o primeiro retorne falso.

Talvez você possa tentar algo como (não testado):

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
rfunduk
fonte
1
Eu amo tentar (não testado). Passeio emocionante.
precisa
Testado e funciona. Eu acho que essa é uma solução muito boa e limpa, obrigado!
precisa saber é o seguinte
5

Parece que uma maneira de lidar com isso no Rails 2.3 (desde que o update_without_callbacks se foi, etc.) seria usar o update_all, que é um dos métodos que ignora os retornos de chamada conforme a seção 12 do Guia do Rails para validações e retornos de chamada .

Além disso, observe que, se você estiver fazendo algo em seu retorno de chamada posterior, faça um cálculo com base em muitas associações (por exemplo, um has_many assoc, em que você também aceita accept_nested_attributes_for), será necessário recarregar a associação, caso faça parte do salvamento , um de seus membros foi excluído.

chrisrbailey
fonte
4

https://gist.github.com/576546

basta despejar esse patch de macaco em config / initializers / skip_callbacks.rb

então

Project.skip_callbacks { @project.save }

ou semelhante.

todo o crédito ao autor

fringd
fonte
4

A maioria das up-votedrespostas pode parecer confusa em alguns casos.

Você pode usar apenas uma ifverificação simples se desejar pular um retorno de chamada, assim:

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

Uma solução que deve funcionar em todas as versões do Rails sem o uso de uma gema ou plugin é simplesmente emitir instruções de atualização diretamente. por exemplo

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

Isso pode (ou não) ser uma opção, dependendo da complexidade da sua atualização. Isso funciona bem para ex bandeiras de atualização em um registro de dentro de um retorno de chamada after_save (sem retriggering o callback).

Dave Smylie
fonte
Não sei por que o voto negativo, mas ainda acho que a resposta acima é legítima. Às vezes, a melhor maneira de evitar problemas com o comportamento do ActiveRecord é evitar o uso do ActiveRecord.
18714 Dave Smylie
Votado por princípio para combater o -1. Acabamos de ter um problema de produção (com uma longa história por trás) que exigia a criação de um novo registro (não uma atualização) e o acionamento de retornos de chamada teria sido catastrófico. Todas as respostas acima são hacks, admitam ou não, e ir para o banco de dados foi a melhor solução. Existem condições legítimas para isso. Embora se deva tomar cuidado com a injeção de SQL com o #{...}.
sinisterchipmunk
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
Sasha Alexandrov
fonte
1

Nenhum desses pontos para o without_callbacksplugin que apenas faz o que você precisa ...

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 funciona com o Rails 2.x

kares
fonte
1

Eu escrevi um plugin que implementa update_without_callbacks no Rails 3:

http://github.com/dball/skip_activerecord_callbacks

A solução certa, eu acho, é reescrever seus modelos para evitar retornos de chamada em primeiro lugar, mas se isso for impraticável no curto prazo, esse plug-in pode ajudar.

Donald ball
fonte
1

Se você estiver usando o Rails 2. Você pode usar a consulta SQL para atualizar sua coluna sem executar retornos de chamada e validações.

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

Eu acho que deve funcionar em qualquer versão de trilhos.

oivoodoo
fonte
1

Quando preciso ter controle total sobre o retorno de chamada, crio outro atributo que é usado como uma opção. Simples e eficaz:

Modelo:

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

Teste:

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
tothemario
fonte
1

Para criar dados de teste no Rails, você usa este hack:

record = Something.new(attrs)
ActiveRecord::Persistence.instance_method(:create_record).bind(record).call

https://coderwall.com/p/y3yp2q/edit

Wojtek Kruszewski
fonte
1

Você pode usar a joia de salvamento furtivo: https://rubygems.org/gems/sneaky-save .

Observe que isso não pode ajudar a salvar associações sem validações. Ele gera o erro 'created_at não pode ser nulo', pois insere diretamente a consulta sql, diferente de um modelo. Para implementar isso, precisamos atualizar todas as colunas geradas automaticamente do db.

Zinin Serge
fonte
1

Eu precisava de uma solução para o Rails 4, então vim com isso:

app / modelos / preocupações / 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

em qualquer modelo:

include SaveWithoutCallbacks

então você pode:

record.save_without_callbacks

ou

Model::WithoutCallbacks.create(attributes)
Steve Friedman
fonte
0

Por que você gostaria de fazer isso no desenvolvimento? Certamente, isso significa que você está construindo seu aplicativo com dados inválidos e, como tal, se comportará de maneira estranha e não como o esperado na produção.

Se você deseja preencher seu banco de dados de desenvolvimento com dados, uma abordagem melhor seria criar uma tarefa de rake que usasse a gema mais falsa para criar dados válidos e importá-los para o banco de dados, criando tantos ou poucos registros quanto desejar, mas se você estiver preparado inclinado e com um bom motivo, acho que update_without_callbacks e create_without_callbacks funcionarão bem, mas quando você estiver tentando dobrar os trilhos à sua vontade, pergunte a si mesmo que tem um bom motivo e se o que está fazendo é realmente uma boa ideia.

nitecoder
fonte
Não estou tentando salvar sem validações, apenas sem retornos de chamada. Meu aplicativo está usando retornos de chamada para gravar HTML estático no sistema de arquivos (como um CMS). Não quero fazer isso enquanto carrego dados de desenvolvimento.
Ethan
Foi apenas um pensamento, acho que sempre que no passado eu via esse tipo de pergunta, ele tentava contornar as coisas por motivos ruins.
Nitecoder 12/03/09
0

Uma opção é ter um modelo separado para essas manipulações, usando a mesma tabela:

class NoCallbacksModel < ActiveRecord::Base
  set_table_name 'table_name_of_model_that_has_callbacks'

  include CommonModelMethods # if there are
  :
  :

end

(A mesma abordagem pode facilitar as coisas para ignorar validações)

Stephan

Stephan Wehner
fonte
0

Outra maneira seria usar ganchos de validação em vez de retornos de chamada. Por exemplo:

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

Dessa forma, você pode obter o do_something por padrão, mas pode substituí-lo facilmente por:

@person = Person.new
@person.save(false)
Ryan Crispin Heneise
fonte
3
Parece uma péssima idéia - você deve usar as coisas para o propósito a que se destinam. A última coisa que você deseja é que suas validações tenham efeitos colaterais.
chug2k
0

Algo que deve funcionar com todas as versões ActiveRecordsem depender de opções ou métodos de registro de ativos que possam ou não existir.

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

TLDR: use um "modelo de registro de ativo diferente" na mesma tabela

choonkeat
fonte
0

Para retornos de chamada personalizados, use um attr_accessore um unlessno retorno de chamada.

Defina seu modelo da seguinte maneira:

class Person << ActiveRecord::Base

  attr_accessor :skip_after_save_callbacks

  after_save :do_something, unless: :skip_after_save_callbacks

end

E então se você precisa salvar o registro sem bater os after_saveretornos de chamada que você definiu, defina o skip_after_save_callbacksatributo virtual para 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.
Joshua Pinter
fonte
-5

Não é a maneira mais limpa, mas você pode agrupar o código de retorno de chamada em uma condição que verifique o ambiente do Rails.

if Rails.env == 'production'
  ...
James
fonte