2016-05-23 59 views
6

che sto utilizzando il metodo AR includes per eseguire un join esterno sinistro tra gli oggetti utente e costruzione, dove un utente può o non può avere un'associazione di costruzione:comportamento imprevisto con ActiveRecord comprende

users = User.includes(:building).references(:buildings) 

Dato che sto usando references, qualsiasi oggetto Building associato sarà caricato con entusiasmo.

La mia aspettativa era che sarei stato in grado di scorrere l'elenco degli utenti e controllare se un utente aveva un edificio associato con loro senza attivare query aggiuntive, ma vedo che in effetti ogni volta che provo ad accedere all'edificio proprietà di un utente che non ne ha uno, AR effettua un'altra chiamata SQL per provare a recuperare tale edificio (anche se nei tentativi successivi restituirà solo zero).

Queste query sono ovviamente ridondanti in quanto l'associazione sarebbe stata caricata durante il join iniziale e sembra vanificare l'intero scopo del caricamento ansioso con include/riferimenti, poiché ora sto osservando N volte il numero di query uguali al numero di associazioni vuote.

users.each do | user | 

    # This will trigger a new query when building is not present: 
    # SELECT "buildings".* FROM "buildings" WHERE "buildings"."address" = $1 LIMIT 1 [["address", "123 my street"]] 
    if user.building 
    puts 'User has building' 
    else 
    puts 'User has no building' 
    end 

end 

classe User:

class User < ActiveRecord::Base 
    belongs_to :building, foreign_key: 'residence_id' 
end 

C'è un modo per verificare la presenza di un'associazione costruzione degli utenti senza innescare query in più?


On Rails 4.2.0/POSTGRES


UPDATE:

Grazie @BoraMa per mettere insieme questo test. Sembra che stiamo ottenendo un comportamento differente tra le versioni Rotaie recenti:

USCITA (rotaie 4.2.0):

User 1 has building 
User 2 has building 
User 3 has no building 
D, [2016-05-26T11:48:38.147316 #11910] DEBUG -- : Building Load (0.2ms) SELECT "buildings".* FROM "buildings" WHERE "buildings"."id" = $1 LIMIT 1 [["id", 123]] 
User 4 has no building 

USCITA (rotaie 4.2.6)

User 1 has building 
User 2 has building 
User 3 has no building 
User 4 has no building 

USCITA (rotaie 5.0 .0)

User 1 has building 
User 2 has building 
User 3 has no building 
User 4 has no building 

rosticcerie:

  • Questo problema è stato limitato a "penzolanti chiavi esterne (cioè la colonna residence_id non è nullo, ma non corrisponde alcun oggetto edificio)" (Grazie @FrederickCheung)
  • Il problema è stato risolto alla Rails 4.2. 6
+0

Mi aspetterei un errore simile a "ActiveRecord :: AssociationNotFoundError: associazione denominata" edifici "non trovata sull'utente". Puoi confermare la sintassi della query e User-> Building association definition? – messanjah

+0

@messanjah Personalmente mi aspetterei che 'user.building' restituisca nil se l'associazione non esiste, e questo è ciò che succede nelle chiamate successive, ma alla prima chiamata spara sempre una query SQL. – Yarin

+0

Strano, non osservo questo comportamento - le mie associazioni nil stanno tornando a zero, senza ulteriori domande. Il codice che hai fornito nella domanda è davvero un esempio operativo minimo del problema? – BoraMa

risposta

3

Suona come hai morso da un bug in Active Record, che è stato fissato in Rails 4.2.3.

Nel caso in cui la colonna fosse nil, Active Record sa già che non è nemmeno necessario provare a caricare l'oggetto associato. I casi rimanenti sono stati quelli interessati da questo errore

+0

Per la vittoria! Bella scoperta, grazie per tutto il tuo aiuto questo mi stava uccidendo – Yarin

+1

Aah, troppo tardi! Ma te lo meriti, Frederick :) Sono contento che abbiamo tutti inchiodato questo bug interessante. – BoraMa

0

sembra un errore di battitura, si prega di notare building invece di buildings: User.includes(:building).references(:buildings)

Questo dovrebbe far scattare il grande query che utilizza il formato di AS tX_rY per ogni associazione e tabl e.

+0

Grazie per aver segnalato l'errore, ma questo non risolve il problema – Yarin

0

Sembra che dal binario 4.1 ci siano potenziali scontri con il modo in cui dovrebbero essere inclusi gli #inclusi impliciti, vedere il seguente open issue.

Questo codice è tutto testato per la sintassi, ma ci sarebbero due approcci vorrei provare:

1/rendono il eager loading implicita

users = User.eager_load(:building).preload(:buildings) 

2/separare i due tipi di utenti, quelli in cui l'edificio è collegato, il che significa che non provi nemmeno a precaricare l'edificio, rimuovendo l'inefficienza.

users = User.includes(:building).where.not(residence_id: nil).references(:buildings) 

users.each do | user| 
    puts "User has building: #{user} #{user.building}" 
end 

# No additional references needed to be eager-loaded. 
users = User.where(residence_id: nil) 
users.each do | user | 
    puts "#{User} has no building." 
end 
+0

Grazie per i suggerimenti. # 1 non ha modificato i risultati - la query iniziale era la stessa, come lo era il comportamento durante l'accesso alle associazioni mancanti. Per quanto riguarda il n. 2, la suddivisione in due query separate funzionerebbe per questo esempio limitato, ma non un'opzione nelle applicazioni reali in cui dovremmo eseguire operazioni di filtraggio/paging nell'intera query – Yarin