Szukasz najlepszych specjalistów od oprogramowania? Znajdziesz ich w kilka kliknięć.

Widoki zmaterializowane w Ruby on Rails ze Scenic

5 sty 20184 min czytania

Dawid Karczewski

Senior full stack developer i CTO w Ideamotive.

scenic

Widoki zmaterializowane nie są czymś powszechnie używanym w aplikacjach Ruby on Rails. Ostatnio jednak spróbowałem ich użyć i wyniki okazały się bardzo satysfakcjonujące.

W tym studium przypadku chciałbym przedstawić prostą aplikację wykorzystującą Ruby 2.4.1, Rails 5.1.4, PostgreSQL 10 oraz gem scenic.

 

Czym są widoki bazodanowe?

Z dokumentacji PostgreSQL:

Załóżmy, że Twoją aplikację szczególnie interesuje połączone zestawienie zapisów pogodowych i lokalizacji miast, ale nie chcesz wpisywać tego zapytania za każdym razem, gdy go potrzebujesz. Możesz utworzyć nad zapytaniem widok, który nadaje mu nazwę, dzięki czemu odwołasz się do niego jak do zwykłej tabeli:



Szerokie korzystanie z widoków to kluczowy element dobrego projektowania baz danych SQL. Widoki pozwalają ukryć za spójnymi interfejsami szczegóły struktury tabel, która może się zmieniać wraz z rozwojem aplikacji.
Widoków można używać niemal wszędzie tam, gdzie da się użyć prawdziwej tabeli. Budowanie widoków na innych widokach nie jest niczym niezwykłym.

To po prostu bardzo wygodny sposób na to, by nie pisać skomplikowanych zapytań. Nie pomaga jednak w wydajności – skomplikowane zapytanie i tak wykonuje się za każdym razem.

Widoki zmaterializowane

Na szczęście widoki można zmaterializować. Zajrzyjmy ponownie do dokumentacji PostgreSQL:

Widoki zmaterializowane w PostgreSQL korzystają z systemu reguł tak samo jak zwykłe widoki, ale zapisują wyniki w formie zbliżonej do tabeli. Główne różnice między:
CREATE MATERIALIZED VIEW mymatview AS SELECT * FROM mytab;
oraz:
CREATE TABLE mymatview AS SELECT * FROM mytab;
polegają na tym, że widoku zmaterializowanego nie da się później bezpośrednio aktualizować, a zapytanie użyte do jego utworzenia jest przechowywane dokładnie tak samo jak zapytanie zwykłego widoku, dzięki czemu świeże dane można wygenerować dla widoku zmaterializowanego poleceniem:
REFRESH MATERIALIZED VIEW mymatview;

Mówiąc prosto, wyniki zapytania widoku są przechowywane w bazie danych – tak jak w każdej innej tabeli. Jedyna różnica polega na tym, że nie możemy aktualizować widoku bezpośrednio, ale można go odświeżyć rekordami z tabel źródłowych.

Dzięki temu zamiast wykonywać kosztowne zapytania, możemy korzystać z danych ułożonych w prostszy sposób w widoku zmaterializowanym.

Studium przypadku

Tutaj znajdziesz bardzo prostą aplikację, którą stworzyłem na potrzeby tego wpisu. To przykład rzeczywistego problemu, na jaki się natknąłem, sprowadzonego w repozytorium do samej istoty.

Pomysł jest prosty. Użytkownicy mają uprawnienia. Uprawnienia nie są jednak przypisywane bezpośrednio do użytkowników. Użytkownicy należą do grup, a grupy mają wiele uprawnień.

Jest 5 modeli:

 

Bardzo prawdopodobne, że widziałeś już podobny wzorzec – grupowanie rekordów o podobnym przeznaczeniu. Nie ma w tym projekcie nic skomplikowanego. Pobranie informacji może jednak wymagać kilku joinów SQL.

Dane testowe

Żeby mieć jakieś dane do pracy i sprawdzania wyników, przygotowałem plik seeds.rb. Tworzy on wszystkie uprawnienia oraz 100 użytkowników, z których każdy należy do 3 grup uprawnień. Grup jest 10, a każda z nich ma 5 uprawnień.

 

Wystarczy uruchomić rails db:seed i baza wypełni się danymi.

Rozwiązanie nr 1 – naiwne podejście

Załóżmy, że chcemy sprawdzić, czy dany użytkownik ma uprawnienia :a i :b jednocześnie:

 

Przekłada się to na następujące zapytanie SQL:

 

Co dostaniemy z EXPLAIN?

 

Jak widać, większość skanów korzysta z indeksów, więc niewiele możemy zrobić, żeby przyspieszyć.

 

Napisałem prosty benchmark jako zadanie rake. Na zaseedowanych danych sprawdzam 1000 razy, czy użytkownik ma 1 uprawnienie, potem 2 losowe uprawnienia, 3, 4 i w końcu 5.

 

rake benchmark_permissions

user system total real
joins - 1 permission 1.750000 0.050000 1.800000 ( 2.266476)
joins - 2 permissions 1.840000 0.060000 1.900000 ( 3.427442)
joins - 3 permissions 1.860000 0.070000 1.930000 ( 3.497956)
joins - 4 permissions 1.880000 0.060000 1.940000 ( 3.568906)

 

No dobrze, niewiele nam to mówi. Czy da się to przyspieszyć?

Rozwiązanie nr 2 – podejście z widokami zmaterializowanymi

Tu robi się ciekawie. Ludzie z thoughtbot stworzyli bardzo fajny gem o nazwie scenic. Ułatwia zarządzanie widokami bazodanowymi w Rails. Polecam Ci go wypróbować. 

 

Dla naszego problemu moglibyśmy stworzyć takie rozwiązanie:

 

rails generate scenic:view permissions_check_results

 

Generuje to dwa pliki. Pierwszy z nich to db/views/permissions_check_results_v01.sql, który wypełniłem poniższym kodem:

 

Co się tu dzieje? Tworzymy prosty widok z dwiema kolumnami: user_idi permission_name (zwróć uwagę, że używamy enuma – permission_name jest przechowywane jako liczba całkowita).

 

uder_id permission_name
1 1
1 2
2 3

 

W powyższym przykładzie użytkownik o id 1 ma dwa uprawnienia: 1 i 2, a użytkownik o id 2 ma tylko jedno uprawnienie: 3. Zamiast łączyć wiele tabel i sprawdzać uprawnienia przez grupy, mamy teraz bardzo prostą, dwukolumnową tabelę (widok).

Drugi plik wygenerowany przez zadanie scenic to migracja bazy danych: db/migrate/[TIMESTAMP]_create_permissions_check_results.rb

 

Ważne jest dodanie opcji materialized: true do migracji. Musimy też stworzyć model.

Czas porównać nasze wcześniejsze wyniki benchmarku z nowymi. Korzystając z kodu z początku artykułu i dodając kilka nowych linii:

 

rake benchmark_permissions

user system total real
joins - 1 permission 1.840000 0.050000 1.890000 ( 2.328792)
joins - 2 permissions 1.860000 0.070000 1.930000 ( 3.452119)
joins - 3 permissions 1.880000 0.060000 1.940000 ( 3.519821)
joins - 4 permissions 1.950000 0.070000 2.020000 ( 3.664984)

view - 1 permission 0.780000 0.060000 0.840000 ( 1.272834)
view - 2 permissions 0.770000 0.050000 0.820000 ( 1.310658)
view - 3 permissions 0.780000 0.060000 0.840000 ( 1.291651)
view - 4 permissions 0.800000 0.050000 0.850000 ( 1.329006)

Nowe wyniki są 3 razy szybsze. To całkiem niezły rezultat. Część z Was może mieć wątpliwości:

Hej, przecież to tylko odczyt. A co z aktualizowaniem rekordów w bazie?

Masz rację – nie wspomniałem o tym ani tego nie testowałem. Powód jest prosty – nie przejmuję się tym. Uprawnienia w moim przypadku aktualizują się bardzo rzadko. Z drugiej strony muszę je pobierać bardzo często. Znacznie bardziej zależy mi na czasie odczytu niż zapisu.

Podsumowanie

Jeśli musisz pisać dużo joinów, żeby wyciągnąć informacje z bazy danych, rozważ stworzenie widoku zmaterializowanego.

  1. Widoki bazodanowe możesz traktować jak swego rodzaju API albo metody publiczne. Zyskujesz spójny sposób dostępu do rekordów. Widok pozostaje taki sam, podczas gdy tabele źródłowe mogą się zmieniać.
  2. Jest szybszy (jeśli czytasz częściej, niż zapisujesz).

Dawid Karczewski

Dawid jest full stack developerem. Tworzy aplikacje w Ruby on Rails i React Native od pierwszego pomysłu aż po wdrożenie. Buduje dla naszych klientów rozwiązania, które pomagają im rosnąć.

Zobacz wszystkie wpisy autora
im_ebook_cover_template 4 (2)

Ruby on Rails w Twoim kolejnym projekcie webowym

Kompletny przewodnik biznesowy

Czytaj teraz
Newsletter 9-1
Newsletter Ideamotive
Twój dwutygodniowy przegląd najgorętszych newsów technologicznych

Szukasz świetnych projektów jako programista Ruby on Rails?

Dołącz do Ideamotive Talent. Pracuj przy międzynarodowych projektach, zarabiaj i rozwijaj karierę na własnych zasadach.