Ho trovato una situazione in cui ActiveRecord convalida i record figlio apparentemente inutilmente. Mi scuso in anticipo per la lunghezza in quanto questo è abbastanza complesso.ActiveRecord sembra convalidare inutilmente i record figlio immutati
Ciò avviene tramite associazioni precedentemente utilizzate ma non modificate in alcun modo. Si verifica dal 3.2 al master recente. Non sono sicuro che si tratti di una decisione di progettazione che ha portato a un comportamento inaspettato o a un bug di qualche tipo.
ho ridotto un banco di prova dal codice attuale come segue:
Modelli:
class A < ActiveRecord::Base
belongs_to :b
has_many :cs, :through => :b
before_validation { puts "A" }
end
class B < ActiveRecord::Base
has_many :as
has_many :cs
before_validation { puts "B" }
end
class C < ActiveRecord::Base
belongs_to :b
before_validation { puts "C" }
end
migrazione:
class AddABC < ActiveRecord::Migration
def change
create_table :as do |t|
t.references :b
end
create_table :bs do |t|
end
create_table :cs do |t|
t.references :b
end
end
end
Il caso di prove ridotto che lo attiva è questo periodo, quando su un database vuoto:
b = B.create!
c = C.create!
b.cs << c
a = A.new
a.b = b
a.cs.first
puts "X"
a.valid?
che dà uscita:
B
C
C
X
A
C
Il che dimostra che la convalida di un A convalida la sua Cs.
Ora, dopo aver esaminato ciò, sono a conoscenza dell'opzione has_many :validate => false
e, utilizzandola, il problema scompare. Ma a me sembra che ci sia di più qui oltre a questo: portami dietro.
Il AR docs say:
: validare Se false, non convalidare gli oggetti associati quando si salva l'oggetto padre. true per impostazione predefinita.
ma trovo questa confusione come questo chiaramente non può significare tutti record. Non convaliderà gli oggetti se non ottengo mai l'associazione (rimuovi a.cs.first
dal codice qui sopra), o lo prendo ma non lo uso mai (sostituisci con a.cs
). Questo perché passa attraverso validate_collection_association
in lib/active_record/autosave_association.rb
che include il codice:
def validate_collection_association(reflection)
if association = association_instance_get(reflection.name)
if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave])
records.each_with_index { |record, index| association_valid?(reflection, record, index) }
end
end
end
E 'tutto subordinato association_instance_get
, che recupera dalla cache associazione. Nessuna cache significa nessun record da convalidare.
Ho provato a fare un semplicissimo has_many, impostando solo un modello B che fa riferimento a A, ma poi avrò bisogno di creare il B prima di A, quindi A non sarà più un nuovo record se provo per salvarlo, e questo codice impedisce il problema come il ramo chiamato non sarà più la prima:
def associated_records_to_validate_or_save(association, new_record, autosave)
if new_record
association && association.target
elsif autosave
association.target.find_all(&:changed_for_autosave?)
else
association.target.find_all(&:new_record?)
end
end
l'unica vera spiegazione che posso venire con per solo convalidare i record caricati è perché l'intenzione di ActiveRecord qui è quello di convalidare solo i record modificati. Davvero mi aspetterei che convalidi se e solo se sta per salvare, e quindi l'opzione di salvataggio automatico di default di salvare solo i record modificati dovrebbe impedire una convalida.
Ho trovato un related ticket e il commit 27aa4dda7d89ce733 (non ancora in alcuna versione credo) che apporta una modifica ma non risolve questo problema specifico dai miei test.Ha, tuttavia contiene l'espressione:
!record.persisted? || record.changed? || record.marked_for_destruction?
e se posso aggiungere questa condizione al ciclo più interno del validate_collection_association
allora il problema va via, con le prove di ActiveRecord ancora passando sulla mia macchina.
Questo è stato un problema di prestazioni significativo nel mio progetto perché il modello in questione doveva essere convalidato solo in admin, dove un campo non indicizzato utilizzato in una convalida personalizzata era accettabile a causa della rarità di esso è stato salvato e quindi I giudicato che indicizzare sarebbe eccessiva indicizzazione (non sarebbe solo un campo). Ovviamente nella maggior parte dei casi questa sovra-validazione sarebbe molto meno seria, e sembra solo che accada in un caso abbastanza specifico, quindi questo potrebbe essere un bug.
Quindi, mentre ho una buona idea di cosa sta succedendo, non sono del tutto sicuro di cosa dovrebbe accadere, motivo per cui non l'ho registrato come biglietto ActiveRecord. Pensi che questo sia un bug? Perché funziona così? A cosa serve veramente l'opzione di convalida? Se questo è un bug, puoi spiegare perché il codice funziona in questo modo e perché è eccessivo? In quale caso il mio codice passerà ad ActiveRecord sopra l'interruzione?
Sarei interessato a vedere se l'aggiunta di inverse_of: sulle associazioni ha cambiato questo comportamento. TBH Non ho grandi speranze che lo sarebbe, comunque. –
Nel mio test non è successo. –