Jak mogę uniknąć wywołań zwrotnych ActiveRecord?

140

Mam kilka modeli, które mają wywołania zwrotne after_save. Zwykle jest to w porządku, ale w niektórych sytuacjach, na przykład podczas tworzenia danych programistycznych, chcę zapisać modele bez uruchamiania wywołań zwrotnych. Czy jest na to prosty sposób? Coś w rodzaju ...

Person#save( :run_callbacks => false )

lub

Person#save_without_callbacks

Zajrzałem do dokumentacji Rails i nic nie znalazłem. Jednak z mojego doświadczenia wynika, że ​​dokumentacja Rails nie zawsze opisuje całą historię.

AKTUALIZACJA

Znalazłem post na blogu, w którym wyjaśniono, jak usunąć wywołania zwrotne z takiego modelu:

Foo.after_save.clear

Nie mogłem znaleźć, gdzie ta metoda jest udokumentowana, ale wydaje się, że działa.

Ethan
źródło
8
Jeśli robisz coś destrukcyjnego lub kosztownego (na przykład wysyłanie e-maili) w wywołaniu zwrotnym, zalecam usunięcie tego i uruchomienie go oddzielnie od kontrolera lub w innym miejscu. W ten sposób nie „przypadkowo” uruchomisz go w fazie rozwoju itp.
ryanb
2
rozwiązanie, które zaakceptowałeś, nie działa dla mnie. Używam rails 3. Otrzymuję następujący błąd: - undefined method `update_without_callbacks 'for # <User: 0x10ae9b848>
Mohit Jain
taaa, ten post na blogu zadziałał ...
Mohit Jain,
1
Powiązane pytanie: stackoverflow.com/questions/19449019/…
Allerin
Czy nie Foo.after_save.clearusunąłbyś wywołań zwrotnych dla całego modelu? A więc jak proponujesz je przywrócić?
Joshua Pinter

Odpowiedzi:

72

To rozwiązanie dotyczy tylko Rails 2.

Właśnie to zbadałem i myślę, że mam rozwiązanie. Istnieją dwie prywatne metody ActiveRecord, których możesz użyć:

update_without_callbacks
create_without_callbacks

Będziesz musiał użyć funkcji send, aby wywołać te metody. przykłady:

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

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

Jest to zdecydowanie coś, czego naprawdę będziesz chciał używać tylko w konsoli lub podczas wykonywania losowych testów. Mam nadzieję że to pomoże!

efalcao
źródło
7
to nie działa dla mnie. Używam rails 3. Otrzymuję następujący błąd: - undefined method `update_without_callbacks 'for # <User: 0x10ae9b848>
Mohit Jain
Twoja sugestia nie działa, ale post na blogu wspomniany w części dotyczącej aktualizacji działa.
Mohit Jain
Spowoduje to również pominięcie weryfikacji.
Daniel Pietzsch,
Mam inne rozwiązanie dla dowolnej wersji Railsów. U nas to działa. Sprawdź to na moim blogu: railsguides.net/2014/03/25/skip-callbacks-in-tests
ka8725
224

Użyj update_column(Rails> = v3.1) lub update_columns(Rails> = 4.0), aby pominąć wywołania zwrotne i walidację. Również w przypadku tych metod nieupdated_at jest aktualizowany.

#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: Pomijanie wywołań zwrotnych, które działają również podczas tworzenia obiektu

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
źródło
2
wygląda na to, że działa również z 2.x, a jest wiele innych metod, które działają podobnie: guide.rubyonrails.org/…
rogerdpack
15
To nie dotyczy :create_without_callbacks:( Jak mogę uruchomić coś podobnego do tego? (Działał w Rails2, usunięty w Rails3).
nzifnab
Zakładając, że @personjest to zmienna gdzieś w kontrolerze, to rozwiązanie oznacza, że ​​osoby czytające klasę modelu nie będą w stanie zrozumieć wywołań zwrotnych. Zobaczą after_create :something_cooli pomyślą „świetnie, po stworzeniu dzieje się coś fajnego!”. Aby właściwie zrozumieć twoją klasę modelu, będą musieli przeszukać wszystkie kontrolery, szukając wszystkich małych miejsc, w których zdecydowałeś się wprowadzić logikę. Nie podoba mi się to> o <;;
Ziggy
1
wymienić skip_callback ..., if: :skip_some_callbacksze after_create ..., unless: :skip_some_callbacksuruchomić to właściwie z after_create.
sakurashinken
28

Zaktualizowano:

Rozwiązanie @Vikrant Chaudhary wydaje się lepsze:

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

Moja pierwotna odpowiedź:

zobacz ten link: Jak pominąć wywołania zwrotne ActiveRecord?

w Rails3,

załóżmy, że mamy definicję klasy:

class User < ActiveRecord::Base
  after_save :generate_nick_name
end 

Podejście 1:

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

Podejście 2: Jeśli chcesz pominąć je w swoich plikach rspec lub czymkolwiek, spróbuj tego:

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

UWAGA: po wykonaniu tej czynności, jeśli nie jesteś w środowisku rspec, powinieneś zresetować wywołania zwrotne:

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

u mnie działa dobrze na szynach 3.0.5

Siwei Shen 申思维
źródło
20

szyny 3:

MyModel.send("_#{symbol}_callbacks") # list  
MyModel.reset_callbacks symbol # reset
guai
źródło
11
Miły. Również MyModel.skip_callback (: create,: after,: my_callback) dla precyzyjnej kontroli .. zobacz dokumentację ActiveSupport :: Callbacks :: ClassMethods dla wszystkich lobang
tardate
4
Przydatne informacje: „symbol” reset_callbacksnie jest :after_save, ale raczej :save. apidock.com/rails/v3.0.9/ActiveSupport/Callbacks/ClassMethods/ ...
nessur
19

Jeśli celem jest po prostu wstawienie rekordu bez wywołań zwrotnych lub walidacji, a chciałbyś to zrobić bez uciekania się do dodatkowych klejnotów, dodawania sprawdzeń warunkowych, używania RAW SQL lub w jakikolwiek inny sposób z zamykającym kodem, rozważ użycie „shadow obiekt ”wskazujący na istniejącą tabelę db. Tak jak to:

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

Działa to z każdą wersją Railsów, jest bezpieczne dla wątków i całkowicie eliminuje wszelkie walidacje i wywołania zwrotne bez modyfikacji istniejącego kodu. Możesz po prostu wrzucić tę deklarację klasy tuż przed rzeczywistym importem i powinieneś być gotowy. Pamiętaj tylko, aby użyć nowej klasy do wstawienia obiektu, na przykład:

ImportedPerson.new( person_attributes )
Brad Werth
źródło
4
Najlepsze rozwiązanie KIEDYKOLWIEK. Eleganckie i proste!
Rafael Oliveira,
1
To zadziałało dla mnie naprawdę fajnie, ponieważ było to coś, co chciałem zrobić tylko w teście, aby zasymulować stan bazy danych „przed”, bez zanieczyszczania mojego obiektu modelu produkcyjnego maszynami, aby opcjonalnie pominąć wywołania zwrotne.
Douglas Lovell
1
Zdecydowanie najlepsza odpowiedź
robomc
1
Pozytywnie, ponieważ pokazuje, jak obejść istniejące ograniczenia szyn i pomogło mi zrozumieć, jak naprawdę działa cały obiekt MVC. Tak proste i czyste.
Michael Schmitz
17

Możesz spróbować czegoś takiego w swoim modelu Person:

after_save :something_cool, :unless => :skip_callbacks

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

EDYCJA: after_save nie jest symbolem, ale przynajmniej po raz tysięczny próbowałem to zrobić.

Sarah Mei
źródło
1
Naprawdę uważam, że to najlepsza odpowiedź. W ten sposób logika, która określa, kiedy wywołanie zwrotne jest pomijane, jest dostępna w modelu i nie masz wszędzie szalonych fragmentów kodu, które odbijają logikę biznesową lub omijają hermetyzację za pomocą send. KOODOS
Ziggy
10

Możesz użyć update_columns:

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

Aktualizuje podane atrybuty obiektu, bez wywoływania funkcji save, co powoduje pominięcie walidacji i wywołań zwrotnych.

Luís Ramalho
źródło
7

Jedynym sposobem uniknięcia wszystkich wywołań zwrotnych after_save jest ustawienie pierwszego zwracającego false.

Być może mógłbyś spróbować czegoś takiego (niesprawdzone):

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
źródło
1
Uwielbiam próbować (niesprawdzone). Dreszcz jazdy.
Adamantish
Przetestowane i działa. Myślę, że to bardzo dobre i czyste rozwiązanie, dzięki!
kernification
5

Wygląda na to, że jednym ze sposobów radzenia sobie z tym problemem w Railsach 2.3 (ponieważ aktualizacja_without_callbacks już nie istnieje, itd.), Byłoby użycie update_all, jednej z metod pomijających wywołania zwrotne zgodnie z sekcją 12 Przewodnika po walidacji i wywołania zwrotne Rails .

Zwróć również uwagę, że jeśli robisz coś w swoim wywołaniu zwrotnym after_, który wykonuje obliczenia oparte na wielu skojarzeniach (np. Asoc_as_many, gdzie również akceptujesz_nested_attributes_for), będziesz musiał ponownie załadować skojarzenie, na wypadek gdyby było częścią zapisywania , jeden z jego członków został usunięty.

chrisrbailey
źródło
4

https://gist.github.com/576546

po prostu zrzuć tę małpą łatkę do config / initializers / skip_callbacks.rb

następnie

Project.skip_callbacks { @project.save }

lub tym podobne.

wszystkie zasługi dla autora

frędzlami
źródło
4

W up-votedniektórych przypadkach większość odpowiedzi może wydawać się zagmatwana.

Możesz użyć prostego ifsprawdzenia, czy chcesz pominąć oddzwonienie, na przykład:

after_save :set_title, if: -> { !new_record? && self.name_changed? }
Aleks
źródło
3

Rozwiązaniem, które powinno działać we wszystkich wersjach Railsów bez użycia klejnotu lub wtyczki, jest po prostu bezpośrednie wydawanie instrukcji update. na przykład

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

Może to być opcja (lub nie) w zależności od stopnia złożoności aktualizacji. Działa to dobrze na przykład flagi aktualizowanie na rekord od wewnątrz na after_save zwrotnego (bez ponownego wyzwolenia wywołania zwrotnego).

Dave Smylie
źródło
Nie jestem pewien, dlaczego głosowano przeciw, ale nadal uważam, że powyższa odpowiedź jest uzasadniona. Czasami najlepszym sposobem uniknięcia problemów z zachowaniem ActiveRecord jest unikanie używania ActiveRecord.
Dave Smylie,
Głosowano za zasadniczo przeciw -1. Właśnie mieliśmy problem z produkcją (z długą historią), który wymagał od nas stworzenia nowego rekordu (nie aktualizacji), a wywołanie zwrotne byłoby katastrofalne. Wszystkie powyższe odpowiedzi to hacki, niezależnie od tego, czy się do tego przyznają, czy nie, a udanie się do bazy danych było najlepszym rozwiązaniem. Istnieją ku temu uzasadnione warunki. Chociaż należy uważać na wstrzyknięcie SQL z rozszerzeniem #{...}.
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
źródło
1

Żaden z tych punktów nie wskazuje na without_callbackswtyczkę, która po prostu robi to, czego potrzebujesz ...

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 działa z Railsami 2.x

kares
źródło
1

Napisałem wtyczkę, która implementuje update_without_callbacks w Railsach 3:

http://github.com/dball/skip_activerecord_callbacks

Myślę, że właściwym rozwiązaniem jest przepisanie modeli, aby przede wszystkim uniknąć wywołań zwrotnych, ale jeśli jest to niepraktyczne w najbliższym czasie, ta wtyczka może pomóc.

Piłka Donalda
źródło
1

Jeśli używasz Railsów 2. Możesz użyć zapytania SQL do aktualizacji swojej kolumny bez wywołań zwrotnych i walidacji.

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

Myślę, że powinno działać w każdej wersji szynowej.

oivoodoo
źródło
1

Gdy potrzebuję pełnej kontroli nad wywołaniem zwrotnym, tworzę inny atrybut, który jest używany jako przełącznik. Prosty i skuteczny:

Model:

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

Test:

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
źródło
1

Do tworzenia danych testowych w Railsach używasz tego hacka:

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

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

Wojtek Kruszewski
źródło
1

Możesz użyć klejnotu podstępnego zapisu: https://rubygems.org/gems/sneaky-save .

Pamiętaj, że nie może to pomóc w zapisywaniu skojarzeń bez walidacji. Zgłasza błąd „created_at nie może być null”, ponieważ bezpośrednio wstawia zapytanie sql w przeciwieństwie do modelu. Aby to zaimplementować, musimy zaktualizować wszystkie automatycznie generowane kolumny bazy danych.

Zinin Serge
źródło
1

Potrzebowałem rozwiązania dla Rails 4, więc wymyśliłem to:

app / models / problems / 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

w dowolnym modelu:

include SaveWithoutCallbacks

wtedy możesz:

record.save_without_callbacks

lub

Model::WithoutCallbacks.create(attributes)
Steve Friedman
źródło
0

Dlaczego miałbyś chcieć móc to robić podczas programowania? Z pewnością będzie to oznaczać, że tworzysz aplikację z nieprawidłowymi danymi i jako taka będzie zachowywała się dziwnie, a nie tak, jak oczekujesz w produkcji.

Jeśli chcesz zapełnić swoją bazę deweloperską danymi, lepszym podejściem byłoby zbudowanie zadania prowizji, które wykorzystywało fałszywy klejnot do zbudowania prawidłowych danych i zaimportowanie ich do bazy danych, tworząc tyle rekordów, ile chcesz, ale jeśli jesteś na pięcie pochyliłem się nad tym i mam dobry powód Myślę, że update_without_callbacks i create_without_callbacks będą działać dobrze, ale kiedy próbujesz zgiąć szyny zgodnie ze swoją wolą, zapytaj siebie, masz dobry powód i czy to, co robisz, jest naprawdę dobrym pomysłem.

nitecoder
źródło
Nie próbuję zapisywać bez walidacji, tylko bez wywołań zwrotnych. Moja aplikacja używa wywołań zwrotnych do zapisywania statycznego kodu HTML w systemie plików (coś w rodzaju CMS). Nie chcę tego robić podczas ładowania danych deweloperskich.
Ethan
To była tylko myśl, wydaje mi się, że kiedykolwiek w przeszłości widziałem tego rodzaju pytanie, próbując obejść różne rzeczy ze złych powodów.
nitecoder
0

Jedną z opcji jest posiadanie oddzielnego modelu takich manipulacji przy użyciu tej samej tabeli:

class NoCallbacksModel < ActiveRecord::Base
  set_table_name 'table_name_of_model_that_has_callbacks'

  include CommonModelMethods # if there are
  :
  :

end

(To samo podejście może ułatwić omijanie walidacji)

Stephan

Stephan Wehner
źródło
0

Innym sposobem byłoby użycie haków walidacji zamiast wywołań zwrotnych. Na przykład:

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

W ten sposób możesz domyślnie uzyskać do_something, ale możesz łatwo nadpisać to:

@person = Person.new
@person.save(false)
Ryan Crispin Heneise
źródło
3
Wydaje się, że to zły pomysł - powinieneś używać rzeczy zgodnie z ich przeznaczeniem. Ostatnią rzeczą, jakiej chcesz, są walidacje, które mają skutki uboczne.
chug2k
0

Coś, co powinno działać ze wszystkimi wersjami ActiveRecordbez w zależności od opcji lub metod activerecord, które mogą istnieć lub nie.

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: użyj „innego modelu aktywnego rekordu” w tej samej tabeli

choonkeat
źródło
0

W przypadku niestandardowych wywołań zwrotnych użyj attr_accessori unlessw wywołaniu zwrotnym.

Zdefiniuj swój model w następujący sposób:

class Person << ActiveRecord::Base

  attr_accessor :skip_after_save_callbacks

  after_save :do_something, unless: :skip_after_save_callbacks

end

A następnie, jeśli chcesz zapisać rekord bez uderzania w after_savezdefiniowane wywołania zwrotne, ustaw skip_after_save_callbacksatrybut wirtualny na 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
źródło
-5

Nie jest to najczystszy sposób, ale możesz opakować kod wywołania zwrotnego w warunku, który sprawdza środowisko Railsów.

if Rails.env == 'production'
  ...
James
źródło