Więcej na rubyonrails.pl: Start | Pobierz | Wdrożenie | Kod (en) | Screencasty | Dokumentacja | Ekosystem | Forum | IRC

Interfejs zapytań modułu Active Record

Ten przewodnik obejmuje różne sposoby pobierania danych z bazy używając modułu Active Record. Kierując się tym przewodnikiem będziesz potrafił:

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.

11 Changelog

Lighthouse ticket

  • February 7, 2009: Second version by Pratik
  • December 29 2008: Initial version by Ryan Bigg