Hej,
Zastanawia mnie, gdzie (oprócz przeglądania kodu źródłowego Rails’ów, bo tego próbowałem, ale chyba nie posiadam tyle czasu by wgryść się w to) znajdę opisany sposób jak działa “where” w Ruby on Rails.
O co mi chodzi?
Otóż jeśli wywołamy np. trzy razy where
, czy też innej FinderMethod na klasie Document, to zostanie wygenerowane tylko jedno zapytanie sql, jakby złożenie tych where’ów.
Document.where(user: user).where(salary: salary).find_by(id: id)
Ciekawi mnie jaki mechanizm za tym stoi, bo jakby Document “wiedział”, że są wywołane trzy FinderMethods (a nie 4, czy 2, czy 5), i z tych trzech metod formowane jest jedno zapytanie do bazy. Czyli wykonanie nastepuje po ‘find_by’, a nie wcześniej. Będe bardzo wdzięczny za jakieś materiały na ten temat.
Klasa w żadnym wypadku nie wie ile jak długi jest łańuch. Nie ma takiej możliwości, w końcu łańcuch może nie być z góry określony. Dla przykładu:
scope = Document.where(user: user)
scope = scope.where(salary: salary) if options[:salary]
# itp.
Na czym zatem polega magia? Na tym, że metody typu where, all, order nie wykonują zapytania tylko dokładają do budowanego obiektu nowe warunki. No ale zaraz zaraz. Przecież każdy kto pracuje w konsoli rails (irb/pry) wie, że wpisanie poniższego kodu spowoduje wywołanie zapytania sql:
pry(main)> User.where(name: "foo")
User Load (0.6ms) SELECT "users".* FROM "users" WHERE "users"."name" = ? [["name", "foo"]]
=> []
Problem w tym, że pry/irb, by wyświetlić jakiś obiekt wywołuje na nim jedną z metod to_s/inspect
, a to jest jedna z metod, która powoduje wykonanie zapytania. By zweryfikować tę teorię dodajmy ; nil
(w pry tak naprawdę wystarczy sam średnik) na końcu lini (wtedy wyrażenie zwróci nil i to on zostanie wyświetlony na ekranie):
User.where(name: "foo"); nil
=> nil
Jak widać zapytanie nagle przestało być wykonywane. Dla porównania wersja z inspectem:
pry(main)> User.where(name: "foo").inspect; nil
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."name" = ? [["name", "foo"]]
=> nil
Dopiero wywołanie niektórych metod spowoduje fizyczne wykonanie się zapytania.
Inną metodą, która powoduje wywołanie zapytania i załadowanie rekordów z bazy jest metoda each
. Żeby sprawdzić gdzie jest ona zdefiniowana najprościej jest skorzystać z komendy $
dostępnej w narzędziu pry (taki lepszy irb).
$ User.where(name: "foo").each
From: /Users/radarek/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/activerecord-5.0.0.1/lib/active_record/relation/delegation.rb @ line 38:
Owner: ActiveRecord::Delegation
Visibility: public
Number of lines: 3
delegate :to_xml, :encode_with, :length, :collect, :map, :each, :all?, :include?, :to_ary, :join,
:[], :&, :|, :+, :-, :sample, :reverse, :compact, :in_groups, :in_groups_of,
:shuffle, :split, to: :records
I naszym oczom ukazała się cała lista metod, która są delegowane do metody records
. Jak można podejrzewać, ta metoda ładuje rekordy z bazy:
$ User.where(name: "foo").records
From: /Users/radarek/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/activerecord-5.0.0.1/lib/active_record/relation.rb @ line 259:
Owner: ActiveRecord::Relation
Visibility: public
Number of lines: 4
def records # :nodoc:
load
@records
end`
$ User.where(name: "foo").load
From: /Users/radarek/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/activerecord-5.0.0.1/lib/active_record/relation.rb @ line 579:
Owner: ActiveRecord::Relation
Visibility: public
Number of lines: 5
def load
exec_queries unless loaded?
self
end
Ufam, że metoda exec_queries
robi to co jej nazwa sugeruje więc tutaj zakończę :-). Jako dodatkowe ćwiczenie możesz sprawdzić gdzie jest zdefiniowana metoda inspect i potwierdzić, że faktycznie ładuje rekordy przez wywołanie metody records
.
Dziękuję za odpowiedź!
Bardzo mi to rozjaśniło sprawę.
Zdawałem sobie sprawę, że nie ma możliwości, by klasa wiedziała, kiedy łańcuch metod się kończy. Opisałem po prostu jak to niby wygląda
Jeszcze małe pytanie:
Opisałeś jak to wygląda w wypadku irb/pry. Czy taki sam mechanizm występuje podczas np. interpretacji kodu produkcyjnego? Interpreter dodaje wywołania inspect
na końcu takiego łańcucha metod?
Nic nie dodaje. To ty dodajesz ;). W końcu gdzieś w kodzie wywołujesz np. metodę each
, która spowoduje wykonanie zapytania. Przykładowo w kontrolerze masz akcję index i w niej @users = User.all
- po wykonaniu akcji zapytanie nie zostało wykonane. Jeśli w widoku masz pętlę wyświetlającą rekordy @users.each do |user| ...
to zapytanie zostanie wywołane tuż przed wykonaniem pętli (trzeba dodać, że tylko raz, niezależnie od ilości wywołań each
).
A no tak!
Dzięki wielki!