Ruby on Rails - Jak działa where?

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.

3 Likes

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 :slight_smile:

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!