2015-10-27 8 views
9

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?

+0

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. –

+0

Nel mio test non è successo. –

risposta

3

La ragione per questo sta accadendo è perché il rapporto tra A e C è attraverso B.

Prima di assegnare a.b = b, a non ha bs o cs.

Se si assegna a.b = b ma non si chiama a.cs, quindi a non ha motivo di cercare di caricare l'associato cs. has_many crea solo il metodo di convenienza cs, non lo chiama per te. Qui solo a.b_id è impostato su b.id.

Una volta chiamato a.cs, a cercherà associati cs oggetti attraverso b dal b è disponibile. Troverà quegli oggetti e li aggiungerà come figli a a.

Vedo il vostro punto, che tecnicamente, non c'è niente da fare in questo caso particolare in questo particolare schema per il cs, ma posso vedere perché ActiveRecord sta controllando. Questi oggetti, per quanto li riguarda, sono figli di a e i record figli sono convalidati a meno che non sia specificamente detto di non farlo tramite validate: false.

In questo caso, a è un figlio di b, quindi a non è necessario per convalidarlo.

In generale, i genitori faranno convalidare i figli associati. I bambini non devono convalidare i loro genitori.

Problemi correlati