Jeżeli jesteś przyzwyczajony do używania czystego SQL do znajdowania rekordów w bazie, dowiesz się, że są na to lepsze sposoby w Railsach. Moduł Active Record odcina cię od konieczności używania SQL w większości przypadków.
Kod przedłożony w tym podręczniku nawiązuje do jednego lub większej ilości następujących modeli:
Wszystkie następujące modele używają id jako klucza głównego, jeśli nie jest to określone w inny sposób.
class Client < ActiveRecord::Base
has_one :address
has_many :orders
has_and_belongs_to_many :roles
end
class Address < ActiveRecord::Base
belongs_to :client
end
class Order < ActiveRecord::Base
belongs_to :client, :counter_cache => true
end
class Role < ActiveRecord::Base
has_and_belongs_to_many :clients
end
Moduł Active Record wykonuje dla ciebie zapytania w bazie danych i jest kompatybilny z większością systemów (MySQL, PostgreSQL and SQLite by wymienić kilka). Niezależnie od tego, jakiego systemu używasz, forma zapytań w module Active Record zawsze będzie taka sama.
1 Uzyskiwanie obiektów z bazy danych
Aby pobrać rekord z bazy danych, moduł Active Record proponuje metodę klasową zwaną Model.find. Pozwala ona na przekazwywanie do niej argumentów, aby przygotwać pewne zapytania bez konieczności pisania ich w czystym SQL.
Podstawowe funkcjonalności Model.find(options) to:
- konwertowanie dostarczonej opcji do zapytania SQL
- wykonanie zapytania i otrzymanie odebranie odpowiednich wyników z bazy
- stworzenie odpowiedniego egzemplarza obiektu Ruby dla właściwego modelu w związku z każdą zwróconą z bazy linią
- wykonanie callbacków after_find, jeśli takowe są zadeklarowane.
1.1 Uzyskiwanie pojedynczego obiektu
Moduł Active Record umożliwia Ci uzyskanie pojedynczego obiektu na trzy sposoby.
1.1.1 Używanie klucza głównego
Używając Model.find(primary_key, options = nil), możesz otrzymać obiekty odpowiadające podanemu kluczowi głównemu i pasujące do zadanych opcji, jeśli zostały określone. Na przykład:
# Find the client with primary key (id) 10.
client = Client.find(10)
=> #<Client id: 10, first_name: => "Ryan">
Odpowiednik SQL powyższego jest następujący:
SELECT * FROM clients WHERE (clients.id = 10)
Model.find(primary_key) zgłosi wyjątek ActiveRecord::RecordNotFound jeśli nie znajdzie pasujących do zapytania rekordów.
1.1.2 first
Model.first(options = nil) szuka pierwszego rekordu pasującego do zadanej opcji. Jeśli żadne opcja nie są określone, zwracany jest pierwszy pasujący rekord. Na przykład:
client = Client.first
=> #<Client id: 1, first_name: => "Lifo">
Odpowiednik SQL powyższego jest następujący:
SELECT * FROM clients LIMIT 1
Model.first zwróci nil, jeśli nie zostanie znaleziony żaden pasujący rekord. Żaden wyjątek nie zostanie zgłoszony.
Model.find(:first, options) jest odpowiednikiem Model.first(options).
1.1.3 last
Model.last(options = nil) szuka ostatniego rekordu pasującego do zadanej opcji. Jeśli żadne opcja nie są określone, zwracany jest ostatni pasujący rekord. Na przykład:
client = Client.last
=> #<Client id: 221, first_name: => "Russel">
Odpowiednik SQL powyższego jest następujący:
SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1
Model.last zwróci nil, jeśli nie zostanie znaleziony żaden pasujący rekord. Żaden wyjątek nie zostanie zgłoszony.
Model.find(:last, options) jest odpowiednikiem Model.last(options).
1.2 Uzyskiwanie wielu obiektów
1.2.1 Używanie wielu kluczy głównych
Model.find(array_of_primary_key, options = nil) akceptuje również tablicę kluczy głównych. Zwracana jest tablica rekordów pasujących do do podanych kluczy głównych. Na przykład:
# Find the clients with primary keys 1 and 10.
client = Client.find(1, 10) # Or even Client.find([1, 10])
=> [#<Client id: 1, first_name: => "Lifo">, #<Client id: 10, first_name: => "Ryan">]
Odpowiednik SQL powyższego jest następujący:
SELECT * FROM clients WHERE (clients.id IN (1,10))
Model.find(array_of_primary_key) zgłosi wyjątek ActiveRecord::RecordNotFound jeśli dla żadnego z kluczy nie zostanie znaleziony żaden pasujący rekord.
1.2.2 Znajdowanie wszystkich
Model.all(options = nil) znajduje wszystkie rekordy pasujące do zadanej opcji. Jeśli żadna opcja nie została określona, zostaną zwrócone wszystkie rekordy z bazy.
# Find all the clients.
clients = Client.all
=> [#<Client id: 1, name: => "Lifo">, #<Client id: 10, name: => "Ryan">, #<Client id: 221, name: => "Russel">]
Odpowiednik SQL powyższego jest następujący:
SELECT * FROM clients
Model.all zwraca pustą tablicę array[], jeśli nie znajdzie pasujących rekordów. Żaden wyjątek nie zostanie zgłoszony.
Model.find(:all, options) jest odpowiednikiem Model.all(options).
1.3 Uzyskiwanie wielu obiektów porcjami
Czasami musisz iterować na dużym zbiorze rekordów, na przykład aby wysłać newsletter lub eksportować dane, itd.
Zacznijmy od tego:
# Very inefficient when users table has thousands of rows.
User.all.each do |user|
NewsLetter.weekly_deliver(user)
end
Ale jeżeli liczba rekordów w tabeli jest bardzo duża, powyższe rozwiązanie może być trudno wykonalne, lub po prostu niemożliwe.
Dzieje się tak, ponieważ moduł Active Record ściąga całą tabelę, buduje po linii obiekt modelu i zapisuje całą tablicę w pamięci. Czasami jest po prostu zbyt wiele obiektów wymagających zbyt wiele pamięci.
1.3.1 find_each
Aby efektywnie iterować na dużych tablicach, moduł Active Record oferuje metodę wyszukującą porcjami zwaną find_each:
User.find_each do |user|
NewsLetter.weekly_deliver(user)
end
Konfigurowanie rozmiaru porcji
Domyślnie find_each pobiera z bazy porcje po tysiąc rekordów i przywołuje je jeden po drugim. Rozmiar tych porcji jest konfigurowalny za pomocą opcji :batch_size.
Pobieranie rekordów User porcjami o rozmiarze 5000:
User.find_each(:batch_size => 5000) do |user|
NewsLetter.weekly_deliver(user)
end
Rozpoczynanie wyszukiwania porcjami od określonego klucza
Rekordy są pobierane w porządku rosnącym zgodnie z kluczem głównym, który musi być obiektem typu integer. Opcja :start umożliwia ci określenie początkowe ID wyszukiwania, jeżeli najniższe nie jest tym, od któego chcesz zacząć. Jest to przydatne, na przykład by móc wznowić przerwane wyszukiwanie, jeżeli zapamiętane zostało ID ostatniego pobranego obiektu.
Wysyłanie newslettera do użytkowników o ID wyższym niż 2000:
User.find_each(:batch_size => 5000, :start => 2000) do |user|
NewsLetter.weekly_deliver(user)
end
Dodatkowe opcje
find_each przyjmuje takie same opcje jak metoda find. Tak czy inaczej, :order i :limit są wewnętrznie potrzebne i w związku z tym nie wolno ich pomijać.
1.3.2 find_in_batches
Możesz pracować używając fragmentów tabel zamiast pojedynczych linii. Jest to metoda analogiczna do find_each, ale pobiera tablicę dla modelu.
# Works in chunks of 1000 invoices at a time.
Invoice.find_in_batches(:include => :invoice_lines) do |invoices|
export.add_invoices(invoices)
end
Powyższe przywoła zadany blok 1000 faktur za każdym razem.
2 Warunki
Metoda find pozwala określić warunki ograniczenia zwracanych rekordów, reprezentujących WHERE-part instrukcji SQL. Warunki mogą być określone jako ciąg znaków, tablica lub tablica asocjacyjna.
2.1 Warunki czystego łańcucha znaków
Jeśli do wyszukań chcesz dodać warunki, wystarczy, że je tam określisz: Client.first(:conditions => "orders_count = '2'"). Funkcja ta znajdzie wszystkich klientów, u których wartość pola orders_count wynosi 2.
Budowa własnych warunków czystego łańcucha może być narażona na ataki typu SQL injection. Na przykład, Client.first(:conditions => "name LIKE '%#{params[:name]}%'") nie jest bezpieczny. Przeczytaj następny rozdział, w którym preferowanym sposobem rozpatrywania warunków jest używanie tablicy.
2.2 Warunki tablicy
Ale co, jeżeli liczba może się zmieniać, być argumentem lub poziomem statusu użytkownika? Wtedy znalezienie odbywa się w następujący sposób:
Client.first(:conditions => ["orders_count = ?", params[:orders]])
Moduł Active Record przejdzie przez pierwszy element wartości warunku i wszystkie elementy dodatkowe pierwszego elementu zmieni w znaki zapytania (?) .
Jeśli chcesz określić dwa warunki, możesz to zrobić w następujący sposób:
Client.first(:conditions => ["orders_count = ? AND locked = ?", params[:orders], false])
W tym przykładzie pierwszy znak zapytania będzie zamieniony z wartością w params[:orders] Drugi natomiast zostanie zamieniony z SQL’ową reprezentacją false, która zależy od sterownika.
Powodem takiego pisania kodu:
Client.first(:conditions => ["orders_count = ?", params[:orders]])
zamiast takiego:
Client.first(:conditions => "orders_count = #{params[:orders]}")
są względy bezpieczeństwa argumentu. Wykorzystywanie zmiennej bezpośrednio w warunkach łańcucha znaków przekaże zmienną do bazy danych taką jaką jest. Oznacza to, że użytkownik, który może mieć złe intencje, przekaże nieprzefiltrowaną zmienną. Jeśli w ten sposób piszesz kod, narażasz swoją bazę danych na ryzyko, ponieważ gdy użytkownik stwierdzi, że może wykorzystać twoją bazę danych będzie mógł z nią zrobić co zechce. Nigdy przenigdy nie wykorzystuj argumentów bezpośrednio w warunkach łańcucha danych.
Więcej informacji o niebezpieczeństwach SQL injection znajdziesz w Ruby on Rails Security Guide.
2.2.1 Warunek zastępczy
Podobnie do (?) wymiennego stylu parametrów, w twoich warunkach tablicy możesz określać klucz/wartość tablicy asocjacyjnej:
Client.all(:conditions =>
["created_at >= :start_date AND created_at <= :end_date", { :start_date => params[:start_date], :end_date => params[:end_date] }])
Jeśli masz dużo zmiennych w warunku, to dzięki temu poprawisz czytelność kodu.
2.2.2 Zakres warunków
Jeśli szukasz zakresu w tabeli (na przykład, użytkowników utworzonych w określonych ramach czasowych) możesz użyć opcji warunku połączonych z instrukcją IN SQL. Jeśli masz dwie daty dołączenia z kontrolera, aby znaleźć zakres możesz zrobić coś takiego:
Client.all(:conditions => ["created_at IN (?)",
(params[:start_date].to_date)..(params[:end_date].to_date)])
Wygenerowane zostanie właściwe zapytanie, które doskonale się sprawdza dla małych przedziałów, ale nie najlepiej dla dużych. Na przykład, jeśli przekażesz w zakresie dat obejmujących obiekt, że rok to 365 (lub w zależności od roku 366) łańcuchów, to nastąpi próba dopasowania danej dziedzinie.
SELECT * FROM users WHERE (created_at IN
('2007-12-31','2008-01-01','2008-01-02','2008-01-03','2008-01-04','2008-01-05',
'2008-01-06','2008-01-07','2008-01-08','2008-01-09','2008-01-10','2008-01-11',
'2008-01-12','2008-01-13','2008-01-14','2008-01-15','2008-01-16','2008-01-17',
'2008-01-18','2008-01-19','2008-01-20','2008-01-21','2008-01-22','2008-01-23',...
‘2008-12-15','2008-12-16','2008-12-17','2008-12-18','2008-12-19','2008-12-20',
'2008-12-21','2008-12-22','2008-12-23','2008-12-24','2008-12-25','2008-12-26',
'2008-12-27','2008-12-28','2008-12-29','2008-12-30','2008-12-31'))
2.2.3 Warunki czasu i daty
Jeśli przejdziesz na obiekty czasowe, co będzie próbą porównania pola do co drugiego w tym zakresie, to może się zrobić prawdziwy bałagan:
Client.all(:conditions => ["created_at IN (?)",
(params[:start_date].to_date.to_time)..(params[:end_date].to_date.to_time)])
SELECT * FROM users WHERE (created_at IN
('2007-12-01 00:00:00', '2007-12-01 00:00:01' ...
'2007-12-01 23:59:59', '2007-12-02 00:00:00'))
Mogłoby to spowodować, że serwer bazy danych wywoła niespodziewany błąd, na przykład MySQL wyrzuci taki błąd:
Got a packet bigger than 'max_allowed_packet' bytes: _query_
Gdzie query jest aktualnym zapytaniem użytym do wywołania tego błędu.
W tym przykładzie byłoby lepiej użyć w SQL operatorów mniejszości i większości:
Client.all(:conditions =>
["created_at > ? AND created_at < ?", params[:start_date], params[:end_date]])
Możesz również użyć operatorów ‘większy lub równy’ bądź ‘mniejszy lub równy’:
Client.all(:conditions =>
["created_at >= ? AND created_at <= ?", params[:start_date], params[:end_date]])
Tak jak w języku Ruby. Jeśli chcesz używać skróconej składni sprawdź punkt Warunki tablic asocjacyjnych w przewodniku.
2.3 Warunki tablic asocjacyjnych
Moduł Active Record pozwala również na przejście w warunki tablic asocjacyjnych, które mogą zwiększyć czytelność twojej składni. Z warunkami tablic asocjacyjnych przechodzisz w tablice asocjacyjne z kluczami pól na których chcesz przeprowadzić warunek i wartościami ,według których chcesz przeprowadzić warunek:
W warunkach tablic asocjacyjnych są możliwe jedynie równości, zakres i sprawdzanie podzbioru.
2.3.1 Warunki równości
Client.all(:conditions => { :locked => true })
Pole name nie musi być symbolem, może być łańcuchem:
Client.all(:conditions => { 'locked' => true })
2.3.2 Warunki zakresu
Dobrą rzeczą jest to, że możemy przejść w zakresy z naszymi polami bez generowania olbrzymich zapytań, jak pokazano we wstępie tego rozdziału.
Client.all(:conditions => { :created_at => (Time.now.midnight - 1.day)..Time.now.midnight})
Używając instrukcji BETWEEN SQL, wyszukamy wszystkich klientów stworzonych wczoraj:
SELECT * FROM clients WHERE (clients.created_at BETWEEN '2008-12-21 00:00:00' AND '2008-12-22 00:00:00')
Przedstawia to skróconą składnię dla przykładu w „Warunki tablic”:#warunki-tablic
2.3.3 Warunek podzbioru
Jeśli chcesz wyszukać rekord używając wyrażenia IN, możesz przekazać tablicę do warunku tablicy asocjacyjnej:
Client.all(:conditions => { :orders_count => [1,3,5] })
Ten kod wygeneruje taki SQL:
SELECT * FROM clients WHERE (clients.orders_count IN (1,3,5))
3 Opcje wyszukiwania
Oprócz :conditions, Model.find ma wiele innych opcji, przez opcje tablic asocjacyjnych do dostosowania zbioru rekordów wynikowych.
Model.find(id_or_array_of_ids, options_hash)
Model.find(:last, options_hash)
Model.find(:first, options_hash)
Model.first(options_hash)
Model.last(options_hash)
Model.all(options_hash)
Następne rozdziały dają pogląd na wszystkie możliwe klucze dla options_hash.
3.1 Porządkowanie
Aby pobrać rekordy z bazy danych w określonej kolejności możesz określić opcję :order dla wywołania find.
Na przykład, jeśli pobierasz zbiór rekordów i chcesz uporządkować je rosnąco używając pola created_at w twojej tabeli:
Client.all(:order => "created_at")
Polecenie możesz również precyzować używając ASC lub DSC:
Client.all(:order => "created_at DESC")
# OR
Client.all(:order => "created_at ASC")
Lub porządkować według wielu pól:
Client.all(:order => "orders_count ASC, created_at DESC")
3.2 Wybór konkretnych pól
Domyślnie, Model.find wybiera wszystkie pola z wyszukanego zbioru używając select *.
Możesz określić podzbiór opcją :select, aby wybrać tylko podzbiór pól wyszukanego zbioru.
Jeśli używana jest opcja :select, wszystkie zwracane obiekty będą tylko do odczytu.
Na przykład, aby wybrać tylko kolumny viewable_by i locked:
Client.all(:select => "viewable_by, locked")
Zapytanie SQL użyte przez znalezienie połączenia będzie czymś w rodzaju:
SELECT viewable_by, locked FROM clients
Bądź ostrożny, ponieważ oznacza to również, że inicjalizujesz obiekt modelu tylko z pola, które wybrałeś. W przypadku próby uzyskania dostępu do pola, którego nie ma w zainicjowanych rekordach, otrzymasz:
ActiveRecord::MissingAttributeError: missing attribute: <attribute>
Where <attribute> is the attribute you asked for. Metoda id nie zgłosi ActiveRecord::MissingAttributeError, więc należy zachować ostrożność podczas pracy z asocjacjami. Aby działać poprawnie potrzebują one metody id.
Możesz również wywołać funkcję SQL bez opcji wyboru. Na przykład, Jeśli chciałbyś tylko pobrać pojedynczy rekord jako unikalną wartość w danej dziedzinie za pomocą funckji DISTINCT, możesz zrobić to tak:
Client.all(:select => "DISTINCT(name)")
3.3 Limit i przesunięcie (Offset)
Aby zastosować LIMIT w SQL uruchamianym przez Model.find, możesz określić LIMIT używając opcji :limit i :offset.
Jeśli chcesz ograniczyć ilość rekordów do pewnego podzbioru wszystkich wyszukanych rekordów, mógłbyś użyć :limit lub połączyć go z :offset. Limit, to maksymalna liczba rekordów, które będą pobierane z zapytania. Przesunięcie (offset), to numer rekordu, od którego rozpocznie się czytanie począwszy od pierwszego rekordu ze zbioru. Na przykład:
Client.all(:limit => 5)
Ten kod zwróci maksymalnie 5 klientów, a ponieważ przesunięcie nie zostało ustalone, zwróci pierwszych 5 klientów z tabeli. SQL, który to wykonuje wygląda następująco:
SELECT * FROM clients LIMIT 5
Lub określający i :limit i :offset:
Client.all(:limit => 5, :offset => 5)
Ten kod zwróci maksymalnie 5 klientów i ponieważ przesunięcie zostało tutaj określone zwróci te rekordy zaczynając od piątego klienta z listy klientów. SQL wygląda tak:
SELECT * FROM clients LIMIT 5, 30
3.4 Grupa
Aby zastosować klauzulę GROUP BY do SQL uruchomionego przez Model.find, możesz określić na wynikach opcję :group.
Na przykład, jeśli chcesz znaleźć kolekcję dat zamówień utworzonych jak poniżej:
Order.all(:group => "date(created_at)", :order => "created_at")
Zwrócony zostanie pojedynczy obiekt Order dla każdej daty dla której w bazie danych istnieje zamówienie.
SQL, który mógłby być wykonywany wyglądałby tak:
SELECT * FROM orders GROUP BY date(created_at)
3.5 Having
SQL używa klauzuli HAVING do określenia warunków do pól GROUP BY. Możesz określić klauzulę HAVING do SQL uruchomionego za pomocą Model.find używając opcji :having na wynikach.
Na przykład:
Order.all(:group => "date(created_at)", :having => ["created_at > ?", 1.month.ago])
Użyty SQL wyglądałby następująco:
SELECT * FROM orders GROUP BY date(created_at) HAVING created_at > '2009-01-15'
Zwróci to pojedyncze obiekty zamówień dla każdego dnia, ale tylko z ostatniego miesiąca.
3.6 Obiekty tylko do odczytu
Aby wyraźnie nie dopuścić do zmiany ani zniszczenia rekordów zwracanych przez Model.find, możesz określić na znalezionym wywołaniu opcję :readonly jako true.
Wszystkie próby zmiany lub znieszczenia rekordów tylko do odczytu zakończą się niepowodzeniem, zgłaszając modułowi ActiveRecord::ReadOnlyRecord wyjątek. Aby ustawić tę opcję, należy określić ją tak:
Client.first(:readonly => true)
Jeśli przypiszesz ten rekord do zmiennej klienta, wywołanie następującego kodu zgłosi modułow wyjątek ActiveRecord::ReadOnlyRecord:
client = Client.first(:readonly => true)
client.locked = false
client.save
3.7 Blokowanie rekordów do aktualizacji
Blokowanie jest pomocne w zapobieganiu wyścigu podczas aktualizacji rekordów w bazie danych i zapewnieniu niepodzielnej aktualizacji. Moduł Active Record oferuje dwa mechanizmy blokujące:
- blokowanie ptymistyczne
- blokowanie pesymistyczne
3.7.1 Blokowanie optymistyczne
Optymistyczne blokowanie umożliwia wielu użytkownikom dostęp do tych samych edytowalnych rekordów i zakłada minimalną sprzeczność z danymi. Czyni to poprzez sprawdzenie, czy inny proces dokonał zmiany zapisu od otwarcia rekordu. Wyjątek ActiveRecord::StaleObjectError jest odrzucany, jeśli nastąpiła zmiana, a aktualizacja jest ignorowana.
Kolumna optymistycznego blokowania
Aby korzystać z optymistycznego blokowania, tabela musi mieć kolumnę o nazwie lock_version. Za każdym razem, gdy wyniki są aktualizowane, moduł Active Record inkrementuje kolumnę lock_version. Programy blokujące umożliwiają – jeśli rekord został stworzony dukrotnie – zgłoszenie informacji o ostatnim zapisie ActiveRecord::StaleObjectError, o ile pierwszy był również aktualizacją. Przykład:
c1 = Client.find(1)
c2 = Client.find(1)
c1.first_name = "Michael"
c1.save
c2.name = "should fail"
c2.save # Raises a ActiveRecord::StaleObjectError
Jesteś odpowiedzialny za rozwiązanie konfliktu przez obsłużenie wyjątku a następnie wycofanie zmian, przyłączenie ich lub, w innym wypadku, wykonanie odpowiedniej logiki biznesowej mającej na celu zażegnanie konfliktu.
Musisz upewnić się, że domyślna schematu bazy danych kolumny lock_version to 0.
Takie zachowanie może być wyłączone przez ustawienie ActiveRecord::Base.lock_optimistically = false.
Aby zastąpić nazwę kolumy lock_version , ActiveRecord::Base zapewnia metody klasy o nazwie set_locking_column:
class Client < ActiveRecord::Base
set_locking_column :lock_client_column
end
3.7.2 Blokowanie pesymistyczne
Pesymistyczne blokowania wykorzystuje mechanizm blokujący dostarczony przez bazę danych.
Przekazywanie :lock => true do Model.find zapewnia wyłączną blokadę na wybranym wierszu. Model.find używając :lock jest zwykle zawinięty wewnątrz transakcji aby zapobiec sytuacji blokowania systemu.
Na przykład:
Item.transaction do
i = Item.first(:lock => true)
i.name = 'Jones'
i.save
end
Powyższa sesja generuje następujący kod SQL dla bazy MySQL
SQL (0.2ms) BEGIN
Item Load (0.3ms) SELECT * FROM `items` LIMIT 1 FOR UPDATE
Item Update (0.4ms) UPDATE `items` SET `updated_at` = '2009-02-07 18:05:56', `name` = 'Jones' WHERE `id` = 1
SQL (0.8ms) COMMIT
Można również przekazać surowego SQL’a do opcji :lock aby umożliwić różne typy blokad. Na przykład, MySQL posiada wyrażenie o nazwie LOCK IN SHARE MODE, za pomocą której możesz zablokować rekord, ale inne zapytania wciąż mogą go odczytać. Aby określić to wyrażenie po prostu prześlij je z opcją blokady:
Item.transaction do
i = Item.find(1, :lock => "LOCK IN SHARE MODE")
i.increment!(:views)
end
4 Łączenie tabel
Model.find zapewnia opcję :joins do określania klauzuli JOIN na rezultatach SQL’a. Istnieje wiele różnych sposobów określania opcji :joins :
4.1 Używanie surowego SQL-a
Wystarczy, że dostarczysz surowego SQL-a określając klauzulę JOIN w opcji :joins. Na przykład:
Client.all(:joins => 'LEFT OUTER JOIN addresses ON addresses.client_id = clients.id')
Zaskutkuje to następującym kodem SQL:
SELECT clients.* FROM clients LEFT OUTER JOIN addresses ON addresses.client_id = clients.id
4.2 Korzystanie z tablic/tablic asocjacyjnych zwanych asocjacjami.
Ta metoda działa wyłącznie z INNER JOIN,
Moduł Active Record pozwala na korzystanie z nazw associations zdefiniowanych na modelu jako skróty dla określania opcji :joins.
Na przykład, biorąc pod uwagę następujące modele Category, Post, Comments i Guest :
class Category < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
belongs_to :category
has_many :comments
has_many :tags
end
class Comments < ActiveRecord::Base
belongs_to :post
has_one :guest
end
class Guest < ActiveRecord::Base
belongs_to :comment
end
Teraz wszystkie z następujących przyniosą oczekiwane dołączone zapytania używając INNER JOIN:
4.2.1 Łączenie prostej asocjacji
Category.all :joins => :posts
Generuje to:
SELECT categories.* FROM categories
INNER JOIN posts ON posts.category_id = categories.id
4.2.2 Łączenie wielu asocjacji
Post.all :joins => [:category, :comments]
Generuje to:
SELECT posts.* FROM posts
INNER JOIN categories ON posts.category_id = categories.id
INNER JOIN comments ON comments.post_id = posts.id
4.2.3 Łączenie zagnieżdżonych asocjacji (jednopoziomowe)
Post.all :joins => {:comments => :guest}
4.2.4 Łączenie zagnieżdżonych asocjacji (wielopoziomowe)
Category.all :joins => {:posts => [{:comments => :guest}, :tags]}
4.3 Określanie warunków połączonych tabel
Możesz określić warunki połaćzonych tabel korzystając z warunków Array i String. Hash conditions zapewnia specjalną składnie dla określania warunków połączonych tabel:
time_range = (Time.now.midnight - 1.day)..Time.now.midnight
Client.all :joins => :orders, :conditions => {'orders.created_at' => time_range}
Alternatywną i czystszą składnią jest zagnieżdżenie warunków tablicy asocjacyjnej:
time_range = (Time.now.midnight - 1.day)..Time.now.midnight
Client.all :joins => :orders, :conditions => {:orders => {:created_at => time_range}}
Funkcja ta znajdzie wszystkich klientów, którzy złożyli wczoraj zamówienia, znów używając wyrażenia SQL BETWEEN.
5 Zachłanne ładowanie asocjacji
Zachłanne ładowanie to mechanizm ładujący rekordy asocjacyjne obiektów zwróconych przez Model.find używając jak najmniejszej ilości zapytań.
Problem N + 1 zapytań
Rozważmy następujący kod, który wyszukuje 10 klientów i wypisuje ich kody pocztowe:
clients = Client.all(:limit => 10)
clients.each do |client|
puts client.address.postcode
end
Ten kod wygląda dobrze na pierwszy rzut oka, ale problem leży w liczbie realizowanych zapytań. Powyższy kod realizuje 1 (do znalezienia 10 klientów) + 10 ( jedno dla każdego klienta do załadowania adresu) = 11 zapytań.
Rozwiązanie dla problemu N + 1 zapytań
Moduł Active Record umożliwia określenie wszystkich asocjacji, które mają być ładowane. Jest to możliwe dzięki ustaleniu opcji :include wywołania Model.find. Stosując :include, moduł Active Record zapewnia ładowanie wszystkich określonych asocjacji używając najmniejszej możliwej liczby zapytań.
Wracając do powyższego przypadku, moglibyśmy nadpisać Client.all aby wykorzystać zachłannie załadowany adres:
clients = Client.all(:include => :address, :limit => 10)
clients.each do |client|
puts client.address.postcode
end
Powyższy kod wykona tylko 2 zapytania, w przeciwieństwie do 11 zapytań we wcześniejszym przykładzie:
SELECT * FROM clients
SELECT addresses.* FROM addresses
WHERE (addresses.client_id IN (1,2,3,4,5,6,7,8,9,10))
5.1 Zachłanne ładowanie wielu asocjacji
Active Record pozwala załadować zachłannie każdą możliwą liczbę asocjacji z pojedynczym wywołaniem Model.find używając tablicy, tablicy asocjacyjnej lub zagnieżdżonej tablicy asocjacyjnej tablicy/tablicy asocjacyjnej z opcją :include.
5.1.1 Tablica wielu asocjacji
Post.all :include => [:category, :comments]
Ładuje to wszystkie posty, stowarzyszone kategorie i komentarze do każdego posta.
5.1.2 Zagnieżdżone asocjacje hash
Category.find 1, :include => {:posts => [{:comments => :guest}, :tags]}
Powyższy kod wyszukuje kategorie z id równym 1 i zachłannie ładuje wszystkie posty stoważyszone z wyszukaną kategorią. Dodatkowo, wszystkie tagi postów i komentarze również zostaną załadowane zachłannie. Ponadto każdy gość stowarzyszony z komentarzem, również zostanie załadowany w ten sposób.
5.2 Określanie warunków dla asocjacji ładowanej zachłannie
Active Record pozwala również określić warunki dla asocjacji ładowanej zachłannie, tak jak ma to miejsce w przypadku klauzuli :joins. Zalecanym sposobem jest użycie :joins.
6 Dynamiczne wyszukiwanie
Active Recors zapewnia metodę wyszukiwania dla każdego pola (również znanego jako atrybut), które definiujesz w tabeli. Jeśli na przykład posiadasz pole o nazwie name w modelu Client, korzystasz za darmo z find_by_name i find_all_by_name z Active Record. Jeśli masz również pole locked w modelu Client, możesz również użyć find_by_locked oraz find_all_by_locked.
Możesz również wykorzystać metodę find_last_by_*, która wyszuka ostatnie rekordy pasujące do argumentu.
Możesz na końcu dynamicznego wyszukiwania określić wykrzyknik (!) aby zgłosić błąd ActiveRecord::RecordNotFound jeśli żaden rekord nie zostanie zwrócony, jak: Client.find_by_name!("Ryan")
Jeśli chcesz wyszukać zarówno po imieniu i zablokowane możesz połączyć ze sobą te wyszukania używając and pomiędzy polami, na przykład Client.find_by_name_and_locked("Ryan", true).
Istnieje jeszcze jeden zestaw dynamicznego wyszukiwania, który umożliwia stworzenie/zainicjowanie obiektów, jeśli nie zostały znalezione. Działa on w sposób podobny do pozostałych wyszukiwań i może być użyty jak find_or_create_by_name(params[:name]). Używając tej formuły najpierw zostanie wykonane wyszukiwanie, a następnie, jeśli zwróconą wartością będzie nil, stworzony zostanie nowy obiekt. SQL dla Client.find_or_create_by_name("Ryan") wygląda następująco:
SELECT * FROM clients WHERE (clients.first_name = 'Ryan') LIMIT 1
BEGIN
INSERT INTO clients (first_name, updated_at, created_at, orders_count, locked)
VALUES('Ryan', '2008-09-28 15:39:12', '2008-09-28 15:39:12', 0, '0')
COMMIT
find_or_create’s sibling, find_or_initialize, wyszuka obiekt i jeśli taki nie istnieje, to zadziała podobnie do wywołania new z argumentami, które wprowadziłeś. Na przykład:
client = Client.find_or_initialize_by_first_name('Ryan')
albo przypisze istniejącego klienta o imieniu “Ryan” do klienta zmiennej lokalnej lub zainicjuje nowy obiekt podobnie do wywołania Client.new(:name => 'Ryan'). Od teraz możesz modyfikować pozostałe pola klienta wywołując na nim ustawienia atrybutu: client.locked = true i gdy zechcesz zapisać je do bazy danych po prostu wywołaj na nich save.
7 Wyszukiwanie za pomocą SQL
Jeśli chciałbyś skorzystać ze swojego własnego SQL’a w celu wyszukania rekordów w tabeli, możesz użyć find_by_sql. Metoda find_by_sql zwróci tablicę obiektów. Możesz użyć takiego zapytania:
Client.find_by_sql("SELECT * FROM clients
INNER JOIN orders ON clients.id = orders.client_id
ORDER clients.created_at desc")
find_by_sql oferuje prosty sposób nawiązywania połączeń z bazą danych i pobierania instancji obiektów.
8 select_all
find_by_sql ma bliską relację zwaną connection#select_all. select_all odzyska obiekty z bazy danych używając SQL klienta, takiego find_by_sql, ale nie stworzy ich. W zamian otrzymasz tablicę hash’ów, gdzie każdy hash wskazuje na rekord.
Client.connection.select_all("SELECT * FROM clients WHERE id = '1'")
9 Istnienie obiektów
Jeśli po prostu chcesz sprawdzić, czy dany obiekt istnieje, użyj metody zwanej exists?. Wyśle ona zapytanie do bazy danych używając tego samego zapytania, co find, ale zamiast zwrócić obiekt lub zbiór obiektów, zwróci true lub false.
Client.exists?(1)
Metoda exists? również pobiera wiele identyfikatorów, ale wyszukanie powiedzie się, jeśli zwrócona zostanie wartość true, czyli jeśli którykolwiek z wyszukiwanych rekordów istnieje.
Client.exists?(1,2,3)
# or
Client.exists?([1,2,3])
Co więcej, exists pobiera opcje conditions podobnie do wyszukania:
Client.exists?(:conditions => "first_name = 'Ryan'")
exists? można również wykorzystać nie podając żadnych argumentów:
Client.exists?
Powyższe zapytanie zwróci false jeśli tabela clients jest pusta, w przeciwnym razie zwróci true.
10 Kalkulacje
W tej sekcji używana jest medota count jako przkład kalkulacji, ale opisane opcje mogą być stosowane do wszystkich pozostałych przypadków.
count przyjmuje warunki w taki sam sposób jak exists?:
Client.count(:conditions => "first_name = 'Ryan'")
Które są wykonywane:
SELECT count(*) AS count_all FROM clients WHERE (first_name = 'Ryan')
Możesz do tego użyć również :include lub :joins aby zrobić to w bardziej złożony sposób:
Client.count(:conditions => "clients.first_name = 'Ryan' AND orders.status = 'received'", :include => "orders")
Co zostanie wykonane:
SELECT count(DISTINCT clients.id) AS count_all FROM clients
LEFT OUTER JOIN orders ON orders.client_id = client.id WHERE
(clients.first_name = 'Ryan' AND orders.status = 'received')
Kod ten określa clients.first_name tylko w jednym przypadku, w którym jedna z przyłączonych tabeli posiada pole o nazwie first_name i używa orders.status, ponieważ taka jest nazwa naszej dołączanej tabeli.
10.1 Count
Jeśli chcesz zobaczyć, jak wiele rekordów znajduje się w twojej tabeli modelu, możesz wywołać Client.count, co zwróci ich ilość. Jeśli chcesz być bardziej szczegółowy i znaleźć wszystkich klientów z ich obecnym wiekiem, możesz użyć Client.count(:age).
Aby znaleźć opcje, proszę zajrzyj do rozdziału Calculations.
10.2 Average
Jeśli chcesz zobaczyć średnią pewnej liczby w jednej z twoich tabeli, możesz wywołać metodę average na klasie, która odnosi się do tabeli. Wywołanie tej metody będzie wyglądało następując:
Client.average("orders_count")
Zwróci to numer (ewentualnie liczbę zmiennoprzecinkową, jak 3.14159265) reprezentujący średnią wartość w polu.
Opcje znajdziesz w rozdziale Calculations.
10.3 Minimum
Jeśli chcesz znaleźć minimalną wartość pola w tabeli, możesz użyć metody minimum na klasie, która odnosi się do tabeli. To wywołanie będzie wyglądac następująco:
Client.minimum("age")
Opcje znajdziesz w rozdziale Calculations.
10.4 Maximum
Jeśli chcesz znaleźć maksymalna wartość pola w tabeli możesz użyć metody maximum na klasie, która odnosi się do tabeli. To wywołanie metody będzie wyglądało następująco:
Client.maximum("age")
Opcje znajdziesz w rozdziale Calculations.
10.5 Sum
Jeśli chcesz znaleźć sumę pola dla wszystkich rekordów w tabeli użyć metody sum na klasie, która odnosi się do tabeli. To wywołanie metody będzie wyglądało następująco:
Client.sum("orders_count")
Opcje znajdziesz w rozdziale Kalkulacje.