Spis treści Wstęp ........................................................................................................................................5 1. Techniki podstawowe ...................................................................................................9 Czym jest metaprogramowanie? Podstawy Ruby Techniki metaprogramowania Programowanie funkcyjne Przykłady Propozycje dalszych lektur
9 12 30 41 46 49
2. ActiveSupport oraz RailTies ........................................................................................ 51 Ruby, jakiego nie znamy Jak czytać kod? ActiveSupport Core Extensions RailTies Propozycje dalszych lektur
51 54 61 65 79 81
3. Wtyczki Rails ................................................................................................................83 Wtyczki Tworzenie wtyczek Przykład wtyczki Testowanie wtyczek Propozycje dalszych lektur
83 87 89 94 97
4. Bazy danych .................................................................................................................99 Systemy zarządzania bazą danych Duże obiekty (binarne) Zaawansowane funkcje baz danych Podłączanie do wielu baz danych Buforowanie Wyrównywanie obciążenia i wysoka dostępność LDAP Propozycje dalszych lektur
99 104 112 118 120 121 126 127
3
5. Bezpieczeństwo ......................................................................................................... 129 Problemy w aplikacji Problemy w sieci WWW Wstrzykiwanie SQL Środowisko Ruby Propozycje dalszych lektur
129 138 145 146 147
6. Wydajność .................................................................................................................. 149 Narzędzia pomiarowe Przykład optymalizacji Rails Wydajność ActiveRecord Skalowanie architektury Inne systemy Propozycje dalszych lektur
150 156 165 174 181 183
7. REST, zasoby oraz usługi Web .................................................................................. 185 Czym jest REST? Zalety architektury REST REST w Rails Analiza przypadku — Amazon S3 Propozycje dalszych lektur
185 203 207 226 230
8. i18n oraz L10n ............................................................................................................ 231 Lokalizacje Kodowanie znaków Unicode Rails i Unicode Rails L10n Propozycje dalszych lektur
231 232 233 235 243 262
9. Wykorzystanie i rozszerzanie Rails ..........................................................................263 Wymiana komponentów Rails Wykorzystanie komponentów Rails Udział w tworzeniu Rails Propozycje dalszych lektur
263 274 279 285
10. Duże projekty .............................................................................................................287 Kontrola wersji Śledzenie błędów Struktura projektu Instalacja Rails Propozycje dalszych lektur
Gdy w roku 2004 zaczynałem korzystać z Ruby on Rails, framework Rails nie miał prawie żadnej dokumentacji. Od tego czasu powstała imponująca liczba książek, blogów i artykułów zawierających informacje o tworzeniu aplikacji WWW za pomocą Rails. Jednak wiele z nich wydaje się powielać wspólny wzór — można utworzyć blog w 15 minut; aplikacja z listą zadań do wykonania była bardzo prosta. Wiele z książek, do których zajrzałem, zawierało cały rozdział na temat instalowania Ruby i Rails. Obecnie nie brakuje zasobów dla początkujących i średnio zaawansowanych programistów Rails. Jednak Rails jest znacznie bardziej użyteczny, nie jest tylko zabawką do blogów i list zadań. Aplikacje firmy 37signals (Basecamp, Highrise, Backpack oraz Campfire) zostały zbudowane z użyciem Rails; wiele popularnych witryn internetowych, takich jak Twitter, Penny Arcade czy Yellowpages.com, korzysta z Rails. Rails jest obecnie wykorzystywany w wielu miejscach, ale przy budowaniu takich dużych aplikacji programiści często muszą polegać wyłącznie na sobie, ponieważ najbardziej aktualne i odpowiednie informacje są rozsiane po różnych blogach programistów. Programowanie i wdrażanie skomplikowanych projektów WWW jest zadaniem wielodyscyplinarnym i zawsze takim pozostanie. W książce tej chciałem połączyć wiele różnych tematów związanych z programowaniem w Rails, od zupełnych podstaw języka Ruby do tworzenia dużych aplikacji Rails.
Wymagania wstępne Jak sugeruje tytuł, Rails. Zaawansowane programowanie nie jest dla początkujących. Czytelnicy powinni dobrze rozumieć architekturę WWW, dobrze znać Ruby 1.8 i mieć doświadczenie w tworzeniu aplikacji za pomocą Ruby on Rails. Nie przedstawiamy instalacji Rails, Rails API ani języka Ruby; zakładam, że Czytelnik ma doświadczenie w każdym z tych zagadnień. Chciałbym zarekomendować poniższe książki do przeczytania przed tą: • Programming Ruby (wydanie drugie, Pragmatic Bookshelf), której autorem jest Dave
Thomas, znana jako „Kilof”; jest to doskonałe wprowadzenie do Ruby dla programistów i spójny przewodnik, który będzie służył przez lata. Bez dwóch zdań jest to najważniejsza książka dla programistów Ruby, niezależnie od poziomu ich doświadczenia.
5
• The Ruby Programming Language (O’Reilly), której autorami są David Flanagan i Yukihiro
Matsumoto. Książa ta, wydana w styczniu 2008 roku, jest wyczerpującym wprowadzeniem i przewodnikiem po Ruby 1.8 oraz 1.9. W doskonały sposób przedstawia najtrudniejsze aspekty Ruby, jednocześnie pozostając zrozumiałą dla programistów uczących się tego języka. • Best of Ruby Quiz (Pragmatic Bookshelf) autorstwa Jamesa Edward Graya II — 25 wybranych
quizów z Ruby Quiz (http://www.rubyquiz.com/); zawiera zarówno same quizy, jak i omówienie ich rozwiązań. Rozwiązywanie zagadek programistycznych i dzielenie się rozwiązaniami z innymi jest świetną metodą doskonalenia umiejętności. • Agile Web Development with Rails (wydanie drugie, Pragmatic Bookshelf), której autorami są
Dave Thomas oraz David Heinemeier Hansson — najlepsza i najbardziej kompletna książka do nauki Ruby on Rails. Drugie wydanie przedstawia Rails 1.2, ale większość koncepcji można stosować w Rails 2.0. • Rails Cookbook (O’Reilly) autorstwa Roba Orsini — zawiera rozwiązania, w stylu książki
kucharskiej, wielu powszechnych problemów w Rails, z których każde jest warte ceny całej książki. Warto również przeczytać podobne książki: Rails Recipes autorstwa Chada Fowlera i Advanced Rails Recipes autorstwa Mike’a Clarka i Chada Fowlera (Pragmatic Bookshelf). W książce tej jest przedstawionych wiele różnych zagadnień; zadałem sobie trud wprowadzenia tematów, które mogą być mało znane (jak na przykład zdecentralizowana kontrola wersji), i przedstawiłem odwołania do zewnętrznych przydatnych zasobów. Każdy rozdział kończy się punktem „Propozycje dalszych lektur” z odwołaniami mającymi za zadanie rozszerzyć lub wyjaśnić tekst. W książce tej przyjąłem podejście wstępujące. Pierwszych kilka rozdziałów przedstawia mechanikę metaprogramowania w Ruby oraz wewnętrznych mechanizmów Rails. W dalszej części książki koncepcje te łączą się w większe zagadnienia, a ostatnich kilka rozdziałów przedstawia najbardziej ogólne tematy dotyczące zarządzania dużymi projektami Rails oraz integracji Rails z innymi systemami. Książka ta jest napisana dla Rails 2.0. W czasie, gdy powstawała, Rails 2.0 był udostępniony w wersji release candidate i nie była to postać finalna. Zmianom ulegały jeszcze szczegóły, ale koncepcje i techniki przedstawione w tej książce powinny obowiązywać w Rails 2.0.
Konwencje stosowane w tej książce W naszej książce korzystamy z następujących konwencji typograficznych: Czcionka pochylona Wskazuje nowe terminy, adresy URL, adresy poczty elektronicznej, nazwy plików, rozszerzenia plików, ścieżki oraz katalogi. Wskazuje również nazwy menu, opcje menu, przyciski menu oraz klawisze skrótów (na przykład Alt lub Ctrl). Czcionka o stałej szerokości znaków
Przedstawia tekst, który powinien zostać zastąpiony wartościami podanymi przez użytkownika.
Pogrubiona czcionka o stałej szerokości znaków
Używana do wyróżniania fragmentów kodu. Ikona oznaczająca poradę, sugestię lub uwagę o charakterze ogólnym.
Ikona oznaczająca ostrzeżenie lub przestrogę.
Korzystanie z przykładów kodu Podręcznik ten został napisany po to, aby ułatwić życie programistom. Dlatego też Czytelnicy mogą swobodnie korzystać z kodu prezentowanego w tej książce w swoich programach i dokumentacji. Nie ma potrzeby kontaktowania się z nami, by uzyskać zgodę na wykorzystywanie fragmentów kodu, o ile nie zamierza się użyć znaczącej porcji kodu. Gdy na przykład ktoś pisze program, który będzie wykorzystywał kilka fragmentów kodu z tej książki, nie ma potrzeby uzyskiwania naszej zgody. Natomiast odsprzedawanie lub rozpowszechnianie płyt CD-ROM z przykładami kodu, dołączanych do niektórych książek wydawanych przez wydawnictwo, już takiej zgody wymaga. Odpowiadanie na pytania, podpierając się cytatami z tej książki nie wymaga naszej zgody. Natomiast dołączanie do dokumentacji własnego komercyjnego produktu znaczącej porcji przykładów z tej książki wymaga naszej zgody. Będziemy wdzięczni za podanie źródła wykorzystywanego kodu, aczkolwiek nie wymagamy tego. Informacja taka powinna zawierać następujące dane: tytuł książki, autor, wydawca i ISBN. Na przykład: Rails. Zaawansowane programowanie, Brad Ediger, Helion 2009, ISBN: 978-83-246-1724-1.
Podziękowania Żadna książka nie powstanie bez pomocy wielu osób. Chciałbym szczególnie podziękować osobom, które pomogły mi zakończyć tę pracę. Bez ich pomocy i wsparcia pomysły nadal tłukłyby się w mojej głowie. Mike Loukides, mój redaktor w O’Reilly, wsparł mnie przy tworzeniu zarysu tej książki. Pomógł mi zrozumieć, jakiego typu książkę chciałem naprawdę napisać, i wsparł mnie przy przekuwaniu idei w słowa. Rozległa wiedza Mike’a na temat przemysłu, informatyki jako takiej i procesu produkcji książek była niezwykle pomocna.
Podz ękowan a
|
7
Miałem niezwykły zespół recenzentów technicznych, którzy wyłapali wiele błędów w moich tekstach. James Edward Gray II, Michael Koziarski, Leonard Richardson i Zed Shaw — dziękuję Wam za te korekty. Wszystkie pozostałe błędy są popełnione przeze mnie i obciążają mnie (jeżeli Czytelnik znajdzie jeden z tych błędów, byłbym wdzięczny za zarejestrowanie go pod adresem http://www.oreilly.com/catalog/9780596510329/errata/). Dział produkcji w wydawnictwie O’Reilly był bardzo profesjonalny i przystosował się do moich dziwnych harmonogramów; to Keith Fahlgren, Rachel Monaghan, Rob Romano, Andrew Savikas, Marlowe Shaeffer i Adam Witwer pomogli uczynić tę książkę tak użyteczną i atrakcyjną. Mam wielu przyjaciół i kolegów, którzy oferowali porady, wsparcie, krytykę i przeglądy. Pragnę wymienić następujące osoby, którym serdecznie dziękuję za wkład w powstanie tej książki: Erik Berry, Gregory Brown, Pat Eyler, James Edward Gray II, Damon Hill, Jim Kane, John Lein, Tim Morgan, Keith Nazworth, Rob Norwood, Brian Sage, Jeremy Weathers oraz Craig Wilson. Dziękuję również Gary’emu i Jean Atkinsonom, którzy choć nie wiedzą nic na temat Rails i tworzenia oprogramowania, zawsze interesowali się postępami pracy i oferowali wsparcie. Inni dostarczali inspiracji poprzez swoje książki i teksty w sieci, jak również przez dyskusję na listach dyskusyjnych. Byli to: François Beausoleil, David Black, Avi Bryant, Jamis Buck, Ryan Davis, Mauricio Fernández, Eric Hodel, S. Robert James, Jeremy Kemper, Rick Olson, Dave Thomas oraz zespół why the lucky stiff. Nic nie byłoby możliwe bez Ruby ani Rails. Dziękuję Yukihiro Matsumoto (Matz) za stworzenie tak pięknego języka, Davidowi Heinemeierowi Hanssonowi za stworzenie tak wspaniałego frameworka oraz osobom wspierającym Ruby oraz Rails za ich pielęgnację. Dziękuję swoim rodzicom za niekończące się wsparcie. Na koniec chciałbym podziękować mojej wspaniałej żonie, Kristen, za całoroczną pomoc w procesie pisania. Zachęcała mnie ona do pisania, gdy myślałem, że jest to niemożliwe, i pomagała mi w każdym kolejnym kroku.
8
|
Wstęp
ROZDZIAŁ 1.
Techniki podstawowe
Do osiągnięcia niezawodności jest wymagana prostota. Edsger W. Dijkstra
Od pierwszego wydania w lipcu 2004 roku środowisko Ruby on Rails stale zdobywa popularność. Rails przyciąga programistów PHP, Java i .NET swoją prostotą — architekturą model-widok-kontroler (MVC), rozsądnymi wartościami domyślnymi („konwencja nad konfiguracją”) oraz zaawansowanym językiem programowania Ruby. Środowisko Rails miało przez pierwszy rok lub dwa słabą reputację z uwagi na braki w dokumentacji. Luka ta została wypełniona przez tysiące programistów, którzy korzystali ze środowiska Ruby on Rails, współtworzyli je i pisali na jego temat, jak również dzięki projektowi Rails Documentation (http://railsdocumentation.org). Dostępne są tysiące blogów, które zawierają samouczki oraz porady na temat programowania w Rails. Celem tej książki jest zebranie najlepszych praktyk oraz wiedzy zgromadzonej przez środowisko programistów Rails i zaprezentowanie ich w łatwej do przyswojenia, zwartej formie. Poszukiwałem ponadto tych aspektów programowania dla WWW, które są często niedoceniane lub pomijane przez środowisko Rails.
Czym jest metaprogramowanie? Rails udostępnia metaprogramowanie dla mas. Choć nie było to pierwsze zastosowanie zaawansowanych funkcji Ruby, to jednak jest ono chyba najbardziej popularne. Aby zrozumieć działanie Rails, konieczne jest wcześniejsze zapoznanie się z tymi mechanizmami Ruby, które zostały wykorzystane w tym środowisku. W tym rozdziale przedstawione zostaną podstawowe mechanizmy zapewniające działanie technik przedstawianych w pozostałych rozdziałach książki. Metaprogramowanie to technika programowania, w której kod jest wykorzystywany do tworzenia innego kodu, bądź dokonania introspekcji samego siebie. Przedrostek meta (z greki) wskazuje na abstrakcję; kod wykorzystujący techniki metaprogramowania działa jednocześnie na dwóch poziomach abstrakcji. Metaprogramowanie jest wykorzystywane w wielu językach, ale jest najbardziej popularne w językach dynamicznych, ponieważ mają one zwykle więcej funkcji pozwalających na manipulowanie kodem jako danymi. Pomimo tego, że w językach statycznych, takich jak C# lub
9
Java, dostępny jest mechanizm refleksji, to nie jest on nawet w części tak przezroczysty, jak w językach dynamicznych, takich jak Ruby, ponieważ kod i dane znajdują się w czasie działania aplikacji na dwóch osobnych warstwach. Introspekcja jest zwykle wykonywana na jednym z tych poziomów. Introspekcja syntaktyczna jest najniższym poziomem introspekcji — pozwala na bezpośrednią analizę tekstu programu lub strumienia tokenów. Metaprogramowanie bazujące na szablonach lub makrach zwykle działa na poziomie syntaktycznym. Ten typ metaprogramowania jest wykorzystany w języku Lisp poprzez stosowanie S-wyrażeń (bezpośredniego tłumaczenia drzewa abstrakcji składni programu) zarówno w przypadku kodu, jak i danych. Metaprogramowanie w języku Lisp wymaga intensywnego korzystania z makr, które są tak naprawdę szablonami kodu. Daje to możliwość pracy na jednym poziomie; kod i dane są reprezentowane w ten sam sposób, a jedynym, co odróżnia kod od danych, jest to, że jest on wartościowany. Jednak metaprogramowanie na poziomie syntaktycznym ma swoje wady. Przechwytywanie zmiennych oraz przypadkowe wielokrotne wartościowanie jest bezpośrednią konsekwencją umieszczenia kodu na dwóch poziomach abstrakcji dla tej samej przestrzeni nazw. Choć dostępne są standardowe idiomy języka Lisp pozwalające na uporanie się z tymi problemami, to jednak są one kolejnymi elementami, których programista Lisp musi się nauczyć i pamiętać o nich. Introspekcja syntaktyczna w Ruby jest dostępna za pośrednictwem biblioteki ParseTree, która pozwala na tłumaczenie kodu źródłowego Ruby na S-wyrażenia1. Interesującym zastosowaniem tej biblioteki jest Heckle2, biblioteka ułatwiająca testowanie, która analizuje kod źródłowy Ruby i zmienia go poprzez modyfikowanie ciągów oraz zmianę wartości true na false i odwrotnie. W założeniach, jeżeli nasz kod jest odpowiednio pokryty testami, każda modyfikacja kodu powinna zostać wykryta przez testy jednostkowe. Alternatywą dla introspekcji syntaktycznej jest działająca na wyższym poziomie introspekcja semantyczna, czyli analiza programu z wykorzystaniem struktur danych wyższego poziomu. Sposób realizacji tej techniki różni się w rożnych językach programowania, ale w Ruby zwykle oznacza to operowanie na poziomie klas i metod — tworzenie, modyfikowanie i aliasowanie metod; przechwytywanie wywołań metod; manipulowanie łańcuchem dziedziczenia. Techniki te są zwykle bardziej związane z istniejącym kodem niż metody syntaktyczne, ponieważ najczęściej istniejące metody są traktowane jako czarne skrzynki i ich implementacja nie jest swobodnie zmieniana.
Nie powtarzaj się Na wysokim poziomie metaprogramowanie jest przydatne do wprowadzania zasady DRY (ang. Don’t Repeat Yourself — „nie powtarzaj się”). Zgodnie z tą techniką, nazywaną również „Raz i tylko raz”, każdy element informacji musi być zdefiniowany w systemie tylko raz. Powielanie jest zwykle niepotrzebne, szczególnie w językach dynamicznych, takich jak Ruby. Podobnie jak abstrakcja funkcjonalna pozwala nam na uniknięcie powielania kodu, który jest taki sam lub niemal taki sam, metaprogramowanie pozwala nam uniknąć podobnych koncepcji, wykorzystywanych w aplikacji. 1
http://www.zenspider.com/ZSS/Products/ParseTree.
2
http://rubyforge.org/projects/seattlerb.
10
|
Rozdz ał 1. Techn k podstawowe
Metaprogramowanie ma na celu zachowanie prostoty. Jednym z najłatwiejszych sposobów na zapoznanie się z metaprogramowaniem jest analizowanie kodu i jego refaktoryzacja. Nadmiarowy kod może być wydzielany do funkcji; nadmiarowe funkcje lub wzorce mogą być często wydzielone z użyciem metaprogramowania. Wzorc e projektowe definiują nakładające się obszary; wzorce zostały zaprojektowane w celu zminimalizowania liczby sytuacji, w których musimy rozwiązywać ten sam problem. W społeczności Ruby wzorce projektowe mają dosyć złą reputację. Dla części programistów wzorce są wspólnym słownikiem do opisu rozwiązań powtarzających się problemów. Dla innych są one „przeprojektowane”. Aby być pewnym tego, że zastosowane zostaną wszystkie dostępne wzorce, muszą być one nadużywane. Jeżeli jednak będą używane rozsądnie, nie musi tak być. Wzorce projektowe są użyteczne jedynie w przypadku, gdy pozwalają zmniejszać złożoność kognitywną. W Ruby część najbardziej szczegółowych wzorców jest tak przezroczysta, że nazywanie ich „wzorcami” może być nieintuicyjne; są one w rzeczywistości idiomami i większość programistów, którzy „myślą w Ruby”, korzysta z nich bezwiednie. Wzorce powinny być uważane za słownik wykorzystywany przy opisie architektury, a nie za bibliotekę wstępnie przygotowanych rozwiązań implementacji. Dobre wzorce projektowe dla Ruby znacznie różnią się w tym względzie od dobrych wzorców projektowych dla C++.
Uogólniając, metaprogramowanie nie powinno być wykorzystywane tylko do powtarzania kodu. Zawsze powinno się przeanalizować wszystkie opcje, aby sprawdzić, czy inna technika, na przykład abstrakcja funkcjonalna, nie nadaje się lepiej do rozwiązania problemu. Jednak w kilku przypadkach powtarzanie kodu poprzez metaprogramowanie jest najlepszym sposobem na rozwiązanie problemu. Jeżeli na przykład w obiekcie musi być zdefiniowanych kilka podobnych metod, tak jak w metodach pomocniczych ActiveRecord, można w takim przypadku skorzystać z metaprogramowania.
Pułapki Kod, który się sam modyfikuje, może być bardzo trudny do tworzenia i utrzymania. Wybrane przez nas konstrukcje programowe powinny zawsze spełniać nasze potrzeby — powinny one upraszczać życie, a nie komplikować je. Przedstawione poniżej techniki powinny uzupełniać zestaw narzędzi w naszej skrzynce, a nie być jedynymi narzędziami.
Programowanie wstępujące Programowanie wstępujące jest koncepcją zapożyczoną z świata Lisp. Podstawową koncepcją w tym sposobie programowania jest tworzenie abstrakcji od najniższego poziomu. Przez utworzenie na początku konstrukcji najniższego poziomu budujemy w rzeczywistości program na bazie tych abstrakcji. W pewnym sensie piszemy język specyficzny dla domeny, za pomocą którego tworzymy programy. Koncepcja ta jest niezmiernie użyteczna w przypadku ActiveRecord. Po utworzeniu podstawowych schematów i modelu obiektowego można rozpocząć budowanie abstrakcji przy wykorzystaniu tych obiektów. Wiele projektów Rails zaczyna się od tworzenia podobnych do zamieszczonej poniżej abstrakcji modelu, zanim powstanie pierwszy wiersz kodu kontrolera lub nawet projekt interfejsu WWW: Czym jest metaprogramowan e?
|
11
class Order < ActiveRecord::Base has_many :line_items def total subtotal + shipping + tax end def subtotal line_items.sum(:price) end def shipping shipping_base_price + line_items.sum(:shipping) end def tax subtotal * TAX_RATE end end
Podstawy Ruby Zakładamy, że Czytelnik dobrze zna Ruby. W podrozdziale tym przedstawimy niektóre z aspektów języka, które są często mylące lub źle rozumiane. Niektóre z nich mogą być Czytelnikowi znane, ale są to najważniejsze koncepcje tworzące podstawy technik metaprogramowania przedstawianych w dalszej części rozdziału.
Klasy i moduły Klasy i moduły są podstawą programowania obiektowego w Ruby. Klasy zapewniają mechanizmy hermetyzacji i separacji. Moduły mogą być wykorzystywane jako tzw. mixin — zbiór funkcji umieszczonych w klasie, stanowiących namiastkę mechanizmu dziedziczenia wielobazowego. Moduły są również wykorzystywane do podziału klas na przestrzenie nazw. W Ruby każda nazwa klasy jest stałą. Dlatego właśnie Ruby wymaga, aby nazwy klas rozpoczynały się od wielkiej litery. Stała ta jest wartościowana na obiekt klasowy, który jest obiektem klasy Class. Różni się od obiektu Class, który reprezentuje faktyczną klasę Class3. Gdy mówimy o „obiekcie klasowym”, mamy na myśli obiekt reprezentujący klasę (wraz z samą klasą Class). Gdy mówimy o „obiekcie Class”, mamy na myśli klasę o nazwie Class, będącą klasą bazową dla wszystkich obiektów klasowych. Klasa Class dziedziczy po Module; każda klasa jest również modułem. Istnieje tu jednak niezwykle ważna różnica. Klasy nie mogą być mieszane z innymi klasami, a klasy nie mogą dziedziczyć po obiektach; jest to możliwe tylko w przypadku modułów.
Wyszukiwanie metod Wyszukiwanie metod w Ruby może być dosyć mylące, a w rzeczywistości jest dosyć regularne. Najprostszym sposobem na zrozumienie skomplikowanych przypadków jest przedstawienie struktur danych, jakie Ruby wewnętrznie tworzy. 3
Jeżeli nie jest to wystarczająco skomplikowane, trzeba pamiętać, że obiekt Class posiada również klasę Class.
12
|
Rozdz ał 1. Techn k podstawowe
Każdy obiekt Ruby4 posiada zbiór pól w pamięci: klass
Wskaźnik do obiektu klasy danego obiektu (została użyta nazwa klass zamiast class, ponieważ ta druga jest słowem kluczowym w C++ i Ruby; jeżeli nazwalibyśmy ją class, Ruby kompilowałby się za pomocą kompilatora C, ale nie można byłoby użyć kompilatora C++. Ta wprowadzona umyślnie literówka jest używana wszędzie w Ruby). iv_tbl
„Tablica zmiennych instancyjnych” to tablica mieszająca zawierająca zmienne instancyjne należące do tego obiektu. flags
Pole bitowe znaczników Boolean zawierające informacje statusu, takie jak stan śladu obiektu, znacznik zbierania nieużytków oraz to, czy obiekt jest zamrożony. Każda klasa Ruby posiada te same pola, jak również dwa dodatkowe: m_tbl
„Tablica metod” — tabela mieszająca metod instancyjnych danej klasy lub modułu. super
Wskaźnik klasy lub modułu bazowego. Pola te pełnią ważną rolę w wyszukiwaniu metod i są ważne w zrozumieniu tego mechanizmu. W szczególności można zwrócić uwagę na różnicę pomiędzy wskaźnikami obiektu klasy: klass i super.
Zasady Zasady wyszukiwania metod są bardzo proste, ale zależą od zrozumienia sposobu działania struktur danych Ruby. Gdy do obiektu jest wysyłany komunikat5, wykonywane są następujące operacje:
1. Ruby korzysta z wskaźnika klass i przeszukuje m_tbl z obiektu danej klasy, szukając odpowiedniej metody (wskaźnik klass zawsze wskazuje na obiekt klasowy).
2. Jeżeli nie zostanie znaleziona metoda, Ruby korzysta z wskaźnika super obiektu klasowego i kontynuuje wyszukiwanie w m_tbl klasy bazowej.
3. Ruby wykonuje wyszukiwanie w ten sposób aż do momentu znalezienia metody bądź też do osiągnięcia końca łańcucha wskaźników super.
4. Jeżeli w żadnym obiekcie łańcucha nie zostanie znaleziona metoda, Ruby wywołuje metodę method_missing z obiektu odbiorcy metody. Powoduje to ponowne rozpoczęcie tego procesu, ale tym razem wyszukiwana jest metoda method_missing zamiast początkowej metody.
4
Poza obiektami natychmiastowymi (Fixnums, symbols, true, false oraz nil), które przedstawimy później.
5
W Ruby często stosowana jest terminologia przekazywania komunikatów pochodząca z języka Smalltalk — gdy jest wywoływana metoda, mówi się, że jest przesyłany komunikat. Obiekt, do którego jest wysyłany komunikat, jest nazywany odbiorcą.
Podstawy Ruby
|
13
Zasady te są stosowane w sposób uniwersalny. Wszystkie interesujące mechanizmy wykorzystujące wyszukiwanie metod (mixin, metody klasowe i klasy singleton) wykorzystują strukturę wskaźników klass oraz super. Przedstawimy teraz ten proces nieco bardziej szczegółowo.
Dziedziczenie klas Proces wyszukiwania metod może być mylący, więc zacznijmy od prostego przykładu. Poniżej przedstawiona jest najprostsza możliwa definicja klasy w Ruby: class A end
Kod ten powoduje wygenerowanie w pamięci następujących struktur (patrz rysunek 1.1).
Rysunek 1.1. Struktury danych dla pojedynczej klasy
Prostokąty z podwójnymi ramkami reprezentują obiekty klas — obiekty, których wskaźnik klass wskazuje na obiekt Class. Wskaźnik super wskazuje na obiekt klasy Object, co oznacza, że A dziedziczy po Object. Od tego momentu będziemy pomijać wskaźniki klass dla Class, Module oraz Object, jeżeli nie będzie to powodowało niejasności. Następnym przypadkiem w kolejności stopnia skomplikowania jest dziedziczenie po jednej klasie. Dziedziczenie klas wykorzystuje wskaźniki super. Utwórzmy na przykład klasę B dziedziczącą po A: class B < A end
Wynikowe struktury danych są przedstawione na rysunku 1.2. Słowo kluczowe super pozwala na przechodzenie wzdłuż łańcucha dziedziczenia, tak jak w poniższym przykładzie: class B def initialize logger.info "Tworzenie obiektu B" super end end
Wywołanie super w initialize pozwala na przejście standardowej metody wyszukiwania metod, zaczynając od A#initialize.
14
|
Rozdz ał 1. Techn k podstawowe
Rysunek 1.2. Jeden poziom dziedziczenia
Konkretyzacja klas Teraz możemy przedstawić sposób wyszukiwania metod. Na początek utworzymy instancję klasy B: obj = B.new
Powoduje to utworzenie nowego obiektu i ustawienie wskaźnika klass na obiekt klasowy B (patrz rysunek 1.3).
Rysunek 1.3. Konkretyzacja klas
Podstawy Ruby
|
15
Pojedyncza ramka wokół obj reprezentuje zwykły obiekt. Trzeba pamiętać, że każdy prostokąt na tym diagramie reprezentuje instancje obiektu. Jednak prostokąty o podwójnej ramce, reprezentujące obiekty, są obiektami klasy Class (których wskaźnik klass wskazuje na obiekt Class). Gdy wysyłamy komunikat do obj: obj.to_s
realizowany jest następujący łańcuch operacji:
1. Wskaźnik klass obiektu obj jest przesuwany do B; w metodach klasy B (w m_tbl) wyszukiwana jest odpowiednia metoda.
2. W klasie B nie zostaje znaleziona odpowiednia metoda. Wykorzystywany jest wskaźnik super z obiektu klasy B i metoda jest poszukiwana w klasie A.
3. W klasie A nie zostaje znaleziona odpowiednia metoda. Wykorzystywany jest wskaźnik super z obiektu klasy A i metoda jest poszukiwana w klasie Object.
4. Klasa Object zawiera metodę to_s w kodzie natywnym (rb_any_to_s). Metoda ta jest
wywoływana z parametrem takim jak #. Metoda rb_any_to_s analizuje wskaźnik klass odbiorcy w celu określenia nazwy klasy wyświetlenia; dlatego pokazywana jest nazwa B, pomimo tego, że wywoływana metoda znajduje się w Object.
Dołączanie modułów Gdy zaczniemy korzystać z modułów, sprawa stanie się bardziej skomplikowana. Ruby obsługuje dołączanie modułów zawierających ICLASS6, które są pośrednikami modułów. Gdy dołączamy moduł do klasy, Ruby wstawia ICLASS reprezentujący dołączony moduł do łańcucha super dołączającej klasy. W naszym przykładzie dołączania modułu uprościmy nieco sytuację przez zignorowanie klasy B. Zdefiniujemy moduł i dodamy go do A, co spowoduje powstanie struktur danych przedstawionych na rysunku 1.4: module Mixin def mixed_method puts "Witamy w mixin" end end class A include Mixin end
Tutaj właśnie do gry wkracza ICLASS. Wskaźnik super wskazujący z A na Object jest przechwytywany przez nowy ICLASS (reprezentowany przez kwadrat narysowany przerywaną linią). ICLASS jest pośrednikiem dla modułu Mixin. Zawiera on wskaźniki do tablic iv_tbl z Mixin (zmienne instancyjne) oraz m_tbl (metody).
6
ICLASS jest nazwą dla klas pośredniczących, wprowadzoną przez Mauricia Fernándeza. Nie mają one oficjalnej nazwy, ale w kodzie źródłowym Ruby noszą nazwę T_ICLASS.
16
|
Rozdz ał 1. Techn k podstawowe
Rysunek 1.4. Włączenie modułu w łańcuch wyszukiwania
Na podstawie tego diagramu można łatwo wywnioskować, do czego służą nam klasy pośredniczące — ten sam moduł może zostać dołączony do wielu różnych klas; klasy mogą dziedziczyć po różnych klasach (i przez to mieć inne wskaźniki super). Nie możemy bezpośrednio włączyć klasy Mixin do łańcucha wyszukiwania, ponieważ jego wskaźnik super będzie wskazywał na dwa różne obiekty, jeżeli zostanie dołączony do klas mających różnych rodziców. Gdy utworzymy obiekt klasy A, struktury będą wyglądały jak na rysunku 1.5. objA = A.new
Rysunek 1.5. Wyszukiwanie metod dla klasy z dołączonym modułem
Wywołujemy tu metodę mixed_method z obiektu mixin, z objA jako odbiorcą: objA.mixed_method # >> Witamy w mixin
Podstawy Ruby
|
17
Wykonywany jest następujący proces wyszukiwania metody:
1. W klasie obiektu objA, czyli A, wyszukiwana jest pasująca metoda. Żadna nie zostaje znaleziona.
2. Wskaźnik super klasy A prowadzi do ICLASS, który jest pośrednikiem dla Mixin. Pasująca metoda jest wyszukiwana w obiekcie pośrednika. Ponieważ tablica m_tbl pośrednika jest taka sama jak tablica m_tbl klasy Mixin, metoda mixed_method zostaje odnaleziona i wywołana.
W wielu językach mających możliwość dziedziczenia wielobazowego występuje problem diamentu, polegający na braku możliwości jednoznacznego identyfikowania metod obiektów, których klasy mają schemat dziedziczenia o kształcie diamentu, jak jest to pokazane na rysunku 1.6.
Rysunek 1.6. Problem diamentu przy dziedziczeniu wielobazowym
Biorąc jako przykład diagram przedstawiony na tym rysunku, jeżeli obiekt klasy D wywołuje metodę zdefiniowaną w klasie A, która została przesłonięta zarówno w B, jak i C, nie można jasno określić, która metoda zostanie wywołana. W Ruby problem ten został rozwiązany przez szeregowanie kolejności dołączania. W czasie wywoływania metody łańcuch dziedziczenia jest przeszukiwany liniowo, dołączając wszystkie ICLASS dodane do łańcucha. Trzeba przypomnieć, że Ruby nie obsługuje dziedziczenia wielobazowego; jednak wiele modułów może być dołączonych do klas i innych modułów. Z tego powodu A, B oraz C muszą być modułami. Jak widać, nie występuje tu niejednoznaczność; wybrana zostanie metoda dołączona jako ostatnia do łańcucha wywołania: module A def hello "Witamy w A" end end module B include A def hello "Witamy w B" end
18
|
Rozdz ał 1. Techn k podstawowe
end module C include A def hello "Witamy w C" end end class D include B include C end D.new.hello # => "Witamy w C"
Jeżeli zmienimy kolejność dołączania, odpowiednio zmieni się wynik: class D include C include B end D.new.hello # => "Witamy w B"
W przypadku ostatniego przykładu, gdzie B został dołączony jako ostatni, diagram obiektów jest przedstawiony na rysunku 1.7 (dla uproszczenia wskaźniki od Object i Class zostały usunięte).
Rysunek 1.7. Rozwiązanie problemu diamentu w Ruby — szeregowanie
Podstawy Ruby
|
19
Klasa singleton Klasy singleton (również metaklasy lub eigenklasy; patrz następna ramka, „Terminologia klas singleton”) pozwalają na zróżnicowanie działania obiektu w stosunku do innych obiektów danej klasy. Czytelnik prawdopodobnie spotkał się wcześniej z notacją pozwalającą na otwarcie klasy singleton: class A end objA = A.new objB = A.new objA.to_s # => "#" objB.to_s # => "#" class < "Obiekt A" objB.to_s # => "#"
Rails korzysta z tablic mieszających do przekazywania słów kluczowych argumentów do wielu metod. Jest to realizowane dzięki syntaktycznym zaletom Ruby — jeżeli jako ostatni argument funkcji zostanie przekazana tablica mieszająca, można pominąć nawiasy, co powoduje, że wywołanie przypomina słowa kluczowe argumentów. Jednak Ruby nie posiada żadnej własnej obsługi słów kluczowych argumentów, więc Rails musi dostarczyć kilka funkcji pomocniczych. ActiveSupport wspiera takie przetwarzanie opcji za pomocą rozszerzenia klasy Hash. • Hash#diff(other) tworzy tablicę mieszającą z parami klucz-wartość, które znajdują się
w jednej, ale nie w drugiej tablicy. a = {:a => :b, :c => :d} b = {:e => :f, :c => :d} a.diff(b) # => {:e=>:f, :a=>:b}
• Hash#stringify_keys (zwracająca kopię) oraz Hash_stringify_keys! (bezpośrednio
modyfikująca odbiorcę) konwertują klucze na ciągi znaków. Hash#symbolize_keys oraz Hash#symbolize_keys! konwertują klucze na symbole. Metody symbolize mają synonimy to_options oraz to_options!. h = {:a => 123, "b" => 456} h.stringify_keys # => {"a"=>123, "b"=>456} h.symbolize_keys # => { a=>123, b=>456}
Core Extens ons
|
71
• Hash#assert_valid_keys(:key1, …) zgłasza wyjątek ArgumentError, jeżeli tablica za-
wiera klucze, które nie znajdują się na liście argumentów. Jest to wykorzystywane do sprawdzania, czy do funkcji z argumentami bazującymi na kluczach są przekazane tylko prawidłowe opcje. • Hash#reverse_merge oraz Hash#reverse_merge! (modyfikująca bezpośrednio) działa jak
Hash#merge, ale w odwrotnej kolejności; w przypadku powielonych kluczy istniejące
krotki są ważniejsze niż te z argumentu. Może to być wykorzystywane do realizacji domyślnych argumentów funkcji. options = {:a => 3, :c => 5} options.reverse_merge!(:a => 1, :b => 4) options # => { a=>3, b=>4, c=>5}
• Hash#slice zwraca nową tablicę mieszającą zawierającą tylko wyspecyfikowane klucze.
Metoda Hash#except działa odwrotnie i zwraca nową tablicę mieszającą z usuniętymi wymienionymi kluczami. options = {:a => 3, :b => 4, :c => 5} options.slice(:a, :c) # => { c=>5, a=>3} options.except(:a) # => { b=>4, c=>5}
Hash#slice oraz Hash#except mają również wersje działające bezpośrednio, odpowiednio Hash#slice! oraz Hash#except!.
HashWithIndifferentAccess
core_ext/hash/indifferent_access.rb
HashWithIndifferentAccess to tablica, której elementy są dostępne przy pomocy ciągów
HashWithIndifferentAccess jest przede wszystkim używana do udostępniania użytkownikom prostszego API; programista może na przykład użyć params[:user] lub params[ user ]. Metody stringify_keys lub symbolize_keys powinny być używane w przetwarzaniu opcji.
Jednak HashWithIndifferentAccess dodatkowo poprawia całą klasę naruszeń bezpieczeństwa w Rails. W Ruby 1.8 nie było wykonywane usuwanie nieużywanych symboli, więc dostęp do tablicy mieszającej params wyłącznie przy pomocy symboli (jak to zwykle było realizowane) mógł prowadzić do ataku typu odmowa usługi. Złośliwy klient mógł doprowadzić do wycieku pamięci i zapełnić tablicę symboli przez wykonywanie wielu żądań z unikalnymi nazwami parametrów. Przypadkowo Ruby 1.9 unieważnia potrzebę stosowania HashWithIndifferentAccess, ponieważ symbole i ciągi będą traktowane identycznie (:aoeu=='aoeu').
• Integer#ordinalize konwertuje liczbę całkowitą na numer kolejny. puts (1..10).map {|i| i.ordinalize}.to_sentence # >> 1st, 2nd, 3rd, 4th, 5th, 6th, 7th, 8th, 9th, 10th
Kernel Daemonize
core_ext/kernel/daemonizing.rb
Normalnie, gdy proces jest podłączony do konsoli i konsola zakończy działanie, kończy się również działanie programu. Metoda Kernel#daemonize jest używana do odłączenia procesu Ruby od konsoli, co pozwala na kontynuowanie działania, nawet jeżeli konsola zostanie wyłączona. Typowy proces daemona wykorzystuje metodę daemonize w sposób przedstawiony w następującym przykładzie: # konfiguracja serwera; przetwarzanie opcji daemonize # odłączenie od tty loop do # oczekiwanie na żądania # przetwarzanie żądania end
Wysłanie sygnału SIGTERM do odłączonego procesu powoduje przerwanie jego działania.
Raportowanie
core_ext/kernel/reporting.rb
Metody najwyższego poziomu znajdujące się w tym pliku sterują możliwymi do wyświetlenia komunikatami konsoli. • silence_warnings wyłącza ostrzeżenia w bieżącym bloku. enable_warnings włącza
ostrzeżenia w bieżącym bloku.
• silence_stream(stream) powoduje wyłączenie danego strumienia (zwykle STDOUT lub
STDERR) w bieżącym bloku.
• suppress(*exception_classes) ignoruje w bieżącym bloku błędy określonej klasy.
Module Synonimy
core_ext/module/aliasing.rb
• alias_method_chain(target, feature) obejmuje metodę w nową funkcję, zwykle do-
dając nowe operacje. Mechanizm ten był omówiony w rozdziale 1. Wywołanie metody: alias_method_chain :target, :feature jest odpowiednikiem: alias_method :target_without_feature, :target alias_method :target, :target_with_feature
Jedną z konsekwencji jest to, że metoda target_with_feature musi istnieć przed wywołaniem alias_method_chain. Znaki przestankowe (na końcu metod ?, ! oraz =) są prawidłowo przenoszone na koniec nazwy metody. Core Extens ons
|
73
• alias_attribute(new_name, old_name) tworzy synonim atrybutu ActiveRecord z nową
nazwą, dodając metodę pobierającą, ustawiającą i predykat (na przykład person.name?).
Delegacja
core_ext/module/delegation.rb
Metoda delegate zapewnia proste mechanizmy delegowania jednej lub więcej metod do obiektu: class StringProxy attr_accessor :target delegate :to_s, :to => :target def initialize(target) @target = target end end proxy = StringProxy.new("Hello World!") proxy.to_s # => "Witaj!" proxy.target = "Do widzenia" proxy.to_s # => "Do widzenia"
Większą kontrolę nad delegowaniem można uzyskać, korzystając z klasy Delegator z biblioteki standardowej.
• Module#included_in_classes przegląda wszystkie klasy (przy użyciu ObjectSpace),
zbierając kasy i moduły, w których został użyty odbiorca. Enumerable.included_in_classes.include? Array # => true Enumerable.included_in_classes.include? String # => true Enumerable.included_in_classes.include? Numeric # => false
• Module#parent oraz Module#parents pozwalają na analizę hierarchii przestrzeni nazw
modułu: module A module B class C end end end A::B::C.parent A::B.parent A.parent
# => A B # => A # => Object
A::B::C.parents # => [A B, A, Object]
Konwersje numeryczne
core_ext/numeric/bytes.rb
Metody te konwertują wyrażenia bajtowe, na przykład 45.megabytes, na odpowiadającą im liczbę bajtów (45.megabytes == 47_185_920). Tak jak w przypadku konwersji numerycznych na jednostkach czasu, akceptowane są jednostki w liczbie pojedynczej i mnogiej (1.kilobyte lub 3.kilobytes). Prawidłowymi jednostkami są bytes, kilobytes, megabytes, gigabytes, terabytes, petabytes oraz exabytes.
74
|
Rozdz ał 2. Act veSupport oraz Ra lT es
Object instance_exec
core_ext/object/extending.rb, core_ext/proc.rb
Metoda instance_exec pozwala nam na wartościowanie bloku w kontekście instancji. Jest ona podobna do instance_eval, poza tym, że pozwala na przekazanie argumentów do bloku. Jej implementacja znajduje się w pliku object/extending.rb. Implementacja instance_exec korzysta z implementacji Proc#bind z proc.rb. Metoda ta pozwala na traktowanie Procs jako UnboundMethods; mogą być one dołączane do obiektu i wywoływane jak metody. Sposób realizacji jest przedstawiony w poniższym fragmencie kodu: class Proc #:nodoc: def bind(object) block, time = self, Time.now (class << object; self end).class_eval do method_name = "__bind_#{time.to_i}_#{time.usec}" define_method(method_name, &block) method = instance_method(method_name) remove_method(method_name) method end.bind(object) end end
Blok class_eval jest wartościowany w kontekście klasy singletonu obiektu, więc tworzona jest specyficzna dla tego obiektu metoda o (mam nadzieję) unikalnej nazwie, przy użyciu Procs jako treści metody. Metoda instance_method przechwytuje nowo utworzony obiekt metody z klasy jako UnboundMethod. Ponieważ przechowujemy referencję metody, możemy bezpiecznie usunąć ją z klasy singletonu, wykorzystując remove_method, a metoda będzie nadal istniała (jako anonimowa). W ostatnim wierszu class_eval zwracana jest metoda z class_eval — metoda ta jest dołączana do bieżącego obiektu i zwracana. Ta definicja instance_exec stanowi obejście w Ruby 1.8. W Ruby 1.9 instance_exec jest metodą natywną. Należy również zwrócić uwagę, że ta implementacja nie jest bezpieczna dla wątków. Próbuje być ona tak bezpieczna, jak jest to możliwe, kwalifikując nazwy metod w czasie mikrosekund i umieszczając je w odpowiednich obiektach klas singleton, ale i tak spowoduje awarię, jeżeli zostanie wywołana dwukrotnie na tym samym obiekcie w czasie tych samych mikrosekund.
Różne metody
core_ext/object/misc.rb
• Object#returning pozwala wykonać pewne operacje na obiekcie, a następnie zwrócić go.
Jest to zwykle używane do hermetyzowania części incydentalnych operacji na obiekcie w postaci bloku: returning(Person.new) do |p| p.name = "Brad" end # => #
• Object#with_options zapewnia możliwość wyodrębniania nadmiarowych opcji dla wielu
wywołań metod. Jest to zwykle używane w kodzie sterującym: map.with_options(:controller => "person") do |person| person.default "", :action => "index" person.details "/details/:id", :action => "details" end
Core Extens ons
|
75
Wywołanie with_options zwraca OptionMerger, będący pośrednikiem przekazującym wszystkie wywołania metod do kontekstu (który w naszym przykładzie jest oryginalnym odwzorowaniem). Dostarczone opcje są łączone w ostatnim argumencie wywołania metody, więc efekt wywołania person.default jest taki sam jak: map.default "", :action => "index", :controller => "person" map.details "/details/:id", :action => "details", :controller => "person"
Nie ma tu żadnych dodatkowych operacji powodujących, że metoda ta jest specyficzna dla ścieżek Rails, można jej używać razem z dowolną metodą oczekującą jako ostatniego parametru tablicy mieszającej słów kluczowych, tak jak w Rails. Obiekt pośredniczący przekazywany do bloku with_options deleguje wszystkie nieznane wywołania metod do obiektu docelowego.
• Range#include? pozwala sprawdzić, czy zakres całkowicie zawiera się w innym zakresie: (1..10).include?(3..5) # => true (1..10).include?(3..15) # => false
• Range#overlaps? pozwala sprawdzić, czy zakres całkowicie pokrywa inny zakres: >> (1..10).overlaps?(3..15) # => true >> (1..10).overlaps?(13..15) # => false
String Inflector
core_ext/string/inflections.rb
Metody z tego pliku są delegowane do klasy Inflector. • String#singularize oraz String#pluralize pozwalają uzyskać liczbę pojedynczą i mnogą.
Przypadki szczególne nie są w pełni obsługiwane, ale można dodać własne zasady nadpisujące zasady domyślne. "wug".pluralize # => "wugs" "wugs".singularize # => "wug" "fish".pluralize # => "fish"
• String#camelize jest używana do konwersji nazw plików na nazwy klas; String#
• String#tableize konwertuje nazwy klas na nazwy tabel (tak jak w ActiveRecord).
String#classify konwertuje nazwy tabel na nazwy klas. "ProductCategory".tableize # => "product_categories" "product_categories".classify # => "ProductCategory"
• String#constantize próbuje zinterpretować dany ciąg jako nazwę stałej. Jest to przydatne
do wyszukiwania klas zwróconych przez String#classify. "ProductCategory".constantize rescue "no class" # => "no class" class ProductCategory; end "ProductCategory".constantize # => ProductCategory
Pliki te zawierają interfejsy do ActiveSupport::Multibyte pozwalające na obsługiwanie znaków wielobajtowych. Metody te korzystają z bieżącej wartości zmiennej globalnej $KCODE, która określa wykorzystywane kodowanie znaków. Zmienna $KCODE może przyjmować następujące wartości: $KCODE
Kodowan e
e, E
EUC
s, S
Shift J S
u, U
UTF 8
a, A, n, N
ASC (doyślne)
W Rails 1.2 $KCODE jest automatycznie ustawiana na u, co włącza operacje wielobajtowe. • String#chars zwraca instancję ActiveSupport::Multibyte::Chars, która jest pośredni-
kiem dla klasy String. Chars definiuje metody działające prawidłowo w Unicode (przy prawidłowym ustawieniu $KCODE) i stanowi obiekt pośredniczący dla metod, które „nie wiedzą” nic na temat String: str = "
• String#to_time (domyślnie UTC), String#to_time(:local) oraz String#to_date po-
zwalają w łatwy sposób delegować operacje do ParseDate: "1/4/2007".to_date.to_s # => "2007-01-04" "1/4/2007 2:56 PM".to_time(:local).to_s # => "Thu Jan 04 14 56 00 CST 2007"
• String#starts_with?(prefix) oraz String#ends_with?(suffix) kontrolują, czy ciąg za-
czyna się lub kończy innym ciągiem. "aoeu".starts_with?("ao") "aoeu".starts_with?("A") "aoeu".ends_with?("eu") "aoeu".ends_with?("foo")
# # # #
=> true => false => true => false
Symbol#to_proc
symbol.rb
ActiveSupport definiuje tylko jedno rozszerzenie dla Symbol, ale jest ono szczególnie zaawansowane — umożliwia konwersję symbolu na Proc przy użyciu Symbol#to_proc. Idiom ten jest używany obecnie w całym Rails. Kod tego typu: (1..5).map {|i| i.to_s } # => ["1", "2", "3", "4", "5"]
jest przekształcany na następujący, przy wykorzystaniu Symbol#to_proc: (1..5).map(&:to_s) # => ["1", "2", "3", "4", "5"]
Symbol & informuje Ruby, aby traktować symbol :to_s jako argument bloku — Ruby „wie”, że argument powinien być obiektem Proc i próbuje przekształcić go przez wywołanie metody to_proc. ActiveSupport dostarcza metody Symbol#to_proc, która zwraca właśnie obiekt Proc i wywołanie powoduje uruchomienie metody podanej w pierwszym argumencie.
TimeZone Instancje TimeZone są obiektami wartościowymi7 reprezentującymi określoną strefę czasową (przesunięcie w stosunku do UTC i nazwa). Są one używane do wykonywania konwersji między różnymi strefami czasowymi: Time.now # => Thu Oct 25 00 10 52 -0500 2007 # UTC-05 TimeZone[-5].now # => Thu Oct 25 00 10 52 -0500 2007 TimeZone[-5].adjust(1.month.ago) # => Tue Sep 25 00 10 52 -0500 2007
7
Obiekt wartościowy jest obiektem reprezentującym wartość, którego identyfikatorem jest tylko jego wartość. Inaczej mówiąc, dwa obiekty są traktowane jako równe, jeżeli mają ten sam stan.
78
|
Rozdz ał 2. Act veSupport oraz Ra lT es
RailTies RailTies to zbiór komponentów łączących ze sobą ActiveRecord, ActionController oraz ActionView w celu utworzenia Rails. Przedstawimy dwie najważniejsze części RailTies — sposób inicjalizacji Rails oraz sposób przetwarzania żądań.
Konfiguracja Rails Klasa Rails::Configuration, definiowana w initializer.rb, przechowuje atrybuty konfiguracji sterujące pracą Rails. Istnieje kilka ogólnych parametrów Rails zdefiniowanych jako atrybuty klasy Configuration, ale w zrębach klas frameworku znajduje się niewiele „inteligencji”. Pięć klas zrębów (action_controller, action_mailer, action_view, active_resource oraz active_record) działa jako pośredniki dla klas atrybutów z odpowiednich klas Base. Dlatego następująca instrukcja konfiguracyjna: config.action_controller.perform_caching = true
jest tym samym co: ActionController::Base.perform_caching = true
oprócz tego, że została użyta zunifikowana składnia.
Inicjalizacja aplikacji w 20 prostych krokach initializer.rb Rails::Initializer to główna klasa obsługująca konfigurację środowiska Ruby on Rails. Ini-
cjalizacja jest uruchamiana przez zawartość pliku config/environment.rb, w którym znajduje się blok: Rails::Initializer.run do |config| # (konfiguracja) end
Rails::Initializer.run przekazuje do bloku nowy obiekt Rails::Configuration. Następnie metoda run tworzy nowy obiekt Rails::Initializer i wywołuje jego metodę process, która wykonuje następujące operacje w celu zainicjowania Rails:
1. check_ruby_version — kontroluje, czy używany jest Ruby 1.8.2 lub nowszy (ale nie 1.8.3). 2. set_load_path — dodaje do ścieżki ładowania Ruby ścieżki frameworku (RailTies, ActionPack8, ActiveSupport, ActiveRecord, Action Mailer oraz Action Web Service) i ścieżki ładowania aplikacji. Framework jest ładowany z katalogu vendor/rails lub lokalizacji zdefiniowanej w RAILS_FRAMEWORK_ROOT.
3. require_frameworks — ładuje każdy framework wymieniony w opcji konfiguracji frame-
works. Jeżeli ścieżka frameworka nie została zdefiniowana w RAILS_FRAMEWORK_ROOT i nie istnieje w vendor/rails, inicjalizacja zakłada, że frameworki są zainstalowane jako RubyGems.
4. set_autoload_paths — ustawia ścieżki automatycznego ładowania bazujące na wartościach zmiennych konfiguracyjnych load_paths oraz load_once_paths. Określają one, które
8
ActionPack = ActionController + ActionView.
Ra lT es
|
79
ścieżki będą przeszukiwane w celu odnalezienia nieznanych stałych. Opcja load_paths jest tą samą, która dostarcza ścieżek ładowania aplikacji używanych w kroku 2.
5. load_environment — ładuje i wartościuje plik konfiguracyjny specyficzny dla środowiska (programowanie, produkcja lub testy).
6. initialize_encoding — ustawia $KCODE na u w celu włączenia obsługi UTF w Rails. 7. initialize_database — jeżeli wykorzystywany jest ActiveRecord, w kroku tym tworzona jest konfiguracja bazy danych i następuje podłączenie do jej serwera.
8. initialize_logger — konfiguruje proces rejestrujący i ustawia stałą najwyższego poziomu RAILS_DEFAULT_LOGGER na jego instancję. Jeżeli w konfiguracji wyspecyfikowany jest logger, zostaje on użyty. Jeżeli nie, tworzony jest nowy proces rejestrujący i kierowany do log_path. Jeżeli operacja ta się nie uda, wyświetlane jest ostrzeżenie i rejestrowanie jest kierowane na standardowe wyjście błędów.
9. initialize_framework_logging — ustawia rejestrowanie dla ActiveRecord, ActionController oraz Action Mailer (jeżeli są użyte) na skonfigurowany właśnie obiekt rejestrowania.
10. initialize_framework_views: Ustawia ścieżki widoku dla ActionController oraz Action Mailer na wartość elementu konfiguracji view_path.
dla frameworka na wywołania metod z klas Base frameworka. Jako przykład można wziąć następującą opcję konfiguracji: config.acti e_record.schema_format = :sql
Obiekt config.active_record jest instancją Rails::OrderedOptions, czyli w rzeczywistości uporządkowaną tablicą mieszającą (uporządkowanie zapewnia zachowanie kolejności dyrektyw konfiguracji). W czasie inicjalizacji metoda initialize_framework_settings przekształca ją na następującą: ActiveRecord::Base.schema_format = :sql
Ma to taką zaletę, że obiekt Configuration nie musi być aktualizowany przy każdym dodaniu lub zmianie opcji konfiguracji przez framework.
15. add_support_load_paths — dodaje ścieżki ładowania do funkcji pomocniczych. Funkcja ta jest obecnie pusta.
16. load_plugins — ładuje wtyczki z ścieżek zapisanych w elemencie konfiguracji plugin_paths (domyślnie vendor/plugins). Jeżeli zostanie zdefiniowany element konfiguracji plugins, wtyczki te są ładowane w takiej właśnie kolejności. Wtyczki są ładowane pod koniec całego procesu, więc mogą one modyfikować dowolny załadowany wcześniej komponent.
17. load_observers — tworzy obserwatory ActiveRecord. Jest to realizowane po wtyczkach, więc wtyczki te mają możliwość modyfikowania klas obserwatorów.
80
|
Rozdz ał 2. Act veSupport oraz Ra lT es
18. initialize_routing — ładuje i przetwarza ścieżki. Dodatkowo ustawia ścieżki kontrolera z elementu konfiguracji controller_paths.
19. after_initialize — wywołuje wszystkie zdefiniowane przez użytkownika metody wywołania zwrotnego after_initialize. Te metody wywołania zwrotnego są definiowane w bloku konfiguracji przez użycie config.after_initialize { … }.
20. load_application_initializers — ładuje wszystkie pliki Ruby z RAILS_ROOT/config/ ´initializers i jego podkatalogów. Stara inicjalizacja frameworku, która poprzednio znajdowała się w pliku config/environment.rb, może być teraz podzielona na osobne inicjalizery. Teraz framework jest gotowy do odbierania żądań.
Rozsyłanie żądań dispatcher.rb, fcgi_handler.rb, webrick_server.rb Klasa Dispatcher jest poza światem interfejsów Rails. Serwer WWW przesyła żądanie do Rails przez wywołanie Dispatcher.dispatch(cgi, session_options, output). Rails przetwarza dane żądanie CGI i prezentuje wynik w podanej lokalizacji (domyślnie jest to standardowe wyjście). Aplikacja Rails może być zresetowana przez wywołanie Dispatcher.reset_application! w celu przetworzenia wielu żądań. Istnieje wiele sposobów udostępniania aplikacji Rails. fcgi_handler.rb zawiera proces obsługi FastCGI (RailsFCGIHandler), który pośredniczy pomiędzy serwerem działającym w standardzie FastCGI (Apache, lighttpd, a nawet IIS) a Rails. webrick_server.rb to serwer bazujący na WEBrick, który może udostępniać Rails. Jednak zalecanym serwerem aplikacji, zarówno do programowania, jak i instalacji, jest Mongrel, którego autorem jest Zed Shaw. Mongrel9 zawiera własny proces obsługi Rails, który bezpośrednio wywołuje metody klasy Dispatcher, wykorzystując własne procedury CGI. Oczywiście więcej informacji można znaleźć w kodzie źródłowym serwera Mongrel.
Propozycje dalszych lektur Książka Diomidisa Spinellisa, Code Reading: The Open Source Perspective (Addison-Wesley), zawiera porady na temat korzystania z dużych baz kodu, szczególnie w przypadku oprogramowania open source. Biblioteka podstawowa Ruby Facets10 jest kolejnym zbiorem kodu zapewniającym metody użytkowe dla Ruby. Biblioteka ta zawiera część funkcji wspólnych z Core Extensions, ale zapewnia również dodatkowe rozszerzenia. W przypadku konieczności wykonywania bardziej skomplikowanych manipulacji językiem angielskim, niż dostępne w klasie Inflector, warto zapoznać się z projektem Ruby Linguistics11.
9
http://mongrel.rubyforge.org.
10
http://facets.rubyforge.org.
11
http://www.deveiate.org/projects/Linguistics.
Propozycje dalszych lektur
|
81
82
|
Rozdz ał 2. Act veSupport oraz Ra lT es
ROZDZIAŁ 3.
Wtyczki Rails
Cywilizacje rozwijają się przez zwiększanie liczby ważnych operacji, które mogą wykonywać bez myślenia o nich. Alfred North Whitehead
Ruby on Rails jest bardzo zaawansowany, ale nie może robić wszystkiego. Istnieje wiele funkcji, które są zbyt eksperymentalne, sytuują się poza zakresem rdzenia Rails lub są po prostu niezgodne ze sposobem, w jaki został zaprojektowany Rails (jest to w końcu opiniowane oprogramowanie). Podstawowy zespół nie może dołączać do Rails wszystkiego, co ktokolwiek sobie zażyczy. Na szczęście Rails ma bardzo elastyczny system rozszerzeń. Wtyczki Rails pozwalają programistom na rozszerzanie i zmienianie niemal dowolnej części frameworka i udostępnianie tych modyfikacji innym w sposób hermetyczny i dający się wielokrotnie używać.
Wtyczki Ładowanie wtyczek Domyślnie wtyczki są ładowane z katalogów poniżej vendor/plugins, z głównego katalogu aplikacji Rails. Jeżeli wymagana jest zmiana lub dodanie kolejnej ścieżki, należy skorzystać z elementu konfiguracji plugin_paths, który zawiera ścieżki ładowania wtyczek: config.plugin_paths += [File.join(RAILS_ROOT, 'vendor', 'other_plugins')]
Domyślnie wtyczki są ładowane w kolejności alfabetycznej — attachment_fu jest ładowana przed http_authentication. Jeżeli wtyczki wzajemnie od siebie zależą, w elemencie konfiguracji plugins można ręcznie określić kolejność ładowania. config.plugins = %w(wymagana_wtyczka bieżąca wtyczka)
Wtyczki niewymienione w elemencie config.plugins nie zostaną załadowane. Jeżeli jednak jako ostatni zostanie podany symbol :all, Rails załaduje od tego miejsca wszystkie pozostałe wtyczki. Jako nazwy wtyczek akceptowane są zarówno symbole, jak i teksty. config.plugins = [ :wymagana_wtyczka :bieżąca wtyczka, :all ]
83
Ścieżka konfiguracji wtyczek jest przeszukiwana rekurencyjnie. Dzięki temu można umieszczać wtyczki w katalogach — na przykład vendor/plugins/active_record_acts oraz vendor/plugins/view_ ´extensions. System wyszukiwania i ładowania wtyczek jest rozszerzalny, więc można pisać własne strategie. System wyszukiwania (domyślnie Rails::Plugin::FileSystemLocator) jest odpowiedzialny za wyszukanie wtyczki, system ładowania (domyślnie Rails::Plugin::Loader) określa, czy katalog zawiera wtyczkę, i wykonuje operację ładowania. Przed rozpoczęciem tworzenia własnego systemu wyszukiwania i ładowania warto zapoznać się z plikami railties/lib/rails/plugin/locator.rb oraz railties/lib/rails/plugin/loader.rb. Systemy wyszukiwania (można użyć więcej niż jednego) oraz system ładowania mogą być zmienione za pomocą następujących dyrektyw konfiguracji: config.plugin_locators += [MyPluginLocator] config.plugin_loader = MyPluginLoader
Instalowanie wtyczek Rails Wtyczki są najczęściej ładowane za pomocą wbudowanego narzędzia script/plugin. Narzędzie to zawiera kilka poleceń: discover/source/unsource/sources Narzędzie plugin korzysta metody wyszukiwania wtyczek ad hoc. Program script/plugin nie wymaga określania adresu URL katalogu wtyczek, ale próbuje znaleźć go samodzielnie. Jednym ze sposobów realizacji tego zadania jest analiza strony Plugins z wiki1 Rails w poszukiwaniu źródłowych adresów URL. Można to wykonać za pomocą polecenia discover. Polecenia source i unsource odpowiednio dodają i usuwają źródłowe adresy URL. Polecenie sources pozwala wyświetlić wszystkie bieżące źródłowe adresy URL. install/update/remove Polecenia te pozwalają na instalowanie, aktualizację i odinstalowywanie wtyczek. Mogą przyjmować jako parametr URL HTTP, URL Subversion (svn:// lub svn+ssh://) lub zwykłą nazwę wtyczki, w którym to przypadku skanowana jest lista źródeł. Polecenie script/plugin install przyjmuje jako opcję -x, która powoduje zarządzanie wtyczkami jako elementami zewnętrznymi Subversion. Jest to korzystne, ponieważ katalog jest nadal połączony z zewnętrznym repozytorium. Jednak taki sposób jest nieco mało elastyczny — nie można pobierać zmian z nadrzędnego repozytorium. Opcję tę przedstawimy w dalszej części rozdziału.
RaPT RaPT (http://rapt.rubyforge.org/) jest zamiennikiem standardowego instalatora wtyczek Rails, script/plugin. Może on zostać zainstalowany za pomocą polecenia gem install rapt. Pierwszą zaletą RaPT jest to, że pozwala wyszukiwać wtyczki z wiersza poleceń (druga zaleta to taka, że jest niezwykle szybki, ponieważ wszystko buforuje).
1
http://wiki.rubyonrails.org/rails/pages/Plugins.
84
|
Rozdz ał 3. Wtyczk Ra ls
Polecenie rapt search pozwala wyszukiwać wtyczki pasujące do słowa kluczowego. Aby wyszukać wtyczkę, która dodaje do Rails funkcję kalendarza, należy przejść do głównego katalogu aplikacji Rails i wykonać: $ rapt search calendar Calendar Helper Info: http://agilewebdevelopment.com/plugins/show/98 Install: http://topfunky.net/svn/plugins/calendar_helper Calendariffic 0.1.0 Info: http://agilewebdevelopment.com/plugins/show/743 Install: http://opensvn.csie.org/calendariffic/calendariffic/ Google Calendar Generator Info: http://agilewebdevelopment.com/plugins/show/277 Install: svn://rubyforge.org//var/svn/googlecalendar/plugins/googlecalendar dhtml_calendar Info: http://agilewebdevelopment.com/plugins/show/333 Install: svn://rubyforge.org//var/svn/dhtmlcalendar/dhtml_calendar Bundled Resource Info: http://agilewebdevelopment.com/plugins/show/166 Install: svn://syncid.textdriven.com/svn/opensource/bundled_resource/trunk DatebocksEngine Info: http://agilewebdevelopment.com/plugins/show/356 Install: http://svn.toolbocks.com/plugins/datebocks_engine/ datepicker_engine Info: http://agilewebdevelopment.com/plugins/show/409 Install: http://svn.mamatux.dk/rails-engines/datepicker_engine
Wybrana wtyczka może być zainstalowana za pomocą na przykład rapt install datepicker_ ´engine.
Piston W Rails wtyczki są chyba najczęstszym sposobem wykorzystania kodu dostarczanego przez zewnętrznych dostawców (innych niż sam framework Rails). Wymaga to specjalnej uwagi wszędzie tam, gdzie ma znaczenie kontrola wersji. Zarządzanie wtyczkami Rails jako modułami zewnętrznymi Subversion ma kilka wad: • Podczas każdej aktualizacji musi nastąpić kontakt z zewnętrznym serwerem w celu spraw-
dzenia, czy cokolwiek nie zostało zmienione. Jeżeli projekt korzysta z wielu modułów zewnętrznych, może to powodować znaczne obniżenie wydajności. Dodatkowo wprowadza to niepotrzebne zależności — jeżeli zdalny serwer zostanie wyłączony, powoduje to problemy. • Projekt jest generalnie na łasce zmian kodu wykonywanych w zdalnej gałęzi — nie istnieje
prosty sposób na ściągnięcie lub zablokowanie zmian wprowadzanych zdalnie. Jedyną zaletą zastosowania Subversion jest możliwość zablokowania określonych zdalnych rewizji. • Podobnie nie istnieje sposób na wprowadzanie lokalnych modyfikacji do zdalnej gałęzi.
Wszystkie potrzebne modyfikacje mogą być utrzymywane w kopii roboczej, gdzie są niewersjonowane. • Nie jest utrzymywana historia relacji wersji zewnętrznych z lokalnym repozytorium. Je-
żeli będziemy chcieli zaktualizować naszą kopię roboczą do wersji z ostatniego miesiąca, nikt nie będzie wiedział, która wersja modułu zewnętrznego wtedy obowiązywała.
Wtyczk
|
85
Aby rozwiązać ten problem, François Beausoleil napisał program Piston2 pozwalający na zarządzanie gałęziami dostawców w Subversion. Piston importuje zdalne gałęzie do lokalnego repozytorium, synchronizując je na żądanie. Ponieważ w repozytorium projektu znajduje się pełna wersja kodu, może być ona modyfikowana w razie potrzeby. Wszystkie zmiany wprowadzone do kopii lokalnej są łączone w momencie aktualizacji projektu ze zdalnego serwera. Piston jest dostępny jako moduł gem, który można zainstalować za pomocą sudo gem install --include-dependencies piston. Aby zainstalować wtyczkę za pomocą Piston, należy ręcznie odszukać adres URL repozytorium Subversion. Następnie wystarczy zaimportować go za pomocą Piston, podając URL repozytorium i ścieżkę docelową kopii roboczej. $ piston import http://svn.rubyonrails.org/rails/plugins/deadlock_retry \ vendor/plugins/deadlock_retry Exported r7144 from 'http://svn.rubyonrails.org/rails/plugins/deadlock_retry/' to 'vendor/plugins/deadlock_retry' $ svn ci
Polecenie svn ci jest niezbędne, ponieważ Piston dodaje kod do naszej kopii roboczej. Dla Subversion nie różni się to od napisania kodu samodzielnie — jest on wersjonowany wraz z pozostałą częścią aplikacji. Pozwala to bardzo łatwo aktualizować gałąź dostawcy do lokalnego użycia; należy po prostu wprowadzić modyfikacje do kopii roboczej i zatwierdzić je. Gdy przyjdzie czas na aktualizację gałęzi dostawcy, polecenie piston update vendor/plugins/ deadlock_retry pozwala pobrać wszystkie zmiany ze zdalnego repozytorium i połączyć je. Po połączeniu wszystkie lokalne modyfikacje będą zachowane. Polecenie piston update może być wywołane bez argumentów — w takim przypadku rekurencyjnie zaktualizuje wszystkie katalogi kontrolowane przez Piston poniżej bieżącego. Katalogi kontrolowane przez Piston mogą mieć blokowane bieżące wersje za pomocą piston lock i odblokowywane za pomocą piston unlock. Dodatkowo dla bieżących użytkowników svn:externals wszystkie istniejące katalogi zarządzane za pomocą svn:externals mogą być skonwertowane do Piston za pomocą polecenia piston convert. Piston dobrze nadaje się do zarządzania najnowszą wersją Rails wraz z wszystkimi wprowadzanymi poprawkami. Aby zaimportować najnowszą wersję Rails wraz z wszystkimi funkcjami Piston, należy wywołać: $ piston import http://svn.rubyonrails.org/rails/trunk vendor/rails
Decentralizowana kontrola wersji Piston efektywnie tworzy dodatkową warstwę pomiędzy zdalnym repozytorium i kopią roboczą. Decentralizowane systemy kontroli wersji sprowadzają ten model do jego logicznej konsekwencji — każda kopia robocza jest repozytorium, mogącym współdzielić zmiany z innymi repozytoriami. Może to być bardziej elastyczny model niż normalne, centralne systemy kontroli wersji. Paradygmat rozproszonej kontroli wersji przedstawimy dokładniej w rozdziale 10.
2
http://piston.rubyforge.org.
86
|
Rozdz ał 3. Wtyczk Ra ls
Wtyczki i pozostały kod zewnętrzny mogą być zarządzane bardzo skutecznie przy użyciu zdecentralizowanego systemu kontroli wersji. System taki zapewnia większą elastyczność, szczególnie w skomplikowanych środowiskach z wieloma programistami i dostawcami. Dostępne jest narzędzie hgsvn3, które pozwala migrować zmiany z repozytorium SVN do repozytorium Mercurial. Może być ono używane do konfigurowania systemów podobnych do Piston, zapewniających znacznie większą elastyczność. Jedno repozytorium („nadrzędne” lub „przychodzące”) może odwzorowywać zdalne repozytorium, a inne projekty mogą pobierać żądane poprawki z repozytorium nadrzędnego i ignorować niechciane. Modyfikacje lokalne odpowiednie dla repozytorium nadrzędnego mogą być eksportowane do postaci poprawek i wysyłane do osoby odpowiedzialnej za projekt.
Tworzenie wtyczek Gdy wiemy, jak rozszerzać Rails przez otwieranie klas, bardzo łatwo jest napisać wtyczkę. Na początek przyjrzyjmy się strukturze katalogów typowej wtyczki (patrz rysunek 3.1).
Rysunek 3.1. Struktura katalogów typowej wtyczki
W skład wtyczki Rails wchodzi kilka plików i katalogów: about.yml (niepokazany) Jest to najnowsza funkcja wtyczek Rails — wbudowane metadane. Obecnie funkcja ta współdziała wyłącznie z RaPT. Polecenie rapt about nazwa_wtyczki powoduje wyświetlenie podsumowania z informacjami na temat wtyczki. W przyszłości oczekiwane jest dodanie większej liczby funkcji, ale obecnie istnieje ona wyłącznie do celów informacyjnych. Metadane są przechowywane w pliku about.yml; poniżej przedstawiony jest przykład pochodzący z wtyczki acts_as_attachment: author: technoweenie summary: File upload handling plugin. homepage: http://technoweenie.stikipad.com plugin: http://svn.techno-weenie.net/projects/plugins/acts_as_attachment license: MIT version: 0.3a rails_version: 1.1.2+ 3
http://cheeseshop.python.org/pypi/hgsvn.
Tworzen e wtyczek
|
87
init.rb Jest to plik wykorzystywany do inicjalizacji wtyczki. Zazwyczaj znajduje się tu operacja pobrania (require) plików z katalogu lib/. Ponieważ wiele wtyczek modyfikuje podstawowe funkcje, init.rb może rozszerzać klasy podstawowe przy pomocy rozszerzeń z wtyczki: require 'my_plugin' ActionController::Base.send :include, MyPlugin::ControllerExtensions
W tym przypadku niezbędne było użycie sztuczki send, ponieważ Module#include jest metodą statyczną i na razie send pozwala pominąć kontrolę dostępu w odbiorcy4. install.rb (niepokazany) Plik ten jest uruchamiany, gdy wtyczka jest instalowana za pomocą jednego z narzędzi automatycznej instalacji wtyczek, takich jak script/plugin lub RaPT. Zalecane jest, aby nie umieszczać w tym pliku ważnych operacji, ponieważ mogą nie być uruchomione, jeżeli wtyczka jest instalowana ręcznie (przez pobranie kodu źródłowego do katalogu poniżej vendor/plugins). Typowym zastosowaniem install.rb jest wyświetlenie zawartości pliku README: puts IO.read(File.join(File.dirname(__FILE__), 'README'))
lib/
Jest to katalog, w którym znajduje się cały kod wtyczki. Rails dodaje ten katalog do ścieżki ładowania Ruby oraz do ścieżki ładowania Dependencies. Załóżmy, że w katalogu lib/my_plugin.rb mamy klasę MyPlugin. Ponieważ znajduje się ona na ścieżce ładowania Ruby, zwykłe polecenie require 'moja_wtyczka' pozwoli na jego odnalezienie. Ponieważ Dependencies automatycznie ładuje brakujące stałe, można również załadować plik przez zwykłe odwołanie się do MyPlugin w naszej wtyczce.
MIT-LICENSE (lub inny plik licencji, niepokazany) Wszystkie wtyczki, niezależnie od wielkości, powinny zawierać licencję. Brak licencji może uniemożliwić innym użytkownikom używanie naszego oprogramowania — niezależnie od tego, jak mało znacząca może być to wtyczka, rozprowadzanie takiego kodu bez pozwolenia może być niezgodne z prawem. Dla większości projektów wystarczająca jest licencja MIT (na bazie której jest udostępniany Rails). Na warunkach tej licencji można swobodnie rozprowadzać oprogramowanie pod warunkiem, że dołączymy kopię licencji (zachowując notkę o prawach autorskich). W tym przypadku dołączenie pliku MIT-LICENSE jest ważne, ponieważ automatycznie następuje zapewnienie zgodności. Rakefile Jest to plik definicji głównego zadania Rake dla wtyczki. Zwykle jest używany do uruchamiania testów wtyczki lub pakowania wtyczki do dystrybucji. README Dobrze jest umieścić tu krótki opis przeznaczenia wtyczki, zastosowania i wszystkie specjalne instrukcje. W pliku install.rb można dołączyć procedurę (opisaną wcześniej) pozwalającą na wyświetlenie tego pliku w czasie instalacji wtyczki.
4
W Ruby 1.9 metoda Object#send nie ignoruje automatycznie kontroli dostępu w obiekcie odbierającym, ale nowa metoda, Object#send!, już tak.
88
|
Rozdz ał 3. Wtyczk Ra ls
test/
Folder ten zawiera testy wtyczki. Testy są wykonywane za pomocą Rake, bez ładowania Rails. Wszystkie testy umieszczone w tym folderze muszą być autonomiczne — muszą one imitować wszystkie potrzebne funkcje Rails lub faktycznie załadować Rails. Obie te opcje zostaną przedstawione w następnej części rozdziału.
uninstall.rb (niepokazany) Jest to skrypt odinstalowujący, uruchamiany w momencie usunięcia wtyczki za pomocą narzędzia takiego jak script/plugin lub RaPT. Jeżeli nie mamy szczególnych potrzeb, użycie tego pliku nie jest zalecane. Podobnie jak install.rb, uninstall.rb nie zawsze jest używany — wiele osób po prostu usuwa katalog wtyczki bez dłuższego namysłu. Oczywiście możemy dodawać dowolne foldery wymagane przez naszą wtyczkę. W pliku init.rb można użyć wyrażenia File.dirname(__FILE__) w celu odwołania się do katalogu wtyczki. Żaden z tych plików nie jest koniecznie wymagany; na przykład prosta wtyczka może wykonywać wszystkie swoje zadania w init.rb. Szkielet wtyczki można wygenerować przy użyciu wbudowanego w Rails generatora. $ script/generate plugin moja_wtyczka
Powoduje to wygenerowanie szkieletu w katalogu vendor/plugins/moja_wtyczka razem z przykładowymi plikami, licencją MIT z pustymi miejscami do wypełnienia oraz instrukcją.
Przykład wtyczki Aby zilustrować projekt typowej wtyczki Rails, przedstawimy kilka wtyczek dostępnych w repozytorium Subversion rubyonrails.org. Większość z tych wtyczek jest dosyć często wykorzystywana, wiele jest używanych w aplikacji 37signals. Można je uznać za „standardowe biblioteki” Rails. Są one dostępne na stronie http://svn.rubyonrails.org/rails/plugins.
Lokalizacja konta Wtyczki mogą mieć bardzo prostą strukturę. Jako przykład przedstawimy wtyczkę account_ ´location, której autorem jest David Heinemeier Hansson. Wtyczka ta zapewnia kontroler i metody pomocnicze pozwalające wykorzystywać część nazwy domeny jako nazwę konta (na przykład w celu obsługi klientów o nazwach domen klient1.przyklad.pl i klient2. ´przyklad.pl, przy użyciu klient1 oraz klient2 jako kluczy wyszukiwania informacji o kontach). Aby skorzystać z wtyczki, należy dołączyć AccountLocation do jednego lub więcej kontrolerów, co powoduje dodanie odpowiednich metod instancyjnych: class ApplicationController < ActionController::Base include AccountLocation end puts ApplicationController.instance_methods.grep /^account/ => ["account_domain", "account_subdomain", "account_host", "account_url"]
Dołączenie modułu AccountLocation do kontrolera pozwala nam na dostęp do różnych opcji URL z poziomu kontrolera i widoku. Aby na przykład ustawić zmienną @account z poddomeny w każdym żądaniu, można skorzystać z następującego kodu:
Przykład wtyczk
|
89
class ApplicationController < ActionController::Base include AccountLocation before_filter :find_account protected def find_account @account = Account.find_by_username(account_subdomain) end end
Wtyczka account_location nie ma pliku init.rb — nie ma potrzeby konfigurowania podczas ładowania, ponieważ wszystkie funkcje znajdują się w module AccountLocation. Poniżej przedstawiona jest implementacja znajdująca się w pliku lib/account_location.rb (bez części tekstu licencji): module AccountLocation def self.included(controller) controller.helper_method(:account_domain, :account_subdomain, :account_host, :account_url) end protected def default_account_subdomain @account.username if @account && @account.respond_to?(:username) end def account_url(account_subdomain = default_account_subdomain, use_ssl = request.ssl?) (use_ssl ? "https://" : "http://") + account_host(account_subdomain) end def account_host(account_subdomain = default_account_subdomain) account_host = "" account_host << account_subdomain + "." account_host << account_domain end def account_domain account_domain = "" account_domain << request.subdomains[1..-1].join(".") + "." if request.subdomains.size > 1 account_domain << request.domain + request.port_string end def account_subdomain request.subdomains.first end end
Metoda self.included jest standardowym idiomem we wtyczkach — jest wyzwalana po dołączeniu modułu do klasy. W tym przypadku metoda ta oznacza dołączone metody instancyjne jako metody pomocnicze Rails, dzięki czemu mogą być używane z widoku. Jak pamiętamy, Dependencies.load_paths zawiera katalogi lib wszystkich załadowanych wtyczek, dzięki czemu użycie wspomnianego AccountLocation powoduje wyszukanie account_ ´location.rb w katalogach lib. Dzięki temu nie ma potrzeby używania polecenia require w celu skorzystania z wtyczki — wystarczy dodać kod do vendor/plugins.
90
|
Rozdz ał 3. Wtyczk Ra ls
Wymaganie SSL Wtyczka ssl_requirement pozwala określić, że niektóre akcje, muszą być chronione przez SSL. Wtyczka ta dzieli koncepcyjnie akcje na trzy kategorie: SSL Required Wszystkie żądania muszą być chronione przez SSL. Jeżeli ta akcja będzie wywołana bez użycia SSL, zostanie przekierowana do SSL. Akcje tego typu są specyfikowane przy użyciu metody klasowej ssl_required. SSL Allowed Dla tych akcji SSL jest dozwolony, ale nie wymagany. Akcje tego typu są specyfikowane przy użyciu metody klasowej ssl_allowed. SSL Prohibited W przypadku tych akcji szyfrowanie SSL nie jest dozwolone. Jeżeli akcja nie jest oznaczana za pomocą ssl_required ani ssl_allowed, żądanie SSL tej akcji będzie przekierowywane do adresu URL nieobsługującego SSL. W typowy dla Rails sposób metody definiujące wymagania SSL tworzą język deklaratywny. Definiują, jakie są wymagania, a nie jak je wymusić. Oznacza to, że kod jest bardzo czytelny: class OrderController < ApplicationController ssl_required :checkout, :payment ssl_allowed :cart end
Podobnie jak w przypadku wtyczki account_location, ssl_requirement jest aktywowany przez dołączenie modułu. Moduł SslRequirement zawiera całą logikę wymagań SSL: module SslRequirement def self.included(controller) controller.extend(ClassMethods) controller.before_filter(:ensure_proper_protocol) end module ClassMethods def ssl_required(*actions) write_inheritable_array(:ssl_required_actions, actions) end def ssl_allowed(*actions) write_inheritable_array(:ssl_allowed_actions, actions) end end protected def ssl_required? (self.class.read_inheritable_attribute(:ssl_required_actions) || []). include?(action_name.to_sym) end def ssl_allowed? (self.class.read_inheritable_attribute(:ssl_allowed_actions) || []). include?(action_name.to_sym) end private
Przykład wtyczk
|
91
def ensure_proper_protocol return true if ssl_allowed? if ssl_required? && !request.ssl? redirect_to "https://" + request.host + request.request_uri return false elsif request.ssl? && !ssl_required? redirect_to "http://" + request.host + request.request_uri return false end end end
Również w tym przypadku metoda SslRequirement.included jest uruchamiana po dołączeniu SslRequirement do klasy kontrolera. Metoda included realizuje dwie operacje. Po pierwsze, rozszerza kontroler za pomocą modułu SslRequirement::ClassMethods w celu dołączenia metod klasowych ssl_required oraz ssl_allowed. Idiom ten jest często stosowany w Ruby przy dołączaniu metod klasowych i jest wymagany, ponieważ metody dołączonego modułu nie stają się metodami klasowymi dołączonej klasy (inaczej mówiąc, ssl_required oraz ssl_allowed nie mogą być metodami modułowymi SslRequirement, ponieważ nie mogą być dodane jako metody klasowe kontrolera). Po drugie, metoda SslRequirement.included konfiguruje before_filter w kontrolerze w celu wymuszenia wymagalności SSL. W zależności od operacji zadeklarowanych przez metody klasowe, filtr ten przekierowuje do odpowiedniego adresu URL, http:// lub https://.
Uwierzytelnianie HTTP Ostatnią analizowaną wtyczką jest http_authentication, która pozwala na ochronę wybranych akcji aplikacji przy pomocy uwierzytelniania HTTP Basic (obecnie uwierzytelnianie Digest jest udostępnione, ale wewnętrzny mechanizm nie jest zaimplementowany). Wtyczka HTTP Authentication jest bardzo prosta — najczęściej wykorzystywanym interfejsem jest metoda authenticate_or_request_with_http_basic klasy ActionController, zwykle używana w before_filter w chronionych akcjach. Metoda ta wymaga jako parametrów obszaru uwierzytelniania oraz bloku procedury blokowania, która weryfikuje przesłane dane logowania. Jeżeli procedura logowania zwróci true, można kontynuować akcję. Jeżeli procedura logowania zwróci false, akcja jest blokowana i wysyłany jest kod statusu HTTP 401 Unauthorized wraz z instrukcją uwierzytelnienia (nagłówek WWW-Authenticate). W takim przypadku przeglądarka zwykle wyświetla użytkownikowi pola na nazwę użytkownika i hasło, pozwalając na trzy próby przed wyświetleniem strony Brak autoryzacji. Poniżej przedstawione jest typowe zastosowanie wtyczki HTTP Authentication: class PrivateController < ApplicationController before_filter :authenticate def secret render :text => "Hasło prawidłowe!" end protected def authenticate authenticate_or_request_with_http_basic do |username, password|
92
|
Rozdz ał 3. Wtyczk Ra ls
username == "bob" && password == "secret" end end end
Należy zauważyć, że w przeciwieństwie do dwóch wcześniej opisanych wtyczek nie musimy tu nic dołączać w PrivateController — metoda authenticate_or_request_with_http_basic jest od razu dostępna. Dzieje się tak, ponieważ wtyczka dodaje kilka metod do ActionCon ´troller::Base (po której dziedziczy ApplicationController). Jednym sposobem na dołączenie metod w taki sposób jest zastosowanie metody monkeypatching. Wtyczka może mieć metody wpisane bezpośrednio do ActionController::Base: class ActionController::Base def authenticate_or_request_with_http_basic(realm = "Application", &login_procedure) authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm) end def authenticate_with_http_basic(&login_procedure) HttpAuthentication::Basic.authenticate(self, &login_procedure) end def request_http_basic_authentication(realm = "Application") HttpAuthentication::Basic.authentication_request(self, realm) end end
Działa to w przypadku małych wtyczek, ale może okazać się zbyt niewygodne. Lepszym rozwiązaniem, wybranym w tej wtyczce, jest utworzenie modułu o takiej samej nazwie jak wtyczka (czasami z dołączonym imieniem programisty lub nazwą firmy, aby zmniejszyć szansę kolizji przestrzeni nazw). Poniżej zamieszczony jest skrócony kod metod klasowych wtyczki HTTP Authentication: module HttpAuthentication module Basic extend self module ControllerMethods def authenticate_or_request_with_http_basic(realm = "Application", &login_procedure) authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm) end def authenticate_with_http_basic(&login_procedure) HttpAuthentication::Basic.authenticate(self, &login_procedure) end def request_http_basic_authentication(realm = "Application") HttpAuthentication::Basic.authentication_request(self, realm) end end end end
Teraz metody te znajdują się wewnątrz HttpAuthentication::Basic::ControllerMethods. Prosta instrukcja w pliku init.rb wtyczki dodaje metody do ActionController::Base: ActionController::Base.send :include, HttpAuthentication::Basic::ControllerMethods
Przykład wtyczk
|
93
Testowanie wtyczek Podobnie jak w przypadku pozostałych części Rails, wtyczki mają bardzo zaawansowane funkcje testujące. Jednak testy wtyczek wymagają zwykle nieco więcej pracy niż standardowe testy Rails, ponieważ są one zaprojektowane do samodzielnego uruchamiania, poza środowiskiem Rails. Pisząc testy dla wtyczek, należy pamiętać o kilku sprawach: • W przeciwieństwie do inicjalizatora wtyczek Rails podczas uruchamiania testów ścieżki ła-
dowania nie są konfigurowane automatycznie, więc Dependencies nie będzie mógł załadować dla nas brakujących stałych. Konieczne jest ręczne skonfigurowanie ścieżek i dołączenie wszystkich części wtyczki, tak jak w zamieszczonym poniżej przykładzie pochodzącym z wtyczki HTTP Authentication: $LOAD_PATH << File.dirname(__FILE__) + '/../lib/' require 'http_authentication'
• Podobnie nie jest ładowany plik init.rb, więc należy skonfigurować wszystko, co jest potrzeb-
ne do testowania, tak jak na przykład dołączenie modułów wtyczki w klasie TestCase: class HttpBasicAuthenticationTest < Test::Unit::TestCase include HttpAuthentication::Basic # … end
• Zwykle konieczne jest odtworzenie (poprzez obiekty fikcyjne lub ich zręby) wszystkich
funkcji Rails wykorzystywanych w teście. W przypadku wtyczki HTTP Authentication ładowanie całego środowiska ActionController dla testów powodowałoby znaczny narzut. Testowane funkcje są bardzo proste i wymagają bardzo niewielkiej części Action ´Controller: def test_authentication_request authentication_request(@controller, "Megaglobalapp") assert_equal 'Basic realm="Megaglobalapp"', @controller.headers["WWW-Authenticate"] assert_equal :unauthorized, @controller.renders.first[:status] end
Aby obsłużyć ograniczony podzbiór funkcji klasy ActionController, metoda setup testu tworzy zrąb kontrolera: def setup @controller = Class.new do attr_accessor :headers, :renders def initialize @headers, @renders = {}, [] end def request Class.new do def env {'HTTP_AUTHORIZATION' => HttpAuthentication::Basic.encode_credentials("dhh", "secret") } end end.new end def render(options) self.renders << options end end.new end
94
|
Rozdz ał 3. Wtyczk Ra ls
Składnia Class.new do…end.new pozwala utworzyć instancję klasy anonimowej o zamieszczonej definicji. Bardziej rozbudowanym odpowiednikiem może być: class MyTestController # definicja klasy … end @controller = MyTestController.new
• Czasami zależności są na tyle skomplikowane, że faktycznie wymagają załadowania całej
biblioteki. Jest to przypadek wtyczki SSL Requirement, która faktycznie ładuje Action ´Controller i konfiguruje kontroler do testowania. Na początku kod ładuje Action ´Controller (wymaga to albo RUBYOPT= rubygems i odpowiedniej wersji gem Action ´Controller, albo ustawienia zmiennej środowiskowej ACTIONCONTROLLER_PATH na kopię źródeł ActionController): begin require 'action_controller' rescue LoadError if ENV['ACTIONCONTROLLER_PATH'].nil? abort <
Następnie kod testu ładuje test_process z ActionController, który uzyskuje dostęp do ActionController::TestRequest oraz ActionController::TestResponse. Po tej operacji wyłączane jest rejestrowane i wykonywane jest przeładowanie ścieżek. require 'action_controller/test_process' require 'test/unit' require "#{File.dirname(__FILE__)}/../lib/ssl_requirement" ActionController::Base.logger = nil ActionController::Routing::Routes.reload rescue nil
Na końcu mamy kontroler testów i przypadki testowe — są one w praktycznie tym samym formacie co testy funkcjonalne Rails, ponieważ wszystkie operacje konfiguracyjne wykonaliśmy ręcznie. class SslRequirementController < ActionController::Base include SslRequirement ssl_required :a, :b ssl_allowed :c # definicje akcji … end class SslRequirementTest < Test::Unit::TestCase def setup @controller = SslRequirementController.new @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new end # testy … end
Testowan e wtyczek
|
95
Wtyczka Deadlock Retry, kolejna standardowa wtyczka Rails, pozwalająca powtarzać zakleszczone transakcje bazy danych, jest dobrym przykładem wykorzystania zrębów klas modelu ActiveRecord5: class MockModel def self.transaction(*objects, &block) block.call end def self.logger @logger ||= Logger.new(nil) end include DeadlockRetry end
Pozwala to na przetestowanie prostych funkcji bez wprowadzania zależności od bazy danych. def test_error_if_limit_exceeded assert_raise(ActiveRecord::StatementInvalid) do MockModel.transaction { raise ActiveRecord::StatementInvalid, DEADLOCK_ERROR } end end
Testowanie zależności wtyczek od bazy danych Semantyka części wtyczek powoduje, że ich przetestowanie bez korzystania z bazy danych jest trudne. Ponieważ jednak testy mogą być uruchamiane w dowolnym środowisku, nie można polegać na określonym zainstalowanym systemie DBMS. Dodatkowo warto uniknąć konieczności tworzenia bazy testowej przez użytkownika w celu przetestowania wtyczki. Scott Barron wpadł na sprytne rozwiązanie, które wykorzystuje jego wtyczkę acts_as_state_ 6 ´machine (wtyczka do przypisywania stanu obiektów modelu ActiveRecord, takich jak oczekujące, wysłane i zwrócone zamówienie). Rozwiązaniem jest umożliwienie użytkownikowi wykonania testu w dowolnym systemie DBMS oraz wykorzystania SQLite (który jest powszechnie instalowany), jeżeli żaden inny system nie zostanie wybrany. Aby skorzystać z tego mechanizmu, zbiór odpowiednich obiektów modelu oraz odpowiednie obiekty osprzętu są dołączane do katalogu test/fixtures wtyczki. Wtyczka zawiera również schemat bazy danych obsługujący modele (schema.rb) oraz kilka instrukcji w test_helper.rb, które ładują osprzęt do bazy danych. Pełna struktura katalogów testu jest przedstawiona na rysunku 3.2. Pierwszym elementem układanki jest plik database.yml, który zawiera nie tylko bloki konfiguracji dla standardowych systemów DBMS, ale również dla SQLite oraz SQLite3, które zapisują bazę danych w pliku lokalnym:
5
Nazwałem to zrębem, a nie obiektem fikcyjnym, ale niektórzy nie potrafią wskazać różnicy. Zrąb z reguły jest „prosty” i nie zawiera logiki związanej z testami — służy tylko do ograniczenia zewnętrznych zależności. Obiekt fikcyjny jest znacznie sprytniejszy i zna środowisko testowe. Może on śledzić własny stan lub wiedzieć, czy jest „prawidłowy” w przypadku testowym go wykorzystującym.
Rysunek 3.2. Struktura katalogu testowanej wtyczki sqlite: :adapter: sqlite :dbfile: state_machine.sqlite.db sqlite3: :adapter: sqlite3 :dbfile: state_machine.sqlite3.db # (postgresql i mysql pominięte)
Pliki schematu, osprzęt i modele same się komentują — są to odpowiednio plik schematu Ruby, osprzęt YAML oraz klasy modelu ActiveRecord. Właściwe operacje znajdują się w pliku test_helper.rb, który łączy wszystkie elementy. Operacje pomocnicze testu na początku konfigurują ścieżki ładowania Rails i ładują ActiveRecord. Następnie ładowany jest plik database.yml i ActiveRecord jest podłączany do bazy danych (domyślnie do SQLite): config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml')) ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log") ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'sqlite'])
Następnie do bazy danych jest ładowany schemat: load(File.dirname(__FILE__) + "/schema.rb") if File.exist?(File.dirname(__FILE__) + "/schema.rb")
Na koniec ścieżka obiektów obiektu jest ustawiana na ścieżkę z TestCase i dodawana jest ścieżka ładowania, dzięki czemu rozpoznawane są modele w tym katalogu: Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/" $LOAD_PATH.unshift(Test::Unit::TestCase.fixture_path)
Teraz test (acts_as_state_machine_test.rb) może odwoływać się do klas ActiveRecord i ich danych osprzętu identycznie jak do zwykłych testów Rails.
Propozycje dalszych lektur Geoffrey Grosenbach napisał dwuczęściowy artykuł na temat wtyczek Rails, zawierający nieco informacji o pisaniu wtyczek. Te dwie części są dostępne na następujących stronach: http://nubyonrails.com/articles/the-complete-guide-to-rails-plugins-part-i http://nubyonrails.com/articles/the-complete-guide-to-rails-plugins-part-ii
Propozycje dalszych lektur
|
97
98
|
Rozdz ał 3. Wtyczk Ra ls
ROZDZIAŁ 4.
Bazy danych
Wszystkie nietrywialne abstrakcje są do pewnego stopnia dziurawe. Joel Spolsky
Dla wielu programistów Rails zaczyna się od bazy danych. Jedną z najbardziej interesujących funkcji Rails jest ActiveRecord, warstwa odwzorowania obiektowo relacyjnego (ORM). ActiveRecord tak świetnie realizuje zadanie ukrywania szczegółów SQL przed programistami, że niemal wydaje się to magią. Jednak, jak mówi Joel Spolsky, każda abstrakcja jest dziurawa. Nie istnieje doskonale przezroczysty system ORM i prawdopodobnie nigdy taki nie powstanie — z powodu fundamentalnie innej natury modelu obiektowego i relacyjnego. Z tego powodu nie należy ignorować użytej bazy danych.
Systemy zarządzania bazą danych Społeczność Rails przez lata tworzyła systemy na bazie systemu zarządzania bazą danych MySQL (DBMS1). Jednak nadal istnieje wiele błędnych wyobrażeń na temat systemów DBMS, szczególnie w przypadku ich użycia w Rails. Pomimo tego, że MySQL ma ugruntowaną pozycję, nie jest jedyną opcją. W ostatnich latach wsparcie dla innych baz danych znacznie się poprawiło. Zalecamy uważne czytanie tego rozdziału i dokładne rozważenie wszystkich kryteriów przed podjęciem decyzji dotyczącej wyboru systemu DBMS. Rails obsługuje wiele systemów baz danych — w czasie, gdy powstawała ta książka, dostępne były: DB2, Firebird, FrontBase, MySQL, OpenBase, Oracle, PostgreSQL, SQLite, Microsoft SQL Server oraz Sybase. Należy zapoznać się z dokumentacją RDoc dla adaptera połączenia, zwracając szczególną uwagę na problemy specyficzne dla używanego systemu DBMS, ponieważ niektóre funkcje, takie jak migracje, są obsługiwane tylko dla kilku adapterów połączenia.
1
Nieformalnie systemy DBMS są nazywane „bazami danych”. Zgodnie ze stosowanymi w przemyśle praktykami, w książce tej „system zarządzania bazą danych” odnosi się do pakietu oprogramowania lub jego instalacji, natomiast „baza danych” to zarządzany zbiór danych.
99
PostgreSQL Zaczniemy od przedstawienia PostgreSQL2, ponieważ jest to moja ulubiona platforma. Jest to jeden z najbardziej zaawansowanych dostępnych obecnie systemów baz danych open source. System ten ma długą historię, rozpoczynającą się we wczesnych latach osiemdziesiątych ubiegłego wieku, gdy na Uniwersytecie Kalifornijskim w Berkeley prowadzony był projekt Ingres. W przeciwieństwie do MySQL, PostgreSQL od długiego czasu obsługuje zaawansowane funkcje, takie jak wyzwalacze, procedury składowane, własne typy danych i transakcje. Obsługa współbieżności w PostgreSQL jest znacznie bardziej dojrzała niż w MySQL. Postgres obsługuje wielowersyjną kontrolę współbieżności (MVCC), która jest nawet bardziej zaawansowana niż blokowanie na poziomie wiersza. MVCC pozwala izolować transakcje, korzystać ze znaczników czasu do nadania każdej współbieżnej transakcji własnej migawki zbioru danych. W przypadku poziomu izolacji Serializable zapobiega to takim problemom, jak niespójne, niepowtarzalne lub fantomowe odczyty3 danych. Więcej informacji na temat MVCC znajduje się w ramce „Wielowersyjna kontrola współbieżności”. Jedną z zalet PostgreSQL, jaka może być szczególne ważna w środowisku korporacyjnym, jest podobieństwo do takich systemów, jak Oracle, MS SQL Server lub DB2. Choć Postgres nie jest w żadnym wypadku klonem lub emulacją komercyjnych baz danych, to z całą pewnością jest przyjazny dla programistów i administratorów mających doświadczenia z jedną z komercyjnych baz danych. Dodatkowo łatwiej jest wykonać migrację z Postgres do (na przykład) Oracle niż z MySQL do Oracle. PostgreSQL ma niestety reputację powolnego systemu. Powodem tego jest domyślna konfiguracja, zoptymalizowana dla niewielkich komputerów. Dlatego system będzie działał w dosyć spójny sposób na serwerze mającym tylko 64 MB pamięci RAM, jak również na takim, który ma zainstalowane 64 GB. Podobnie jak każda baza danych, Postgres może być strojony do swoich zadań. Oficjalna dokumentacja, dostępna pod adresem http://www.postgresql.org/docs, zawiera wiele doskonałych informacji na temat strojenia wydajności. Jedną z wad stosowania PostgreSQL jest znacznie mniejsza społeczność. Znacznie więcej programistów, szczególnie w świecie Rails, korzysta z MySQL. Istnieje znacznie więcej przetestowanych rozwiązań zbudowanych na podstawie MySQL niż na PostgreSQL. Firma produkująca MySQL, MySQL AB, zapewnia komercyjne wsparcie swojego produktu. Nie istnieje analogiczna centralna struktura wspierająca Postgres, ponieważ nie ma jednej firmy produkującej PostgreSQL, jednak istnieje kilka firm, które specjalizują się w konsultingu związanym z Postgres i zapewniają użytkownikom wsparcie.
2
Nazwa powinna być wymawiana jako „post-gres-Q-L”; zwykle występuje w wariancie „Postgres”. Pretenduje ona do najmniej intuicyjnej nazwy w dzisiejszym przemyśle komputerowym. Ma ona swoje korzenie w starym poprzedniku PostgreSQL o nazwie „Postgres”, który nie obsługiwał SQL.
3
Szczegółowy opis obsługi współbieżności przez Postgres, wraz z podsumowaniem potencjalnych problemów oraz ze sposobami ich obsługi przez Postgres, znajduje się w dokumentacji dostępnej pod adresem http://www.postgresql.org/docs/8.2/interactive/transaction-iso.html.
100
|
Rozdz ał 4. Bazy danych
Wielowersyjna kontrola współbieżności Wielowersyjna kontrola współbieżności (MVCC) jest jednym z najbardziej zaawansowanych sposobów zapewnienia izolacji pomiędzy równoległymi transakcjami w bazie danych. MVCC udostępnia każdej transakcji migawkę wykorzystywanych danych w takiej postaci, w jakiej były na początku transakcji. Transakcje są wykonywane na danych i są rejestrowane z użyciem znaczników czasu. W trakcie zatwierdzania transakcji system DBMS kontroluje dziennik, aby sprawdzić, czy nie występują konflikty z innymi transakcjami; jeżeli transakcja może być wykonana bez problemów, jest nakładana na bazę danych w sposób atomowy. Alternatywą dla MVCC jest blokowanie rekordów, które jest wykorzystywane w silniku zapisu InnoDB z MySQL. Blokowanie wierszy powoduje zablokowanie wierszy zmienianych w czasie transakcji (w przeciwieństwie do blokowania tabel, które jest znacznie mniej precyzyjne). Podstawową przewagą MVCC nad blokowaniem wierszy jest brak blokowania operacji odczytu w przypadku MVCC. Ponieważ wszystkie transakcje aktualizacji są stosowane automatycznie, baza danych jest stale spójna. Oczekujące transakcje są przechowywane w postaci dzienników obok bazy danych i są zapisywane w czasie zatwierdzania, zamiast zapisywania zmian w bazie danych w czasie trwania transakcji. Najważniejszą konsekwencją tego sposobu zapisu jest to, że nigdy nie blokuje odczytów, ponieważ odczyty są realizowane w bazie danych, która jest stale spójna. Należy jednak pamiętać, że izolacja współbieżnych transakcji zwykle obniża wydajność. MVCC wykorzystuje więcej przestrzeni dyskowej niż blokowanie, ponieważ konieczne jest przechowywanie migawek dla każdej otwartej transakcji. Pomimo tego, że MVCC nigdy nie blokuje odczytów, DBMS może wycofać transakcję powodującą konflikt.
MySQL System zarządzania bazą danych MySQL jest kontrowersyjny. Niektórzy uważają go za zabawkę, natomiast inni za dobrą podstawę dla aplikacji WWW. Pomimo tego MySQL jest najczęściej wykorzystywanym systemem DBMS w aplikacjach Rails i został znacznie usprawniony od wersji 3. do 5. Częścią filozofii skalowania w Rails jest podejście „nic nie współdzielimy” — każdy serwer aplikacji powinien być w stanie pracować samodzielnie. Dzięki temu można podłączyć pięć takich serwerów do wyrównywacza obciążenia i nie ma znaczenia, który serwer będzie obsługiwał żądanie. Jednak wąskim gardłem staje się baza danych. Poważnym założeniem architektury bez współdzielenia jest to, że serwery aplikacji korzystają z tej samej bazy danych. Jeżeli użyjemy bazy danych, która nie ma świetnej obsługi współbieżności, napotkamy na problemy. Stare wersje MySQL miały z tym dosyć spore kłopoty, które dotyczyły spójności danych oraz ograniczeń. Problemy te nie były tak duże w stosunku do innych usterek, ponieważ programiści MySQL zdawali się przekonywać, że „nie będziemy tego potrzebować”. Do dziś w domyślnym silniku zapisu (MyISAM) nie są obsługiwane transakcje. W wersjach wcześniejszych niż 5.0 było wiele usterek powodujących, że błędne dane były odrzucane bez zgłaszania jakiegokolwiek błędu. Jednak trzeba uczciwie stwierdzić, że w nowych wersjach MySQL rozwiązano wiele problemów. Nadal zalecam korzystanie z PostgreSQL w zastosowaniach, w których szybkość nie jest podstawowym kryterium, ponieważ znacznie dłużej posiada on funkcje klasy korporacyjnej. Jeżeli korzystamy z MySQL, warto pamiętać o następujących zaleceniach:
Systemy zarządzan a bazą danych
|
101
• Należy używać wersji 5.0 lub nowszej. Wiele z problemów występujących w poprzed-
nich wersjach zostało usuniętych lub usprawnionych w wersjach 5.0 i nowszych.
• Jeżeli spójność danych lub współbieżność ma znaczenie, należy koniecznie korzystać
z InnoDB. MyISAM, domyślny silnik zapisu w większości instalacji MySQL, nie obsługuje wielu funkcji, które w większości systemów DBMS są traktowane jako kluczowe — ograniczeń kluczy obcych, blokowania rekordów oraz transakcji. W większości środowisk biznesowych braku tych funkcji nie można negocjować. InnoDB, jako silnik zapisu z mechanizmem dziennika, jest znacznie bardziej odporny na awarie. Rails przemawia za tym wyborem, ponieważ w czasie tworzenia tabel jest domyślnie używana maszyna InnoDB.
• Niestety, maszyna InnoDB jest znacznie wolniejsza niż MyISAM, a dodatkowo tabele są
kilka razy większe. MyISAM jest zwykle szybsza, gdy liczba odczytów znacznie przekracza liczbę zapisów, natomiast InnoDB jest zwykle szybsza, gdy odczyty i zapisy są zrównoważone. Wszystko zależy od wymagań określonej aplikacji. Zawsze należy wykonać testy na rzeczywistych danych z użyciem odpowiedniej liczby wykonywanych zapytań, w realistycznym środowisku.
• Istnieje kilka wyjątków od tych wskazówek — MyISAM może być lepszym wyborem
w przypadku konieczności użycia indeksowania pełnotekstowego (które obecnie jest obsługiwane tylko w tabelach MyISAM). Dodatkowo, jeżeli podstawowym zmartwieniem jest szybkość odczytów i zapisów, również może pomóc zastosowanie MyISAM. Na przykład z tabel MyISAM może korzystać serwer rejestrowania do analizy sieci WWW — chcemy, aby dzienniki były zapisywane najszybciej, jak jest to możliwe, a odczyty są wykonywane znacznie rzadziej niż zapisy.
• Ustawić tryb SQL na TRADITIONAL. Można to wykonać za pomocą następującego polecenia: SET GLOBAL sql_mode='TRADITIONAL';
Powoduje to, że MySQL staje się nieco bardziej dokładny, zgłaszając błędy nieprawidłowych danych, zamiast je po cichu odrzucać. W niektórych sytuacjach MySQL ma przewagę nad PostgreSQL. MySQL generalnie jest szybszy. W przypadku wielu aplikacji WWW szybkość wykonywania zapytań jest ważnym czynnikiem. MySQL ma również bardziej stabilne, sprawdzone opcje replikacji i klastrowania. MySQL również nieco lepiej obsługuje dane binarne zapisywane w bazie danych (przedstawimy to dokładniej w dalszej części rozdziału). W przypadku wielu aplikacji MySQL może być zdecydowanym zwycięzcą.
SQLite SQLite to niewielka baza danych, która jest świetnie się nadaje do małych projektów. Choć nie obsługuje wielu przydatnych funkcji, jest doskonałym wyborem w przypadku projektów, które się zbytnio nie rozrosną. Obsługuje ona transakcje ACID4. SQLite jest biblioteką dołączaną do naszego programu — nie istnieje osobny proces serwera. Kod biblioteki znajduje się w przestrzeni adresowej procesu aplikacji i realizuje dostęp do pliku bazy danych. SQLite nie zapewnia współbieżności, ponieważ nie istnieje proces serwera wymuszający zachowanie własności ACID. Z tego powodu wykorzystuje blokowanie na poziomie plików — 4
Nazwa transakcji ACID pochodzi od określeń Atomic, Consistent, Isolated, Durable („atomowa, spójna, izolowana, trwała”), które to określenia definiują niezbędne właściwości transakcji bazy danych. Pełne objaśnienie można znaleźć w Wikipedii: http://pl.wikipedia.org/wiki/ACID.
102
|
Rozdz ał 4. Bazy danych
w czasie wykonywania transakcji cały plik bazy danych jest blokowany na poziomie systemu plików. W przypadku małych aplikacji doskonale się to sprawdza. Jest to dobry zamiennik dla danych przechowywanych w płaskich plikach, ponieważ obsługuje większość standardu SQL92 i w przypadku wzrostu potrzeb można łatwo przemigrować dane na większy system DBMS.
Microsoft SQL Server Pomimo tego, że Rails wyrastał w świecie Linuksa i Uniksa, zorganizowała się również społeczność zapewniająca obsługę platformy Windows. W Rails nie tylko obsługiwane są połączenia do serwera bazy danych Microsoft SQL Server, ale również zapewnione jest podłączanie się do SQL Server z systemów pracujących w systemie Linux, z wykorzystaniem biblioteki FreeTDS5. W przypadku klienta Windows standardową metodą jest użycie Ruby-DBI (niezależny od bazy danych adapter Ruby) oraz ADO. Konfiguracja taka wygląda następująco: development: adapter: sqlserver host: server_name database: my_db username: user password: pass
Konfiguracja może być różna w zależności od wersji SQL Server oraz zainstalowanych bibliotek ADO. Po utworzeniu konfiguracji bazy danych można manipulować danymi przy pomocy standardowych metod API ActiveRecord.
Oracle Rails obsługuje Oracle w wersjach 8i, 9i oraz 10g za pomocą biblioteki ruby-oci86, która wspiera API OCI8. Obsługiwane jest oprogramowanie klienckie systemów Windows, Linux oraz OS X. Konfiguracja połączenia jest zupełnie standardowa i używa oci jako nazwy adaptera. Jednak biblioteki klienckie Oracle nadal odwzorowują nazwy usług sieciowych na specyfikacje połączenia, więc parametr host zawiera nazwę usługi, a nie nazwę fizycznego hosta: development: adapter: oci host: ORCL username: user password: pass
W zamieszczonej powyżej konfiguracji ORCL odnosi się do sekcji w pliku TNSNAMES.ORA, który wygląda podobnie do zamieszczonego poniżej: ORCL = (DESCRIPTION = (ADDRESS_LIST = (ADDRESS = (PROTOCOL = TCP)(HOST = srv)(PORT = 1521)) ) ... )
5
Opis realizacji tego połączenia znajduje się na stronie http://wiki.rubyonrails.org/rails/pages/HowtoConnectToMicrosoft ´SQLServerFromRailsOnLinux, natomiast biblioteka FreeTDS jest dostępna pod adresem http://www.freetds.org.
6
http://rubyforge.org/projects/ruby-oci8.
Systemy zarządzan a bazą danych
|
103
Można również zapisać specyfikacje połączenia w jednym wierszu w pliku konfiguracyjnym Rails: development: dapter: oci host: (DESCRIPTION = (ADDRESS_LIST = (...))) username: user password: pass
Najtrudniejszym zadaniem jest konfiguracja połączenia. Po podłączeniu bazy danych Rails obsługuje połączenia Oracle w identyczny sposób, jak połączenia z innymi systemami RDBMS. Procedury składowane oraz inne elementy mające składnię specyficzną dla Oracle korzystają ze standardowych metod udostępniających interfejs SQL, takich jak ActiveRecord::Base. ´find_by_sql.
Duże obiekty (binarne) Wcześniej lub później wiele aplikacji WWW musi rozwiązać problem danych LOB (dużych obiektów binarnych). Dane LOB mogą być niewielkie, ale zwykle w porównaniu z pozostałymi przechowywanymi atrybutami są duże (od dziesiątków kilobajtów do setek gigabajtów). W przypadku danych LOB aplikacja nie „wie” nic na temat budowy wewnętrznej tych danych. Podstawowym przykładem są dane obrazów; aplikacja WWW zwykle nie musi mieć informacji, że dane w pliku JPEG reprezentują wizerunek użytkownika, ponieważ może ten plik wysyłać do klienta, zamieniać go i usuwać w razie potrzeby. Metody przechowywania danych LOB są inne dla obiektów CLOB (duże obiekty znakowe), zawierających dane tekstowe, oraz obiektów BLOB (duże obiekty binarne), przeznaczonych na pozostałe dane. Niektóre systemy DBMS mają różne typy danych dla tych dwóch rodzajów obiektów. Typy CLOB często mogą być indeksowane, sortowane i przeszukiwane, w przeciwieństwie do danych BLOB.
Przechowywanie w bazie danych Administratorzy baz danych często preferują przechowywanie dużych obiektów w bazie danych. Teoretycznie podejście takie jest najprostszym i czystym rozwiązaniem. Posiada kilka poważnych zalet: • Wszystkie dane aplikacji znajdują się w tym samym miejscu — w bazie danych. Istnieje
tylko jeden interfejs danych i jeden program odpowiedzialny za zarządzanie danymi we wszystkich postaciach. • Mamy znacznie większą elastyczność kontroli dostępu, co naprawdę pomaga w przy-
padku dużych projektów. Przy użyciu uprawnień DBMS, różne uprawnienia mogą być nadawane do różnych tabel w tej samej bazie danych. • Dane binarne nie są związane z fizyczną ścieżką — w przypadku użycia przechowywa-
nia w systemie plików konieczne jest aktualizowanie ścieżek w bazie danych w przypadku przeniesienia lokalizacji przechowywania. Istnieje jednak wiele uwag praktycznych, związanych z implementacją dużych obiektów w używanym systemie DBMS. 104
|
Rozdz ał 4. Bazy danych
PostgreSQL PostgreSQL ma dosyć dziwną obsługę danych binarnych. Istnieją dwa sposoby przechowywania danych binarnych w bazie PostgreSQL — typ danych BYTEA oraz duże obiekty. Typ BYTEA7 jest najbliższym typem w Postgres względem typu BLOB z innych baz — wyłącznie sekwencja bajtów — ale jest naprawdę okropny dla obszernych danych binarnych. Protokół przesyłania danych BYTEA do i z bazy danych wymaga oznaczania wszystkich znaków niedrukowalnych, więc jeden bajt null będzie zakodowany jako ciąg ASCII \000 (4 bajty). Nie trzeba wspominać, że powoduje to niepotrzebny rozrost danych. Dodatkowo niemożliwe jest utworzenie strumienia danych z bazy danych do przeglądarki WWW bez przechodzenia przez filtr. Pobranie pliku o wielkości 2 MB z bazy danych zwykle wymaga przesłania około 6 MB przefiltrowanych danych8. Filtr ten przesyła wszystkie dane poprzez teksty Ruby, co powoduje znaczny wzrost użycia pamięci. Lepszym rozwiązaniem jest użycie biblioteki C postgres do filtrowania, ale wymaga to dużo pracy i nadal nie jest optymalne. Do 1 GB danych można przechowywać dane w kolumnie BYTEA. Drugą możliwością jest zastosowanie dużych obiektów. Obsługa dużych obiektów w PostgreSQL działa wystarczająco dobrze, ale jest nieco niewygodna. Pliki są przechowywane w katalogu systemowym pg_largeobject na małych stronach9. W tabeli korzystającej z obiektu przechowywany jest wskaźnik do OID (ID obiektu) pliku. W dużym obiekcie można przechowywać do 2 GB danych. Metoda ta jest szybka, ma dobre API, ale również kilka wad. Nie istnieje kontrola obiektu na poziomie tabeli lub obiektu — katalog pg_largeobject jest globalny dla bazy danych i dostępny dla każdego, kto ma uprawnienia do podłączenia się do bazy danych. Mechanizm dużych obiektów jest również nieco mniej zalecany na rzecz przechowywania w tabeli, ponieważ techniki zapisu TOAST pozwalają na zapisanie bezpośrednio w tabeli wartości o wielkości do 1 GB. W przypadku korzystania z PostgreSQL zalecam korzystanie z przechowywania wszystkich obiektów binarnych w systemie plików. Choć baza danych może być właściwszym miejscem dla tego typu danych, to jednak mechanizmy zapisu nie działają wystarczająco dobrze. Jeżeli konieczne jest użycie bazy danych, dosyć dobrze sprawdzają się duże obiekty. Należy unikać za wszelką cenę korzystania z BYTEA.
MySQL MySQL całkiem dobrze radzi sobie z danymi binarnymi. Kolumny o typach LOB (w tym typ TEXT) mogą przechowywać do 4 GB danych przy zastosowaniu typu LONGLOB. Faktyczna pojemność i wydajność zależy od użytego protokołu komunikacji, wielkości bufora oraz dostępnej pamięci. Zapis jest efektywny, ponieważ na początku zapisywana jest długość danych przy użyciu maksymalnie 4 bajtów, po czym zapisywane są same dane binarne. Jednak MySQL ma te same problemy z tworzeniem strumienia danych co PostgreSQL i dla aplikacji WWW tworzenie strumienia danych z bazy danych jest bardziej kłopotliwe niż z systemu plików.
7
Skrót od „tablica bajtów”.
8
Zakładając jednorodne dane binarne, zasady filtrowania BYTEA powodują średni wzrost wielkości o 1:2,9.
9
Rozmiar jest definiowany za pomocą LOBLKSIZE. Jego wartością domyślną jest 2 kB.
Duże ob ekty (b narne)
|
105
Oracle Oracle obsługuje typ danych BLOB dla obiektów o wielkości do 4 GB. Jest on obsługiwany przez dosyć zaawansowane API, które może być wykorzystywane bezpośrednio z Rails. Oracle posiada również typ BFILE, będący wskaźnikiem do pliku binarnego na dysku. Więcej informacji znajduje się w kolejnym punkcie, poświęconym zapisowi w systemie plików. Może to być wartościowe w niektórych sytuacjach.
Zapis w systemie plików W rzeczywistości najlepszą opcją jest korzystanie z systemu plików. Systemy plików są zoptymalizowane do obsługi dużych ilości danych binarnych i znakowych oraz realizują to szybko. Jądro systemu Linux posiada wywołanie systemowe sendfile(), które operuje na fizycznych plikach. Istnieją setki narzędzi, które można wykorzystać w czasie korzystania z plików fizycznych: • Przetwarzanie obrazu jest niewątpliwie najpopularniejszym zastosowaniem przechowy-
wania danych binarnych. Programy takie jak ImageMagick są znacznie łatwiejsze w użyciu w postaci polecenia działającego na plikach niż przy wykorzystaniu często sprawiających problemy bibliotek Ruby, takich jak RMagick. • Pliki fizyczne mogą być współdzielone poprzez NFS lub AFS, umieszczane na MogileFS
lub klastrowane w inny sposób. Osiągnięcie dużej wydajności lub wyrównania obciążenia w przypadku zapisywania dużych obiektów w bazie danych może być kłopotliwe. • Dowolne narzędzie operujące na plikach musi zostać zintegrowane lub w inny sposób
zmodyfikowane, aby działało we współpracy z bazą danych.
Wysyłanie danych za pomocą X-Sendfile Często zdarza się, że plik po przetworzeniu za pomocą Rails musimy wysłać do klienta w celu jego pobrania. Najczęściej spotykanym przykładem jest plik o kontrolowanym dostępie — przed wysłaniem pliku konieczna jest weryfikacja, czy zalogowany użytkownik ma odpowiedni poziom dostępu. Najprostszym sposobem jest zrealizowanie tych operacji za pomocą wywołań API send_file lub send_data, które tworzą strumień danych z serwera do klienta: class DataController < ApplicationController before_filter :check_authenticated def private_document file = File.find params[:id] send_file file.path if file end end
Jest to prosta metoda, ale powolna, jeżeli wysyłamy pliki statyczne. Rails odczytuje plik i bajt po bajcie tworzy strumień danych do klienta. Protokół X-Sendfile ułatwia i przyspiesza ten proces, ponieważ pozwala Rails na wykonanie przetwarzania, ale następnie przekazuje proces przekazywania pliku do serwera WWW (który może przekazać przetwarzanie do jądra systemu operacyjnego, jak wcześniej pokazaliśmy).
106
|
Rozdz ał 4. Bazy danych
Dlaczego system plików jest tak szybki? Najkrótszą odpowiedź jest taka, że serwery WWW są zoptymalizowane do wyrzucania plików binarnych przez gniazdo TCP. A najczęściej wykonywaną operacją na plikach binarnych jest ich wysłanie do sieci poprzez gniazdo TCP. Druga odpowiedź: sekretem tej wydajności w systemach Linux i BSD jest wywołanie jądra sendfile() (nie należy go mylić z X-Sendfile, które przedstawimy w dalszej części rozdziału). Funkcja sendfile() szybko kopiuje dane z deskryptora pliku (reprezentującego otwarty plik) do gniazda (połączonego z klientem). Jest to realizowane w trybie jądra, a nie użytkownika — cały proces jest obsługiwany przez system operacyjny. Serwer WWW nie musi się tym zajmować. Po wywołaniu sendfile() proces wygląda podobnie do przedstawionego na rysunku 4.1.
Rysunek 4.1. Udostępnianie pliku przy użyciu sendfile() W przypadku odczytu danych z bazy danych Rails jest zaangażowany w cały proces. Plik musi być przekazany (fragment po fragmencie) z bazy danych do Rail, gdzie tworzona jest odpowiedź, i całość (razem z plikiem) jest wysyłana do serwera WWW. Następnie serwer wysyła odpowiedź do klienta. Użycie sendfile() byłoby tu niemożliwe, ponieważ dane nie są zapisane w pliku. Dane muszą być buforowane w pamięci i cała operacja jest wykonywana w trybie użytkownika. Cały plik jest przetwarzany kilka razy przez kod systemu użytkownika, co jest znacznie bardziej skomplikowanym procesem, pokazanym na rysunku 4.2.
Rysunek 4.2. Udostępnianie pliku z bazy danych
Duże ob ekty (b narne)
|
107
Protokół X-Sendfile jest bardzo prostym standardem, wprowadzonym na początku w serwerze WWW Lighthttpd, który zamawia w serwerze wykonanie przesłania pliku z systemu plików do klienta zamiast odpowiedzi generowanej przez serwer aplikacji. Ponieważ serwer WWW jest zoptymalizowany pod kątem przesyłania plików do klienta, zwykle pozwala to na znaczną poprawę szybkości działania w stosunku do odczytywania pliku do pamięci i wysyłania go z Rails za pomocą wywołań API send_file lub send_data. Ponieważ serwer WWW wymaga dostępu do pliku w celu wysłania go do klienta, konieczne jest wykorzystanie zapisywania dużych obiektów w systemie plików. Dodatkowo wysyłane pliki muszą mieć tak ustawione uprawnienia, aby były dostępne dla serwera WWW. Pliki te powinny być jednak poza głównym katalogiem WWW, aby nie pozwolić na pobranie plików prywatnych poprzez zgadywanie nazwy. X-Sendfile korzysta z nagłówka HTTP X-Sendfile wskazującego na plik do wysłania przez serwer, jak również innych standardowych nagłówków HTTP. Typowa odpowiedź z użyciem X-Sendfile wygląda następująco: X-Sendfile: /home/rails/sample_application/private/secret_codes_69843.zip Content-Type: application/octet-stream Content-Disposition: attachment; file="secret_codes.zip" Content-Length: 654685
Jeśli serwer WWW jest prawidłowo skonfigurowany, zignoruje treść odpowiedzi i wyśle plik z dysku do klienta. W Rails można ustawić zawartość response.headers przez modyfikację tablicy response. ´headers: response.headers['X-Sendfile'] = file_path response.headers['Content-Type'] = 'application/octet-stream' response.headers['Content-Disposition'] = "attachment; file=\"#{file_name}\"" response.headers['Content-Length'] = File.size(file_path)
Konfiguracja serwera WWW Serwer WWW musi być prawidłowo skonfigurowany, aby rozpoznawał i przetwarzał nagłówki 10 X-Sendfile. Mongrel nie obsługuje X-Sendfile, ponieważ zakłada, że będzie on pośrednikiem dla serwera lepiej dającego sobie radę z udostępnianiem statycznej zawartości. Jeżeli korzystamy z Lighttpd, ma on wbudowaną obsługę X-Sendfile. W przypadku Lighttpd/ FastCGI, wystarczy włączyć opcję allow-x-send-file w konfiguracji serwera: fastcgi.server = ( ".fcgi" => ( "localhost" => ( ... "allow-x-send-file" => "enable", ... ) ) )
10
Szybki serwer obsługujący aplikacje napisane we frameworku Ruby on Rails. Jego instalacja wymaga Ruby w wersji 1.8.4. Instalacja wymaga wpisania polecenia gem install mongrel — przyp. red.
108
|
Rozdz ał 4. Bazy danych
Jeżeli korzystamy z Apache 2, sprawa się nieco komplikuje (choć nie bardzo). Należy zainstalować moduł mod_xsendfile11 w Apache. Istnieją dwie opcje konfiguracji, obie akceptujące wartości on i off, które pozwalają sterować działaniem X-Sendfile: XSendFile Określa, czy nagłówek X-Sendfile będzie przetwarzany. XsendFileAllowAbove Określa, czy za pomocą tego nagłówka można wysyłać pliki z katalogów powyżej ścieżki żądania. Z powodów bezpieczeństwa ma ona domyślnie wartość off. Obie z tych opcji konfiguracji mogą być użyte w dowolnym kontekście konfiguracji, aż do pliku .htaccess (w każdym katalogu). Najlepsze praktyki dyktują, że powinniśmy udostępniać XSendFile w możliwie najwęższym kontekście. Jeżeli mechanizm X-Sendfile jest niepotrzebnie włączony, stanowi zagrożenie bezpieczeństwa, ponieważ pozwala serwerowi aplikacji na wysłanie do klienta dowolnego pliku dostępnego dla serwera. Według mojej wiedzy obecnie nie można użyć X-Sendfile w Apache 1.3.
Udostępnianie plików statycznych Jedną z zalet przechowywania plików w systemie plików jest to, że jeżeli pliki danych nie muszą być chronione za pomocą kontroli dostępu lub w inny sposób nadzorowane dynamicznie, do udostępniania danych można wykorzystać serwer WWW. Przez wyeksportowanie ścieżki przechowywania za pomocą NFS (lub buforującego systemu plików, takiego jak AFS, co zapewnia zmniejszenie ruchu) można udostępniać pliki aplikacji za pomocą serwerów statycznych w sieci udostępniania treści. Pozwala to znacznie obniżyć obciążenie serwerów aplikacji i zapewnić bardziej skalowane rozwiązanie.
Zarządzanie przesyłaniem w Rails Większość aplikacji korzystających z dużych obiektów musi obsługiwać przesyłanie plików na serwer. Jest to kłopotliwe w każdym środowisku, ale Rails obsługuje za nas większość szczegółów, a przedstawione tu najlepsze praktyki przeprowadzą nas przez resztę zadań.
Wtyczka Attachment Jednym z najłatwiejszych sposobów obsługi przesyłania plików w Rails jest użycie jednej z popularnych wtyczek obsługujących to zadanie. Najczęściej wykorzystywana jest wtyczka Ricka Olsona, acts_as_attachment (http://svn.techno-weenie.net/projects/plugins/acts_as_attachment). Wielu programistów Rails zna ten interfejs i od dłuższego czasu jest to standardowy sposób obsługi przesłanych danych. Jednak występuje tu kilka czynników, które powodują, że wtyczka ta nie nadaje się do wielu aplikacji: • Jest związana z RMagick (więc jednocześnie z ImageMagick) do operacji przetwarzania
obrazu. Program ImageMagick jest bardzo często trudny do zainstalowania, przede wszystkim dlatego, że zależy on od wielu bibliotek przetwarzania różnych formatów obrazu. W czasie pisania acts_as_attachment ImageMagick był najlepszym rozwiązaniem. Obecnie 11
jednak dostępne jest znacznie lżejsze rozwiązanie alternatywne, ImageScience, bazujące na bibliotece FreeImage. • Całe dane załącznika muszą być wczytane do pamięci i skonwertowane do postaci ciągu
Ruby. Dla dużych plików jest to kosztowna operacja — Rails przekazuje aplikacji obiekt TempFile, który jest wciągany do String. Jeżeli wykorzystywany jest zapis w systemie
plików, to ciąg znaków jest zapisywany znów do pliku! • Nie istnieje obsługa alternatywnych metod przechowywania, takich jak S3 firmy Amazon.
Na szczęście istnieje rozwiązanie alternatywne. Rick zmodyfikował act_as_attachment, aby rozwiązać te problemy. Zmieniona biblioteka nosi nazwę attachment_fu i jest dostępna pod adresem http://svn.techno-weenie.net/projects/plugins/attachment_fu. Biblioteka attachment_fu obsługuje wszystkie opcje act_as_attachment i kilka nowych. Może wykorzystywać RMagick do przetwarzania, ale obsługuje również MiniMagick (lekka alternatywa dla RMagick, która nadal korzysta z ImageMagick) oraz ImageScience. Od razu po zainstalowaniu może zapisywać załączniki w bazie danych, systemie plików lub S3. Posiada świetną opcję do rozszerzeń — bardzo łatwo można napisać własny procesor lub procedury zapisu. Typowe zastosowanie attachment_fu wygląda następująco: class UserAvatar < ActiveRecord::Base belongs_to :user has_attachment :content_type => :image, :max_size => 100.kilobytes, :storage => :file_system, :resize_to => [100, 100] end
Attachment_fu jest niemal całkowicie zgodna wstecz z act_as_attachment. Wystarczy zmienić wywołanie metody act_as_attachment na has_attachment. Pełna dokumentacja API jest dostarczana wraz z wtyczką w postaci RDoc.
Własny mechanizm Wtyczki załączników mają wiele funkcji, ale nie mogą robić wszystkiego. Jeżeli zdecydujemy się na wykonanie własnych procedur przetwarzania przesłanych danych, należy wziąć pod uwagę następujące wskazówki: • Konieczna jest kontrola poprawności przesłanych danych. Co składa się na prawidłowy
przesłany plik? Czy występują ograniczenia dotyczące wielkości przesłanych danych (minimalna lub maksymalna wielkość)? Czy przesłany plik musi mieć określony typ MIME lub rozszerzenie? • Rails pozwala na przekazanie nam kilku różnych typów obiektów, w zależności od tego,
co zostało przesłane i jakiej jest wielkości. James Edward Gray II napisał artykuł12 na temat prawidłowego i efektywnego obsługiwania wszystkich przypadków.
• Należy upewnić się, że pliki mogą być prawidłowo powielane, jeżeli skojarzony z nimi re-
kord zostanie powielony (w przypadku systemu plików powinno to być po prostu wywołanie FileUtils.cp).
• Należy się upewnić, że po usunięciu rekordu usuwany jest również plik z dysku. Może to
być zrealizowane przy użyciu procedury wywołania zwrotnego after_destroy w modelu. W przypadku przechowywania w bazie danych bardziej efektywne może być użycie wyzwalacza lub reguły.
Postęp przesyłania Jedną z funkcji potrzebnych w wielu aplikacjach jest powiadamianie o postępie przesyłania — użytkownik widzi pasek postępu wskazujący, jaka część pliku została już przesłana. Jest to zaskakująco trudne do wykonania i zależne od serwera, ale istnieją narzędzia ułatwiające to zadanie. Dla uproszczenia ograniczymy nasz opis do serwera aplikacji Mongrel. Mongrel serializuje żądania Rails i w danym momencie jeden proces Mongrel może wykonywać jedno żądanie Rails. Jest to wymagane, ponieważ ActionController nie jest bezpieczny dla wątków. Jednak podgląd postępu przesyłania pliku wymaga zastosowania dwóch równoczesnych żądań — samego procesu przesyłania, jak również żądań AJAX kontrolujących postęp. Jak możemy to pogodzić? W serwerze Mongrel zastosowano bardzo konserwatywne podejście do blokowania; żądania są serializowane tylko w momencie wykonywania kodu kontrolera. W czasie przesyłania pliku Mongrel buforuje go w pamięci i w tym czasie pozwala na realizację innych żądań. Po zakończeniu przesyłania pliku Mongrel przetwarza to żądanie Rails, blokując się tylko na czas wykonywania kodu Rails. Moduł gem mongrel_upload_progress dołącza się do serwera Mongrel w celu zapewnienia zmiennej współdzielonej, która może być wykorzystywana przez wiele żądań do komunikacji stanu przesłania pliku. Zmienna ta jest dostępna dla Rails jako Mongrel::Uploads. Prosta akcja Rails (uruchamiana za pomocą AJAX) wywołuje metodę Mongrel::Uploads.check(upload_id) w celu kontroli stanu, a następnie aktualizuje klienta. Choć cały ten złożony mechanizm umożliwia wykorzystywanie tylko jednego procesu Mongrel, to większość aplikacji o średnim obciążeniu wymaga zastosowania wielu serwerów Mongrel. Wszystkie żądania Rails są nadal serializowane, więc liczba żądań przetwarzanych równolegle w Rails jest ograniczona do liczby procesów Mongrel. Przedstawione rozwiązanie z pamięcią współdzieloną nie działa jednak w przypadku zastosowania więcej niż jednego serwera Mongrel — każdy z nich jest osobnym procesem i nie mają one pamięci współdzielonej. Rozwiązaniem jest użycie DRb (Distributed Ruby). Do realizacji zadania udostępniania stanu przesyłania uruchamiany jest proces tła. Po otrzymaniu kolejnych bloków pliku każda procedura obsługi przesyłania powiadamia proces tła, za pomocą DRb, o swoim statusie. Rails może teraz odpytywać wspólny proces o status dowolnego pliku, niezależnie od tego, który serwer obsługuje proces przesyłania i żądania odczytu stanu. Moduł gem obsługujący postęp przesyłania może być zainstalowany za pomocą polecenia gem install mongrel_upload_progress. Przykładowa aplikacja Rails ilustrująca sposób uży-
cia tego modułu jest dostępna pod adresem http://svn.techno-weenie.net/projects/mongrel_upload_ ´progress. Oficjalna dokumentacja realizacji postępu przesyłania w Mongrel jest dostępna pod adresem http://mongrel.rubyforge.org/wiki/UploadProgress.
Duże ob ekty (b narne)
|
111
Zaawansowane funkcje baz danych Wśród programistów Rails zaawansowane funkcje baz danych są często kością niezgody. Niektórzy uważają, że ograniczenia, wyzwalacze i procedury są niezbędne, natomiast inni całkowicie je pomijają, argumentując, że inteligencja przynależy wyłącznie do aplikacji. Przychylam się do argumentu, że cała logika biznesowa należy do aplikacji — niemal niemożliwe jest wprowadzanie zmian do wymagań, gdy logika jest podzielona pomiędzy dwiema lokacjami. Uważam jednak, że ograniczenia, wyzwalacze, a nawet procedury przechowywane mają swoje zastosowania w aplikacjach korporacyjnych. W celu przedstawienia mojego punktu widzenia przeanalizujemy różnicę, która często jest związana z tą dyskusją — różnicę pomiędzy aplikacją a integracyjną bazą danych.
Aplikacyjne i integracyjne bazy danych Różnica pomiędzy aplikacyjnymi bazami danych a integracyjnymi bazami danych jest szczególnie dobrze przedstawiona przez Martina Fowlera13. Podstawową różnicą jest to, że integracyjna baza danych jest współdzielona pomiędzy różnymi aplikacjami, a aplikacyjna baza danych „należy” do wykorzystującej ją aplikacji. W tym sensie „aplikacja” może oznaczać jeden lub kilka programów wewnątrz granic aplikacji (o tym samym logicznie zastosowaniu). Zwykle różnica ta określa sposób organizacji schematu; w Rails integracyjne bazy danych są często nazywane bazami danych o „zastanych schematach”. W aplikacyjnych bazach danych integracja może być wykonywana za pomocą komunikatów w warstwie aplikacji, a nie w warstwie bazy danych. Rails definiuje sposób, w jaki powinien być zorganizowany schemat bazy danych — klucz główny powinien być identyfikatorem, klucze obce powinny mieć budowę element_id, a nazwy tabel powinny być w liczbie mnogiej. Nie jest to fanatyzm — w Rails zostały wybrane sensowne wartości domyślne dla realizacji paradygmatu „konwencja przed konfiguracją”, aby mógł on działać w sposób efektywny. Zmiana niemal każdej z tych konwencji przebiega w sposób względnie bezproblemowy. Rails świetnie współpracuje z integracyjnymi bazami danych. Wielu programistów Rails neguje potrzebę stosowania integracyjnych baz danych — utrzymują oni, że integracja powinna być realizowana w warstwie aplikacji. Niektórzy idą krok dalej i uważają, że kontrola spójności danych należy wyłącznie do aplikacji, dzięki czemu cała logika biznesowa znajduje się w tym samym miejscu. Choć może to być idealne rozwiązanie, to w rzeczywistym świecie nie zawsze działa ono tak dobrze. Nawet jeżeli integracja jest realizowana na poziomie aplikacji, nadal mamy mnóstwo ważnych powodów do korzystania z ograniczeń bazy danych. Dodatkowo większość baz danych w środowiskach korporacyjnych ma tendencję stawania się z czasem bazami integracyjnymi. Bazy danych, które są przydatne do jednego celu, są często odpowiednie do innych zastosowań. Czasami mamy bazę danych, która nie jest pod naszą kontrolą, i chcemy skorzystać z jej danych bez wykonywania pełnego cyklu ETL (pobieranie, transformacja, ładowanie). Nawet uruchomienie małego skryptu na naszej bazie danych bez użycia modelu ActiveRecord lub ręczne utrzymywanie bazy danych za pomocą klientów konsolowych, 13
takich jak mysql lub psql, oznacza, że czasami odwołujemy się do naszej bazy poza modelem domeny. Jeżeli kontrola poprawności w domenie jest jedynym sposobem zapewnienia spójności danych, może to prowadzić do problemów.
Ograniczenia Ograniczenia na poziomie bazy danych pozwalają jawnie zdefiniować założenia na temat danych używane niejawnie w aplikacji. Istnieją dwa typy ograniczeń, które nie powinny być ze sobą mylone: Logika biznesowa „Kierownik nie może mieć więcej niż pięciu podwładnych”. Najważniejszą charakterystyką ograniczeń logiki biznesowej jest to, że mogą się one zmieniać w czasie wykorzystywania bazy danych. Ograniczenia logiki biznesowej nie powinny znajdować się w bazie danych, co dostarcza bardzo dobrego argumentu przeciwnikom. Spójność „Numer PESEL musi składać się z dokładnie jedenastu cyfr”. Ograniczenia spójności definiują naturę reprezentowanych danych. Oczywiście „natura danych” to nieco niejasna koncepcja, której znaczenie może różnić się pomiędzy bazami. Ograniczenia spójności muszą znajdować się w bazie danych, przynajmniej jako ostatni poziom kontroli poprawności. Tak jak w innych obszarach modelowania danych, istnieją szare obszary. Przykładem może być stwierdzenie: „wynagrodzenie pracownika musi być liczbą dodatnią”, które może być przyporządkowane do obu kategorii14. Zaletą ograniczeń jest ograniczenie dziedziny możliwych wyników generowanych w bazie danych. Gdy wiemy, że baza danych sklepu internetowego nigdy nie zwróci ujemnej ceny produktu, można sumować ceny pozycji zamówienia bez obawiania się o nieprawidłowe ceny. Podstawowa zasada brzmi: baza danych nie powinna wymuszać logiki biznesowej, ale powinna wymuszać spójność i integralność; jednak ten podział jest rożnie przesuwany w różnych aplikacjach. Pomimo różnych opinii na temat ograniczeń kontrolnych jeden z typów ograniczeń nie podlega dyskusji — ograniczenia kluczy obcych. Jeżeli wymagana jest relacja klucza obcego, niezwiązane rekordy są semantycznie bez znaczenia i nie można dopuszczać do ich pojawienia się. Formalizacja tego połączenia ma głęboki sens praktyczny. Jedyny skuteczny sposób na zapewnienie spójności bazy danych przez lata zbierania danych (jak to bazy danych mają w zwyczaju) to zadeklarowanie odpowiednich ograniczeń na danych. Jeżeli nie możemy zagwarantować, że każda aplikacja lub osoba korzystająca z bazy danych będzie realizowała to za każdym razem poprzez model domeny (z wszystkimi umieszczonymi w nim procedurami kontrolnymi), to jedynym sensownym rozwiązaniem jest potraktowanie bazy jako integracyjnej. Istnieje jednak premia za stosowanie ograniczeń — zazwyczaj, im więcej ograniczeń jest zdefiniowanych w bazie danych, tym lepszy plan zapytania może wykonać optymalizator.
14
Prawdopodobnie pozostawiłbym je na poziomie aplikacji, ponieważ zawiera regułę biznesową mówiącą, że żaden z pracowników nie ma wynagrodzenia równego zeru. Jednak stwierdzenie „wynagrodzenie pracownika nie może być liczbą ujemną” najprawdopodobniej trafiłoby do ograniczeń spójności, ponieważ niemal niepojęte jest, że „płacimy” pracownikowi ujemne wynagrodzenie.
Zaawansowane funkcje baz danych
|
113
Najczęściej zgłaszaną uwagą na temat ograniczeń bazy danych jest to, że wymagane jest umieszczanie informacji semantycznych w dwóch miejscach — w bazie danych oraz kodzie aplikacji (ponieważ zwykle chcemy przechwycić nieprawidłowe dane w kodzie kontroli poprawności aplikacji, przed wykonaniem wstawienia ich do bazy danych, nawet jeżeli baza danych przechwyciłaby błąd). Biblioteka DrySQL15 pozwala na uniknięcie tego powielania. Odczytuje ona relacje schematu oraz zasady kontroli poprawności z typów i ograniczeń bazy danych, więc nie musimy ich definiować w aplikacji. DrySQL współdziała z wszystkimi głównymi systemami DBMS — PostgreSQL 8 i nowsze, MySQL 5 i nowsze, SQL Server, Oracle oraz DB2. Po zainstalowaniu DrySQL można po prostu dołączyć bibliotekę do pliku konfiguracyjnego środowiska: require 'drysql'
Następnie należy poinformować ActiveRecord o odwzorowaniu pomiędzy tabelami i klasami modelu (nie jest to konieczne, jeżeli tabele mają nazwy zgodne z wartościami domyślnymi): class Client set_table_name "customers" end
Jeżeli tabela nosiłaby nazwę clients, nie byłoby potrzebne wywołanie set_table_name. Relacje i ograniczenia zostaną odczytane z ograniczeń tabeli customers.
Klucze złożone Klucze złożone to klucze główne składające się z dwóch lub więcej atrybutów — najlepiej ich unikać. Nie tylko są trudniejsze do zarządzania niż proste klucze główne, ale zwykle są bardziej narażone na awarie. Powodem użycia kluczy złożonych jest zwykle pewien wewnętrznie unikalny aspekt danych, który powoduje, że klucz główny będzie znaczący (związany z danymi), a nie bez znaczenia (związany tylko z bazą danych). Zwykle mamy duże opory przed przydzielaniem nieznaczących kluczy głównych wykorzystywanych wyłącznie w bazie danych. Dzięki temu spójność bazy danych jest związana z bazą danych, a nie z zewnętrznym systemem lub procesem. Przykładem może być system rejestrujący samochody na podstawie ich numerów rejestracyjnych. Kluczem kandydatem może być {województwo, numer}. Zaletą użycia klucza sztucznego jest to, że liczby całkowite są łatwiejsze do reprezentacji niż listy — łatwiej jest odwołać się do rekordu 12345 niż do [SG, 1234]. Upraszcza to również klucze obce, jak również usługi WWW i inne protokoły wykorzystywane do integracji. Jednak podstawowym problemem jest to, że klucz główny jest zwykle traktowany jako unikalny, stabilny identyfikator rekordu. Klucz złożony może nie być w praktyce unikalny i może się nawet zmieniać. Jeżeli użylibyśmy przedstawionego wcześniej klucza złożonego, trzeba być przygotowanym na następujące pytania: • Co się stanie, gdy użytkownik przeprowadzi się lub zostanie wydany nowy numer reje-
stracyjny?
• Co się stanie, gdy zmianie ulegnie wewnętrzna charakterystyka klucza? Na przykład, co
powinniśmy zrobić, gdy numery rejestracyjne zostaną rozszerzone do 10 znaków? Jest to ogólny problem stosowania kluczy na znaczących danych.
15
http://drysql.rubyforge.org.
114
|
Rozdz ał 4. Bazy danych
• Czy jesteśmy przygotowani na odrzucenie każdego rekordu z duplikatem lub brakującym
kluczem? Czy lepiej system powinien przechowywać nieprawidłowe dane do momentu ich poprawienia?
Istnieją jednak sytuacje, gdy stosowanie kluczy złożonych jest prawidłowe. Dobrym przykładem jest replikacja wielonadrzędna. Ogromnym problemem w przypadku asynchronicznej replikacji wielonadrzędnej jest synchronizacja sekwencji kluczy głównych. Jeżeli wstawiamy dwa rekordy w zbliżonym czasie na dwóch różnych serwerach nadrzędnych, musi istnieć mechanizm zapewniający, że te dwa serwery wygenerują różne wartości kluczy głównych dla każdego z rekordów, dzięki czemu unikniemy problemów w momencie replikacji rekordów. Rozwiązaniem problemu sekwencji wielonadrzędnych jest zastosowanie klucza złożonego i wykorzystywanie identyfikatora serwera jako części tego klucza — dzięki temu każdy serwer może korzystać z własnej sekwencji, niezależnie od innych. Dwa rekordy mogą posiadać klucze główne {SerwerA, 5} i {SerwerB, 5}, więc konflikt nie wystąpi. Należy zwrócić uwagę, że jest to prawidłowe zastosowanie kluczy złożonych, ponieważ nie mają one znaczenia (względem danych przechowywanych w atrybutach). Do obsługi takich sytuacji dr Nic Williams przygotował rozwiązanie pozwalające na operacje na kluczach złożonych w ActiveRecord. Moduł gem composite_primary_keys jest dostępny pod adresem http://compositekeys.rubyforge.org. Jako przykład rozważmy przedstawiony tu problem sekwencji wielonadrzędnych. Mamy model Order, który jest replikowany pomiędzy dwoma serwerami z użyciem replikacji wielonadrzędnej. Musimy użyć klucza złożonego w celu zapewnienia unikalności klucza głównego, niezależnie od tego, na którym serwerze zostało złożone zamówienie. Na początek należy zainstalować gem: gem install composite_primary_keys
Następnie należy dołączyć tę bibliotekę do aplikacji. W przypadku Rails możemy dodać następującą instrukcję na końcu pliku environment.rb: require 'composite_primary_keys'
Następnym krokiem będzie wywołanie metody set_primary_keys(*keys) w celu poinformowania ActiveRecord, że korzystamy z klucza złożonego: class Order < ActiveRecord::Base set_primary_keys :node_id, :order_id end
Po skonfigurowaniu klucza złożonego większość operacji ActiveRecord działa tak jak do tej pory, z wyjątkiem tego, że klucze główne są teraz reprezentowane w postaci tablicy, a nie liczby całkowitej. Order.primary_key # => [ node_id, order_id] Order.primary_key.to_s # => "node_id,order_id" Order.find 1, 5 # => #"1", "order_id"=>"5"}>
Nawet asocjacje działają w normalny sposób; trzeba jednie jawnie określić klucz obcy po obu stronach asocjacji. W celu zademonstrowania działania dodamy model LineItem, który należy do odpowiedniego modelu Order. class Order < ActiveRecord::Base set_primary_keys :node_id, :order_id has_many :line_items, :foreign_key => [:order_node_id, :order_id] end
Zaawansowane funkcje baz danych
|
115
class LineItem < ActiveRecord::Base set_primary_keys :node_id, :line_item_id belongs_to :order, :foreign_key => [:order_node_id, :order_id] end
Należy zwrócić uwagę, że tak samo jak w zwykłych asocjacjach klucze obce są takie same po obu stronach, ponieważ relacja jest definiowana poprzez jeden klucz obcy (pomimo tego, że jest on złożony z dwóch atrybutów). Może to być nieco mylące, jeżeli nie weźmiemy pod uwagę sposobu reprezentowania relacji w schemacie, ponieważ opcja foreign_key zdefiniowana w instrukcji has_many :line_items klasy Orders faktycznie odwołuje się do atrybutów LineItem. Na koniec możemy tak skonfigurować model, aby można było zapomnieć o budowie kluczy w kodzie. Jak pamiętamy, początkowym powodem użycia kluczy złożonych było umożliwienie stosowania niezależnych sekwencji na każdym serwerze bazy danych. Na początek utworzymy te sekwencje w SQL przy tworzeniu tabel. Sposób ich konfiguracji jest zależny od systemu DBMS — składnia dla PostgreSQL jest następująca: CREATE SEQUENCE orders_order_id_seq; CREATE TABLE orders( node_id integer not null, order_id integer not null default nextval('orders_order_id_seq'), (inne atrybuty) PRIMARY KEY (node_id, order_id) ); CREATE SEQUENCE line_items_line_item_id_seq; CREATE TABLE line_items( node_id integer not null, line_item_id integer not null default nextval('line_items_line_item_id_seq'), -- FK to orders order_node_id integer not null, order_id integer not null, (inne atrybuty) PRIMARY KEY (node_id, line_item_id) );
Po wykonaniu tych instrukcji DDL na węzłach bazy danych i aktywowaniu replikacji pomiędzy nimi każdy z węzłów będzie korzystał z własnej sekwencji, niezależnej od innych. Teraz musimy tylko zapewnić, że każdy węzeł będzie używał własnego identyfikatora węzła. Możemy to zrealizować w bazie danych poprzez wartość domyślną kolumny (jeżeli będziemy korzystać z różnego kodu DDL dla każdego węzła) lub w aplikacji, w wywołaniu before_create (jeżeli każda aplikacja korzysta z jednego węzła).
Wyzwalacze, reguły i procedury składowane Wchodzimy teraz na niebezpieczny teren. Trzeba wiedzieć, że powinniśmy mieć dobry powód użycia wyzwalaczy, reguł lub procedur składowanych dla nawet skomplikowanych zadań. Nie można powiedzieć, że nie są one potrzebne — czasami mogą nam uratować życie. Jednak powinny być używane do rozwiązywania specyficznych problemów, takich jak: • Skomplikowane procesy wymagające przeszukiwania dużej ilości danych (na przykład
OLAP lub analiza dzienników) mogą być znacznie szybsze, jeżeli będziemy je realizować na serwerze bazy danych. Jak zawsze, kluczem jest profilowanie — przedwczesna optyma-
116
|
Rozdz ał 4. Bazy danych
lizacja może skutkować spadkiem wydajności, jak również zmarnowaniem czasu przez programistę. • Zagadnienia, które mają niewiele wspólnego z logiką aplikacji, na przykład dzienniki audytu,
które mogą być bezpiecznie realizowane w bazie danych przez wyzwalacze. • PostgreSQL może wykorzystać reguły do utworzenia perspektyw aktualizowanych. Nie-
stety, jest to jedyny sposób uzyskania perspektyw aktualizowanych. • Przy wykorzystywaniu dużych obiektów w PostgreSQL powinniśmy wykorzystać wyzwala-
cze do usuwania dużych obiektów w momencie usunięcia odpowiadającego mu rekordu (zawierającego OID obiektu LOB). Można to traktować jako formę spójności powiązań. • Procedury składowane można wykorzystywać do dostępu do typów rozszerzonych lub
nienatywnych. PostGIS, geoprzestrzenna baza danych dla PostgreSQL, korzysta z funkcji do zarządzania danymi i indeksami przestrzennymi. • Biblioteka TSearch2, zintegrowana w PostgreSQL 8.3 i nowszych, wykorzystuje funkcje
do dostępu do mechanizmów indeksowania pełnotekstowego. Niektóre aplikacje korzystają z procedur składowanych do wszystkich operacji dostępu do danych w celu zapewnienia kontroli dostępu. Zdecydowanie nie jest to sposób zgodny z Rails. Choć rozwiązanie takie ma szansę działać, będzie znacznie trudniejsze niż bezpośredni dostęp do tabel i widoków. Widoki zapewniają wystarczający poziom kontroli dostępu w większości aplikacji korporacyjnych — procedury składowane należy wykorzystywać tylko wtedy, gdy będziemy zmuszeni użyć tego rozwiązania. ActiveRecord może w sposób przezroczysty użyć aktualizowanych widoków, jakby były to zwykłe tabele.
Przykłady Usuwanie dużych obiektów Ponieważ duże obiekty są w PostgreSQL odłączone od wykorzystujących je rekordów, przydatne jest skonfigurowanie prostej reguły usuwającej je przy usuwaniu rekordu. Zasada może być zbudowana w następujący sposób: -- (nazwa tabeli to 'attachments'; LOB OID to 'file_oid') CREATE RULE propagate_deletes_to_lob AS ON DELETE TO attachments DO ALSO SELECT lo_unlink(OLD.file_oid) AS lo_unlink
Partycjonowanie danych PostgreSQL ma zaawansowany system zasad, który pozwala zmieniać przychodzące zapytania na wiele sposobów. Jednym z zastosowań systemu reguł jest implementacja partycjonowania, w którym dane z jednej tabeli znajdują się w jednej z kilku tabel, w zależności od zdefiniowanego warunku. Weźmy pod uwagę aplikację obsługi biura nieruchomości. Dla celów historycznych chcemy zachować oferty, których ważność wygasła, zostały sprzedane lub usunięte z systemu. Jednak większość danych używanych w codziennej pracy to dane ofert bieżących. Dodatkowo zbiory danych „bieżących ofert” i „wszystkich ofert” mają różne przeznaczenie — pierwszy jest używany transakcyjnie, a drugi analitycznie. Sensowne jest przechowywanie ich osobno, ponieważ mają różne charakterystyki. Zaawansowane funkcje baz danych
|
117
Na początek załóżmy, że mamy tabelę ofert o nazwie listings, w której znajduje się kolumna status, reprezentująca stan oferty. Utworzymy dwie tabele, current_listings i non_current_ ´listings, dziedziczące po głównej tabeli. W ten sposób możemy użyć polecenia SELECT * FROM listings i PostgreSQL automatycznie dołączy dane z dwóch dziedziczących tabel. CREATE TABLE current_listings (CHECK (status = 'C')) INHERITS (listings); CREATE TABLE non_current_listings (CHECK (status != 'C')) INHERITS (listings);
Następnie utworzymy regułę zmieniającą operacje wstawienia do tabeli nadrzędnej na wstawienia do prawidłowej tabeli podrzędnej: CREATE RULE listings_insert_current AS ON INSERT TO listings WHERE (status = 'C') DO INSTEAD INSERT INTO current_listings VALUES(NEW.*); CREATE RULE listings_insert_non_current AS ON INSERT TO listings WHERE (status != 'C') DO INSTEAD INSERT INTO non_current_listings VALUES(NEW.*);
Po utworzeniu zasad możemy przenieść istniejące dane do prawidłowej podtabeli: INSERT INTO current_listings SELECT * FROM listings WHERE STATUS = 'C'; INSERT INTO non_current_listings SELECT * FROM listings WHERE STATUS != 'C'; DELETE FROM listings;
Wiemy, że polecenie DELETE jest bezpieczne, ponieważ dane nie będą wstawiane do tabeli listings, dzięki zastosowaniu zasad zmieniających zapytania. Dlatego właśnie ważne jest, aby wyrażenie partycjonujące prawidłowo realizowało operację podziału, na przykład status = 'C' i status != 'C' (warunki się nie nakładają i w pełni obejmują wszystkie możliwości). Zapewnia to, że każdy wiersz zostanie wstawiony do jednej z tabel podrzędnych, a nie nadrzędnej. Należy zwrócić uwagę, że partycjonowanie nie będzie dobrze działać, jeżeli kolumna status dopuszczać będzie wartości NULL, ponieważ oba warunki będą miały wartość false. Teraz możemy wstawiać i wybierać dane z listings tak, jakby była to jedna tabela, natomiast PostgreSQL w sposób przezroczysty obsłuży partycjonowanie i będzie działać na odpowiedniej partycji. Jest to tylko bardzo prosty przykład. W szczególności przed użyciem tego schematu konieczne będzie napisanie reguł dla zapytań UPDATE i DELETE. Metoda ta może być rozszerzona na wiele partycji, nawet wybieranych za pomocą skomplikowanych warunków.
Podłączanie do wielu baz danych Czasami zachodzi potrzeba podłączania się do kilku różnych baz danych z jednej aplikacji. Jest to przydatne w przypadku wykonywania migracji ze starego schematu do nowego. Jest również przydatne, jeżeli mamy różne wymagania dotyczące danych używanych w jednej aplikacji — część danych może być krytyczna i być przechowywana na klastrze bazy danych o wysokiej dostępności. Obsługa każdego z tych przypadków jest prosta w Rails. Na początek należy zdefiniować wiele środowisk baz danych w pliku konfiguracyjnym database.yml: legacy: adapter: mysql database: my_db username: user password: pass host: legacy_host new: adapter: mysql
118
|
Rozdz ał 4. Bazy danych
database: my_db username: user password: pass host: new_host
Następnie można odwoływać się do tych bloków konfiguracji z klasy ActiveRecord przy wykorzystaniu metody ActiveRecord::Base.establish_connection: class LegacyClient < ActiveRecord::Base establish_connection "legacy" end class Client < ActiveRecord::Base establish_connection "new" end
Podejście to działa również w przypadku różnych środowisk Rails. Wystarczy tak jak zwykle zdefiniować każde środowisko w pliku database.yml: legacy_development: # ... legacy_test: # ... legacy_production: # ... new_development: # ... new_test: # ... new_production: # ...
Następnie należy użyć stałej RAILS_ENV w nazwie bloku konfiguracji bazy danych: class LegacyClient < ActiveRecord::Base establish_connection "legacy_#{RAILS_ENV}" end class Client < ActiveRecord::Base establish_connection "new_#{RAILS_ENV}" end
Można również pójść krok dalej i napisać ten kod, korzystając z dziedziczenia klas do zdefiniowania bazy danych, do której należy dana klasa ActiveRecord: class LegacyDb < ActiveRecord::Base self.abstract_class = true establish_connection "legacy_#{RAILS_ENV}" end class NewDb < ActiveRecord::Base self.abstract_class = true establish_connection "new_#{RAILS_ENV}" end class LegacyClient < LegacyDb end class Client < NewDb end
Podłączan e do w elu baz danych
|
119
Instrukcja self.abstract_class = true informuje ActiveRecord, że klasy LegacyDb i NewDb nie mogą być konkretyzowane, ponieważ reprezentują połączenia z bazą danych — nie są obsługiwane przez konkretne tabele w bazie danych.
Magiczne połączenia wielokrotne Moduł gem Magic Multi-Connections (http://magicmodels.rubyforge.org/magic_multi_connections), którego autorem jest dr Nic Williams, pozwala na równoczesne podłączanie się do wielu baz danych z tej samej aplikacji. Jest on bardzo przydatny, jeżeli wykorzystywany jest jeden serwer nadrzędny i kilka podrzędnych tylko do odczytu, które udostępniają ten sam model. Składnia jest przezroczysta — korzysta z przestrzeni nazw modułu i importuje modele (klasy dziedziczące po ActiveRecord::Base) do przestrzeni nazw. W przypadku konfiguracji z jednym serwerem nadrzędnym możemy zdefiniować w pliku database.yml kolejne połączenie bazy danych dla serwera podrzędnego: read_slave: adapter: postgresql database: read_only_production username: user password: pass host: read_slave_host
Ta baza danych jest obsługiwana przez moduł, który zawiera lustrzane odbicie klas ActiveRecord wykorzystujących to połączenie bazy danych: require 'magic_multi_connections' module ReadSlave establish_connection :read_slave end
Następnie, przez dodanie do klas modelu przedrostka ReadSlave::, wszystkie wcześniej istniejące modele zmieniają swoje połączenie na read_slave: # użycie połączenia tylko do odczytu @user = ReadSlave::User.find(params[:id]) # zapis do połączenia nadrzędnego (nie można używać @user.update_attributes # ponieważ może to spowodować próbę zapisu do połączenia do odczytu) User.update(@user.id, :login => "new_login")
Buforowanie Jeżeli aplikacja wykonuje więcej odczytów niż zapisów, buforowanie modelu pozwala zmniejszyć obciążenie serwera bazy danych. Standardowym, stosowanym obecnie systemem buforowania w pamięci jest memcached16. Rozwiązanie to, opracowane dla LiveJournal, jest rozproszonym buforem funkcjonującym jak gigantyczna tabela mieszająca. Z powodu swojej prostoty jest skalowalny i szybki. Jest zaprojektowany w taki sposób, że nigdy nie używa blokad, więc nie istnieje ryzyko zakleszczenia. Bufor ten oferuje cztery proste operacje, z których każda jest wykonywana w stałym czasie.
16
Nazwa pochodzi od memory cache daemon. Program jest udostępniony na stronie http://danga.com/memcached.
120
|
Rozdz ał 4. Bazy danych
Programu memcached można użyć w kilku mechanizmach Rails. Pozwala realizować magazyn sesji lub bufor fragmentów, przy założeniu, że zainstalowany jest moduł gem ruby-memcache. Może być również wykorzystywany do przechowywania całych modeli — należy jednak pamiętać, że jest to efektywne w przypadku, gdy liczba odczytów ogromnie przekracza liczbę zapisów. Istnieją dwie biblioteki obsługujące buforowanie modelu: cached_model oraz acts_as_cached. Biblioteka cached_model (http://rubyfurnace.com/gems/cached_model) udostępnia abstrakcyjną klasę CachedModel, dziedziczącą po ActiveRecord::Base. Próbuje ona być możliwie przezroczysta, buforując tylko proste zapytania odwołujące się do pojedynczych obiektów, i nie próbuje wykonywać nic bardziej wymyślnego. Wadą jest to, że wszystkie buforowane modele muszą dziedziczyć po CachedModel. Użycie biblioteki cached_model jest niezmiernie proste: class Client < CachedModel end
Wtyczka acts_as_cached (http://errtheblog.com/post/27) daje większą kontrolę nad buforowanymi danymi. Ma się wrażenie programowania z użyciem API memcached, ale mając do dyspozycji większe możliwości i bardziej zwięzłe wywołania. Wtyczka ta posiada obsługę relacji między obiektami i może nawet wersjonować klucze w celu unieważniania starych kluczy po zmianie schematu. Przykładowy obiekt korzystający z acts_as_cached wygląda następująco: class Client < ActiveRecord::Base acts_as_cached # Musimy samodzielnie unieważnić bufor po znacznych zmianach after_save :expire_me after_destroy :expire_me protected def expire_me expire_cache(id) end end
Oczywiście odpowiednie rozwiązanie zależy od specyficznych wymagań aplikacji. Należy pamiętać, że buforowanie jest przede wszystkim optymalizacją, więc zawsze trzeba brać pod uwagę ostrzeżenie o przedwczesnej optymalizacji. Optymalizacja zawsze powinna być rozwiązaniem dla specyficznego i zdiagnozowanego problemu z wydajnością. Bez precyzyjnego zlokalizowania problemu nie będziemy wiedzieć, co mierzymy (lub powinniśmy mierzyć). Bez pomiaru nie będziemy wiedzieć, kiedy i o ile poprawiliśmy wydajność.
Wyrównywanie obciążenia i wysoka dostępność Wiele aplikacji wymaga pewnej formy wyrównywania obciążenia i (lub) wysokiej dostępności. Terminy te są często używane razem i cechy te mogą być uzyskiwane przy pomocy tych samych metod, ale są to fundamentalnie różne wymagania. Powinniśmy więc zdefiniować je. Wyrównywanie obciążenia Rozkładanie obciążenia żądaniami na kilka systemów w celu zmniejszania obciążenia pojedynczego systemu. Wysoka dostępność Odporność na awarie jednego lub kilku składników, zdolność do ciągłej realizacji usług pomimo awarii składnika.
Wyrównywan e obc ążen a wysoka dostępność
|
121
Są to zupełnie różne cechy, ale często są one wymagane i (lub) dostarczane razem. Ważne jest, aby rozumieć różnicę pomiędzy nimi, aby móc prawidłowo analizować wymagania aplikacji. Możliwe jest zapewnienie wyrównywania obciążenia bez wysokiej dostępności — jako przykład weźmy grupę serwerów udostępnionych w internecie poprzez DNS cykliczny. Obciążenie jest rozkładane mniej więcej równo w tej grupie serwerów, ale systemy nie zapewniają wysokiej dostępności! Jeżeli jeden serwer zostanie wyłączony, DNS nadal będzie dystrybuował do niego żądania i co N żądanie zostanie bez odpowiedzi. Dla porównania — wysoka dostępność może być zapewniana bez wyrównywania obciążenia. Wysoka dostępność wymaga zastosowania nadmiarowych składników, ale nie istnieje wymaganie, że komponenty te muszą być podłączone i używane. Często stosowaną konfiguracją jest gotowa do pracy rezerwa — drugi serwer jest uruchomiony, ale zamiast realizować żądania, stale monitoruje pracę pierwszego, gotowy do przejęcia zadań w razie potrzeby. Może to być bardziej ekonomiczne niż próba wyrównywania obciążenia pomiędzy dwoma serwerami i utrzymania spójności. W kolejnych punktach przedstawimy podstawowe rozwiązania wyrównywania obciążenia i wysokiej dostępności w przypadku często używanych systemów zarządzania bazami danych.
MySQL Replikacja MySQL ma wbudowaną obsługę replikacji typu master-slave. Serwer główny wykonuje dzienniki wszystkich transakcji, wykorzystując binlog (dziennik binarny). W czasie replikacji binlog jest odtwarzany na serwerach podrzędnych, co powoduje wykonanie na nich zapisanych transakcji. Serwery podrzędne mogą korzystać z różnych silników zapisu, co powoduje, że funkcja ta jest przydatna w różnych zastosowaniach, takich jak kopie bezpieczeństwa i indeksowanie pełnotekstowe. Replikacja master-slave dobrze się nadaje do wyrównywania obciążenia w aplikacjach, w których liczba odczytów jest znacznie większa niż liczba zapisów, ponieważ wszystkie zapisy muszą być realizowane na serwerze głównym. Jednak opisana tu replikacja master-slave nie zapewnia wysokiej dostępności — występuje tu jeden serwer nadrzędny stanowiący pojedynczy punkt awarii. Serwer podrzędny może być wypromowany do nadrzędnego podczas procedury podjęcia pracy po awarii, ale polecenia powodujące tę akcję muszą być wykonane ręcznie, przez napisany samodzielnie skrypt monitorujący. Nie istnieje obecnie możliwość automatycznej promocji serwera podrzędnego. Dodatkowo wszyscy klienci muszą być w stanie określić, który serwer jest obecnie nadrzędnym. W dokumentacji MySQL sugerowane jest skonfigurowanie wpisu w dynamicznym DNS wskazującego na bieżący serwer nadrzędny, jednak powoduje to wprowadzenie kolejnego potencjalnego punktu awarii.
Klaster MySQL Podstawowym rozwiązaniem zapewniającym wysoką dostępność dla MySQL jest technologia MySQL Cluster, dostępna od wersji 4.1. Klaster jest przede wszystkim pamięciową bazą danych, choć od wersji 5. obsługiwany jest zapis na dysku. Klaster ten bazuje na silniku zapisu NDB, wspieranym węzłami danych.
122
|
Rozdz ał 4. Bazy danych
Rozwiązanie MySQL Cluster jest zaprojektowane dla lokalnych węzłów — klastry rozproszone nie są obsługiwane, ponieważ protokół używany pomiędzy węzłami nie jest szyfrowany ani zoptymalizowany pod względem użycia pasma. Połączenia mogą wykorzystywać Ethernet (100 Mb/s lub szybszy) lub SCI (Scalable Coherent Interconnect — szybki protokół połączeniowy dla klastrów). Jest to najbardziej efektywne dla klastrów od średnich do dużych zbiorów danych — zalecana konfiguracja obejmuje od 1 do 8 węzłów mających po 16 GB pamięci RAM. Ponieważ większość danych jest przechowywana w pamięci, klaster musi mieć odpowiednio dużo pamięci, aby mógł przechować tyle nadmiarowych kopii pełnych zbiorów roboczych, ile potrzebuje aplikacja. Liczba ta jest nazywana współczynnikiem replikacji. W przypadku współczynnika replikacji 2 każda dana jest przechowywana na dwóch serwerach, więc można utracić jeden serwer bez utraty danych. W celu zachowania wysokiej dostępności muszą zostać użyte co najmniej trzy fizyczne serwery — dwóch węzłów danych i węzła zarządzającego. Węzeł zarządzający jest potrzebny do wykonania arbitrażu pomiędzy dwoma węzłami danych, jeżeli zostaną one rozłączone i utracą synchronizację ze sobą. Użyty zostanie współczynnik replikacji równy 2, więc dwa węzły danych muszą mieć odpowiednio dużo pamięci, aby przechować zbiory danych, o ile nie zostanie użyte przechowywanie na dysku. Ponieważ oprogramowanie klastra jest po prostu silnikiem przechowywania, klaster jest dostępny poprzez serwer MySQL z tabelami zdefiniowanymi w zapleczu NDB. Serwer odwołuje się do klastra w celu realizacji żądań klienta. Architektura tego rozwiązania jest pokazana na rysunku 4.3.
Rysunek 4.3. Architektura MySQL Cluster
Wyrównywan e obc ążen a wysoka dostępność
|
123
Ponieważ serwery mysqld różnią się od serwerów nieklastrowanych swoim zapleczem, mogą być one replikowane z użyciem dzienników binarnych identycznie jak serwery niepracujące w klastrze. Możliwe jest więc realizowanie długodystansowej replikacji master-slave pomiędzy wieloma klastrami. Możliwe jest używanie kilku serwerów mysqld korzystających z tego samego klastra i użycie obsługi tych samych klientów w celu zapewnienia nadmiarowości. Na zamieszczonym diagramie serwer MySQL jest pojedynczym punktem awarii — jeżeli zostanie on wyłączony, aplikacja nie będzie mogła odwołać się do klastra. Istnieją trzy podejścia do obsługi wyrównywania obciążenia i przełączania awaryjnego wielu serwerów MySQL: • Modyfikacja kodu aplikacji do obsługi uszkodzonych serwerów i powtarzania zapytań
na innych serwerach. W tym scenariuszu każdy serwer MySQL może mieć własny adres IP. • Użycie osobnego sprzętowego lub programowego wyrównywacza obciążenia pomiędzy
aplikacją i serwerami MySQL. Spowoduje to utworzenie wirtualnych adresów IP (VIP) kierowanych do jednego z serwerów fizycznych za pośrednictwem DNAT. Metoda ta jest kosztowna, ponieważ wymagane są co najmniej dwa wyrównywacze obciążenia dla zapewnienia wysokiej dostępności. • Użycie programowego rozwiązania wysokiej dostępności, takim jak Wackamole (http://www.
´backhand.org/wackamole). Pozwoli to udostępnić pulę wirtualnych adresów IP i upewnić się, że tylko jeden działający serwer posiada przez cały czas jeden adres IP. Jeżeli serwer ulegnie awarii, jego adresy VIP zostaną rozesłane pośród pozostałych. Pula adresów VIP jest rozprowadzana za pomocą listy cyklicznej DNS, więc aplikacja otrzyma mniej lub bardziej losowy adres VIP.
PostgreSQL Dla systemu PostgreSQL dostępnych jest kilka rozwiązań wyrównywania obciążenia i wysokiej dostępności. Ponieważ nie ma jednej firmy odpowiedzialnej za PostgreSQL, rozwiązania te są opracowywane przez różne organizacje i firmy. Każde z nich korzysta z innego paradygmatu replikacji lub klastrowania. W punkcie tym przedstawimy kilka możliwości.
Wysoka dostępność — działająca rezerwa Działająca rezerwa jest najprostszym sposobem osiągnięcia wysokiej dostępności w PostgreSQL. Wymaga to opracowania odpowiedniej konfiguracji, ale proces ten jest dobrze udokumentowany. Działająca rezerwa korzysta z dziennika wyprzedzającego (WAL), w którym PostgreSQL rejestruje wykonywane operacje. Zmiany są zapisywane do WAL przed ich zatwierdzeniem, dzięki czemu stan bazy danych może być odtworzony nawet w przypadku katastrofalnego przerwania transakcji. Przesyłanie dzienników jest procesem przesyłania WAL w postaci plików — od serwera nadrzędnego do podrzędnego. W konfiguracji działającej rezerwy serwer działa w stanie pogotowia, w trybie odtwarzania. Jest on stale odtwarzany z serwera podstawowego przy użyciu polecenia odtwarzania, które oczekuje na dostępność pliku WAL i nakłada zmiany tak szybko, jak jest możliwe. Gdy podstawowy serwer stanie się niedostępny, system monitorowania (który musi być zapewniony przez użytkownika) wyznacza serwer rezerwowy jako nowy serwer podstawowy.
124
|
Rozdz ał 4. Bazy danych
Replikacja master-slave — Slony-I Slony-I jest systemem replikacji master-slave, podobnym do mechanizmów dostępnych w MySQL. Obsługuje on promowanie serwerów podrzędnych do nadrzędnego, ale w przeciwieństwie do MySQL nie zapewnia żadnego mechanizmu wykrywania awarii węzła. Następca Slony, Slony-II jest obecnie we wczesnych stadiach rozwoju. Planowane jest zapewnienie synchronicznej replikacji wielonadrzędnej dla PostgreSQL, bazującej na bibliotece komunikacji grupowej Spread.
Replikacja wielonadrzędna — PGCluster PGCluster (http://pgcluster.projects.postgresql.org) jest produktem zapewniającym replikację wielonadrzędną i klastrowanie dla PostgreSQL. Zapewnia ona wyrównywanie obciążenia i wysoką dostępność dla klastra bazy danych. Oprogramowanie obsługuje przełączanie w przypadku awarii i zapewnia gotowe rozwiązanie w przypadku użycia trzech lub więcej fizycznych serwerów. Replikacja PGCluster działa w sposób synchroniczny — aktualizacje są przesyłane do wszystkich serwerów przed zakończoną aktualizacją transakcji. Dlatego powinna być używana w środowiskach, w których wszystkie serwery nadrzędne znajdują się w tej samej lokalizacji i są stale podłączone. Replikacja asynchroniczna, w której zmiany są propagowane do innych serwerów po pewnym czasie po zatwierdzeniu transakcji, jest zwykle uważana za trudny problem. Replikacja asynchroniczna jest również specyficzna dla aplikacji, ponieważ prawidłowy sposób obsługi konfliktów pomiędzy dwoma zatwierdzonymi transakcjami zależy od potrzeb aplikacji.
Oracle Oracle realizuje klastrowanie za pomocą Oracle Real Application Clusters (RAC). W porównaniu z rozwiązaniami klastrowymi z innych systemów DBMS, działającymi na zasadach nieudostępniania niczego, RAC jest klastrem typu udostępnij wszystko. W przypadku RAC wiele instancji Oracle korzysta ze współdzielonego klastra bazy danych. Architektura udostępniania wszystkiego zależy od wspólnego magazynu danych, takiego jak sieć pamięci masowych (SAN). Oracle obsługuje wiele elastycznych opcji replikacji, od zwykłej jednokierunkowej replikacji danych do rozproszonej replikacji wielonadrzędnej. Rozwiązania te dają bardzo dużo możliwości i jednocześnie są bardzo skomplikowane.
Microsoft SQL Server Podobnie jak Oracle, SQL Server posiada obszerne funkcje obsługujące zarówno replikację, jak i klastrowanie. SQL Server obsługuje nawet „replikację złączeniową”, która w istocie jest asynchroniczną replikacją wielonadrzędną. Oczywiście, opcje klastrowania i replikacji wymagają dużego nakładu pracy przy konfiguracji. Jeszcze nie istnieje gotowe po zainstalowaniu rozwiązanie wyrównywania obciążenia dla SQL Server — nadal należy tak napisać kod aplikacji, aby kierowała żądania do odpowiedniego serwera.
Wyrównywan e obc ążen a wysoka dostępność
|
125
LDAP LDAP, Lightweight Directory Access Protocol, to system bazy danych zoptymalizowanej do przechowywania danych o użytkownikach. Najczęściej jest używany w dużych organizacjach i jest zintegrowany z uwierzytelnianiem korporacyjnym i systemami pocztowymi. Jest to w pełni funkcjonalna baza danych. Nie mamy tu miejsca na szczegółowe opisywanie LDAP, ale dostępnych jest wiele zasobów poświęconych korzystaniu z LDAP za pomocą Rails.
ActiveLDAP Biblioteka ActiveLDAP (http://ruby-activeldap.rubyforge.org) jest niemal bezpośrednim zastępnikiem ActiveRecord, który korzysta z LDAP zamiast z systemu DBMS. Aby użyć tej biblioteki w Rails, należy wpisać do pliku konfiguracyjnego, config/ldap.yml, następujące dane: development: host: (nazwa serwera ldap) port: 389 base:dc=mycompany,dc=com password: moje_haslo production:
Następnie na końcu pliku config/environment.rb należy skonfigurować połączenie: ldap_path = File.join(RAILS_ROOT,"config","ldap.yml") ldap_config = YAML.load(File.read(ldap_path))[RAILS_ENV] ActiveLDAP::Base.establish_connection(ldap_config)
Aby skonfigurować ActiveLDAP, należy odziedziczyć po klasie ActiveLDAP::Base i klasa po klasie skonfigurować odwzorowania LDAP: class Employee < ActiveLDAP::Base ldap_mapping :prefix => "ou=Employees" end
Zapytania LDAP mogą być wykonywane za pomocą metod klasowych z ActiveLDAP::Base: @dan = Employee.find :attribut => "cn", :value => "Dan"
Uwierzytelnianie za pomocą LDAP Jednym z najczęstszych powodów korzystania z LDAP jest integracja z istniejącą strukturą uwierzytelniania. Jeżeli dla domeny Windows uruchomiony jest serwer LDAP, pozwoli to aplikacji WWW na uwierzytelnianie użytkowników w domenie, zamiast tworzyć osobny model użytkowników. W tym celu należy skonfigurować plik ldap.yml w sposób przedstawiony poprzednio (bez określania hasła), ale nie wykonuje się dołączania serwera LDAP w pliku environment.rb. Dołączanie będzie realizowane jako część procesu uwierzytelniania. Poniższy kod jest zapożyczony z wiki Rails17: class LdapUser < ActiveLDAP::Base ldap_mapping :prefix => (LDAP prefix for your users)
LDAP_PATH = File.join(RAILS_ROOT,"config","ldap.yml") LDAP_CONFIG = YAML.load(File.read(ldap_path))[RAILS_ENV] def self.authenticate username, password begin ActiveLDAP::Base.establish_connection(config.merge( :bind_format => "uid=#{username},cn=users,dc=mycompany,dc=com", :password => password, :allow_anonymous => false )) ActiveLDAP::Base.close return true rescue ActiveLDAP::AuthenticationError return false end end end
Sama operacja uwierzytelniania jest bardzo prosta: LdapUser.authenticate "użytkownik", "hasło" # => true lub false
Propozycje dalszych lektur Bardzo przystępnym wprowadzeniem do teorii relacji, kierowanym do programistów doświadczonych w korzystaniu z relacyjnych baz danych, jest Database in Depth (O’Reilly) autorstwa Chrisa Date’a. Ponownie wprowadza ona w podstawy techniczne wspierające model relacyjny. Książka Theo Schlossnagle’a Scalable Internet Architectures (Sams) zawiera bardzo krótkie, ale wyczerpujące streszczenie sposobów realizacji skalowalności (opisana jest zarówno wysoka dostępność, jak i wyrównywanie obciążenia) — opisuje ona konfiguracje od najmniejszych, dwuwęzłowych klastrów odpornych na awarie, do największych systemów globalnego wyrównywania obciążenia serwerów. Zarówno podręcznik MySQL (http://dev.mysql.com/doc), jak i PostgreSQL (http://www.postgresql. ´org/docs) zawiera ogólne zagadnienia związane z bazami danych, a także szczegółowe informacje na temat korzystania z tych systemów DBMS.
Propozycje dalszych lektur
|
127
128
|
Rozdz ał 4. Bazy danych
ROZDZIAŁ 5.
Bezpieczeństwo
Mając wybór pomiędzy tańczącymi świniami i bezpieczeństwem, użytkownicy za każdym razem wybiorą świnie. Ed Felten i Gary McGraw
Zagadnienia bezpieczeństwa są często pomijane w przypadku mniejszych witryn lub aplikacji o małym obciążeniu — niestety, zasięg sieci WWW rozszerzył się do takich rozmiarów, że zachowanie pełnego bezpieczeństwa jest niezbędne we wszystkich witrynach dostępnych publicznie. Okazuje się, że istnieją ludzie niemający niczego lepszego do zrobienia niż wykonanie rozproszonego ataku typu odmowa usługi na witrynę „Zabawne zdjęcia kotów cioci Edny”. Nikt nie może sobie pozwolić na ignorowanie niebezpieczeństw, jakie czyhają na witrynę dostępną w internecie. W tym rozdziale przedstawimy podejście zstępujące do analizy różnych problemów związanych z bezpieczeństwem, którymi muszą zajmować się programiści aplikacji. Rozpoczniemy od analizy zasad architektury i poziomu aplikacji, o których należy pamiętać. Później zaczniemy stopniowo zwiększać poziom szczegółowości. Będziemy zajmować się problemami związanymi z bezpieczeństwem, o których należy pamiętać, pracując na niższych poziomach w Rails.
Problemy w aplikacji Na początek przedstawimy kilka ważnych zasad, do których powinniśmy się stosować w każdej aplikacji WWW.
Uwierzytelnianie Najważniejsza zasada dotycząca uwierzytelniania jest prosta: Zawsze stosuj wartość początkową i koduj wszystkie hasła! Istnieje niewiele wyjątków od tej reguły i bardzo rzadko dotyczą one aplikacji WWW. Jedynym możliwym powodem przechowywania haseł w postaci otwartego tekstu jest konieczność ich przekazania do zewnętrznej usługi właśnie w postaci otwartego tekstu. Nawet wtedy hasła powinny być zakodowane symetrycznie z użyciem współdzielonej frazy, co zapewnia lepsze zabezpieczenie.
129
Przeanalizujmy powody stojące za tą regułą. Kodowanie haseł zapobiega przed ich odczytaniem w przypadku włamania do bazy danych lub do kodu źródłowego. Użycie wartości początkowej zapobiega atakom typu rainbow. Użycie wartości początkowej (ang. salting) zapewnia, że to samo hasło będzie kodowane w różny sposób przez różnych użytkowników. Przeanalizujmy poniższy fragment kodu, gdzie hasła są kodowane bez wartości początkowej. require 'digest/sha1' $hashes = {} def hash(password) Digest::SHA1.hexdigest(password) end def store_password(login, password) $hashes[login] = hash(password) end def verify_password(login, password) $hashes[login] == hash(password) end store_password('alice', 'kittens') store_password('bob', 'kittens') $hashes # => {"alice"=>"3efd62ee86d4a141c3e671d86ba1579f934cf04d", # "bob"=> "3efd62ee86d4a141c3e671d86ba1579f934cf04d"} verify_password('alice', 'kittens') # => true verify_password('alice', 'mittens') # => false verify_password('bob', 'kittens') # => true
Choć jest to bardziej bezpieczne niż przechowywanie haseł w postaci otwartego tekstu, nadal jest mało bezpieczne, ponieważ każdy, kto zobaczy plik z hasłami, może stwierdzić, że konta alice i bob mają takie same hasła. Co ważniejsze, schemat taki jest nieodporny na atak typu rainbow. Napastnik może przygotować tablice rainbow przez zakodowanie każdego słowa ze słownika za pomocą funkcji hash. Następnie może porównywać każde hasło z tablicy rainbow z zakodowanym hasłem systemu. Ponieważ te same hasła będą po zakodowaniu również takie same, napastnik może uzyskać za jednym przebiegiem wszystkie hasła słownikowe. Można tego uniknąć przez użycie wartości początkowej w procesie kodowania. Porównajmy następujący kod: require 'digest/sha1' $hashes = {} $salts = {} def hash(password, salt) Digest::SHA1.hexdigest("--#{salt}--#{password}--") end def generate_salt(login) Digest::SHA1.hexdigest("--#{Time.now.to_s}--#{login}--") end
Metoda ta zapewnia, że te same hasła będą po zakodowaniu miały różną wartość, o wysokim stopniu losowości. Wtyczka acts_as_authenticated (http://wiki.rubyonrails.org/rails/pages/Acts_as_ ´authenticated) zapewnia domyślne stosowanie wartości początkowej. Jednym z powodów, dla których stosuje się przechowywanie haseł w postaci otwartego tekstu, jest odzyskiwanie haseł. W rzeczywistości przechowywanie i wysyłanie haseł otwartym tekstem nigdy nie było dobrym pomysłem. Prawidłowym sposobem na odzyskiwanie haseł jest wysłanie do użytkownika wiadomości e-mail z łączem zawierającym losowo wygenerowany żeton. Łącze to otwiera stronę, na której żeton jest weryfikowany, a następnie umożliwia się użytkownikowi wprowadzenie nowego hasła.
Kodowanie haseł w Rails W aplikacjach Rails są zdefiniowane standardowe praktyki dotyczące operacji na zakodowanych hasłach. Po pierwsze, baza danych zawiera atrybuty dla kodowanych haseł i wartości początkowej: ActiveRecord::Schema.define do add_column :users, :crypted_password, :string add_column :users, :salt, :string end
Najprostszym sposobem na wykorzystanie instrukcji definicji schematu z Rails jest użycie ActiveRecord::Schema.define w konsoli Rails lub innego kodu Rails poza migracją. Wewnątrz bloku dostępny jest pełny zbiór metod definicji schematu (patrz ActiveRecord::ConnectionAdapters::SchemaStatements).
Model User posiada atrybut wirtualny dla niezaszyfrowanych haseł, więc można ustawić hasło za pomocą metody instancyjnej User#password= i zostanie ono automatycznie zaszyfrowane. Szyfrowanie jest realizowane w metodzie wywołania zwrotnego before_save: class User < ActiveRecord::Base attr_accessor :password before_save :encrypt_password protected
Problemy w apl kacj
|
131
def encrypt_password return if password.blank? self.salt = Digest::SHA1.hexdigest("--#{Time.now.to_s}--#{login}--") if new_record? self.crypted_password = encrypt(password) end def encrypt(password) Digest::SHA1.hexdigest("--#{salt}--#{password}--") end end
Uwierzytelnianie jest obsługiwane za pomocą metody klasowej User.authenticate, która oczekuje podania nazw konta i hasła i zwraca odpowiedni obiekt User lub nil, jeżeli konto lub hasło jest nieprawidłowe. class def u u end
User < ActiveRecord::Base self.authenticate(login, password) = find_by_login(login) && u.authenticated?(password) ? u : nil
def authenticated?(password) crypted_password == encrypt(password) end end
Nie ufaj klientowi ActionPack (ActionController oraz ActionView) ułatwia wiele zadań wykonywanych przez programistów. W tym celu zapewnia abstrakcję wielu szczegółów cyklu żądania i odpowiedzi HTTP. Jest to zwykle korzystne — nie chcemy zajmować się każdym szczegółem protokołu CGI. Jednak ważne jest, aby ta abstrakcja nie spowodowała zboczenia z ścieżki pisania bezpiecznego kodu. Jedną z podstawowych zasad, o których powinniśmy pamiętać, jest to, że nigdy nie powinniśmy ufać danym wysłanym do nas przez przeglądarkę (klienta). Jest to jeden z obszarów, gdzie wygodna abstrakcja zapewniana przez Rails może zaszkodzić. Naprawdę opłaca się zapoznać z tym, jak działa HTTP, co najmniej do momentu, w którym wiemy, czy dany fragment danych pochodzi od klienta, z aplikacji lub ze środowiska. Nigdy nie wolno ufać danym pochodzącym od klienta, ponieważ może on wysłać do nas dowolne dane. Może wstawić fałszywe nagłówki, dodatkowe parametry, nieprawidłowo zbudowane ciągi zapytania i wiele innych. Poniżej zamieszczona jest krótka lista elementów, którym nie można ufać. Nie jest to kompletna lista, ale powinna zmusić do myślenia1. • Parametry formularza (ciąg zapytania i dane POST) — najczęściej spotykaną pomyłką
jest ufanie parametrom formularza przekazanym w żądaniu HTTP. Problem ten zostanie omówiony dalszej części tego rozdziału. • Cookies (jednak później przedstawimy wyjątek).
1
Oczywiście są to tylko wrażliwe elementy z poziomu protokołu HTTP. Później przedstawimy, jakie zagrożenia mogą pojawić się na wyższych poziomach. Zagrożenia na niższych poziomach, na przykład przejęcie sesji TCP, zwykle nie są problemem programisty.
132
|
Rozdz ał 5. Bezp eczeństwo
2
• Nagłówek Referer , który zawiera URI strony, z której nastąpiło odwołanie do strony bie-
żącej. Został on pomyślany jako pomoc dla webmasterów przy śledzeniu zerwanych łączy. Stosowanie go do uwierzytelniania lub zabezpieczeń jest zupełnym nieporozumieniem.
• Nagłówek User-Agent, który w założeniu ma zawierać nazwę oprogramowania klienta,
za pomocą którego otwarliśmy stronę. Podobnie jak Referer, jest wykorzystywany do analizy dzienników i nigdy nie powinien być wykorzystywany w zabezpieczeniach. Jako przykład przedstawimy jeden ze słabych projektów pod względem bezpieczeństwa, pochodzący z innej platformy. PHP posiada opcję konfiguracji, register_globals, której ustawienie może spowodować poważne problemy z bezpieczeństwem. Po włączeniu tej opcji zmienne z ciągu zapytania są automatycznie dodawane do globalnej przestrzeni nazw. Nudnym, ale pedagogicznym przykładem jest kod uwierzytelniania użytkownika, który wyświetla tajne informacje w zależności od poziomu dostępu użytkownika: } ?> ...
Jeżeli parametr register_globals jest włączony, można odwołać się do strony za pomocą index.php?user_id=4 i zmienna $user_id zostanie zainicjowana wartością 4 z ciągu polecenia. Ponieważ zakładamy, że funkcja authenticated() zwróci false (ponieważ nie jest to prawidłowy użytkownik), instrukcja if jest pomijana. Gdy przejdziemy do fragmentu kodu z tajną częścią, zmienna $user_id ma nadal wartość 4 i tajna część jest wyświetlana, pomimo że użytkownik nie był nigdy uwierzytelniony. Projekt ten prowadzi do poważnych problemów z bezpieczeństwem, ponieważ użytkownik może potencjalnie zmienić wartość każdej zmiennej lokalne, która nie została jawnie ustawiona. Na szczęście dla wszystkich opcja register_globals od pewnego czasu jest domyślnie wyłączona w PHP.
Przetwarzanie formularza W Rails mamy podobny problem z powodu kompromisu pomiędzy bezpieczeństwem i zwięzłością kodu. Rails obsługuje masowe przypisywanie parametrów formularza do obiektów ActiveRecord, dzięki czemu pola formularza o nazwach person[first_name], person[last_ ´name] oraz person[email] mogą zostać użyte do zbudowania obiektu ActiveRecord Person przy użyciu jednego wiersza kodu: person = Person.new params[:person]
2
Tak, jest to napisane z błędem, ale błąd ten tkwi zbyt głęboko w historii HTTP, aby go teraz zmieniać. Uznajmy to za lekcję dla projektantów protokołów.
Problemy w apl kacj
|
133
Obiekt params[:person] jest atrybutem tablicowym odwzorowującym nazwy na wartości, przekształcającym parametry formularza z użyciem ActionController: {:first_name => "Jan", :last_name => "Kowalski", :email => "[email protected]"}
Parametry te są przypisywane do obiektu Person poprzez wywołanie odpowiednio Person# ´first_name=, Person#last_name= i Person#email=. To samo przypisanie można wykonać indywidualnie: person = Person.new person.first_name = params[:person][:first_name] person.last_name = params[:person][:last_name] person.email = params[:person][:email]
Jest to więc wygodny skrót, ale tworzy słaby punkt. Załóżmy, że ktoś wyśle formularz z polem o nazwie person[access_level]. Pamiętamy, że wysłane wartości nie muszą mieć nic wspólnego z formularzem. Domyślnie spowoduje to wywołanie Person#access_level= z wartością przekazaną w formularzu. Jasne jest, że musimy się przed tym bronić. Możemy skorzystać z metod klasowych attr_protected lub attr_accessible z ActiveRecord::Base. Metoda attr_protected pozwala określić, który atrybut może być przypisany z użyciem operacji masowego przypisania: class Person < ActiveRecord::Base attr_protected :access_level end
Metoda attr_accessible ma odwrotne działanie i definiuje, które atrybuty mogą być ustawiane za pomocą masowego przypisania; wszystkie atrybuty nieznajdujące się na tej liście są blokowane. Jest to zalecane, jeżeli lista atrybutów może się zmieniać, ponieważ reprezentuje to filozofię „domyślnego zabraniania”. Jeżeli do modelu zostaną dodane nowe atrybuty, będą one domyślnie blokowane. class Person < ActiveRecord::Base attr_accessible :first_name, :last_name, :email end
Ukryte pola formularza Rails upraszcza operacje CRUD (create, read, update, delete) na pojedynczym obiekcie modelu, przez co bardzo łatwo zignorować konsekwencje bezpieczeństwa. Poniżej przedstawiony jest przykład jak nie przetwarzać formularza. app/models/comment.rb class Comment < ActiveRecord::Base belongs_to :user end
app/views/comment/new.rhtml <% form_for :comment do |f| %> <%= f.hidden_field :user_id %> Comment: <%= f.text_field :comment %> <% end %>
app/controllers/comments_controller.rb class CommentsController < ApplicationController def new @comment = Comment.new :user_id => get_current_user() end
134
|
Rozdz ał 5. Bezp eczeństwo
def create # Ostrożnie! @comment = Comment.create params[:comment] end end
Wygląda to dosyć niewinnie, ale zawiera jeden problem — ukryte pole jest zaufane! Przez zaniechanie weryfikowania wartości params[:comment][:user_id] otrzymanej w metodzie create pozwalamy każdemu utworzyć komentarz dołączony do dowolnego użytkownika. Rails może obsłużyć za nas tylko tyle. Obiekt params ma usuwane znaki specjalne CGI i jest rozdzielany do zagnieżdżonych tablic, ale na tym kończy się działanie frameworka. Przy każdym użyciu obiektu params musimy pamiętać, że może on zawierać dowolne wartości wybrane przez użytkownika. Jeżeli potrzebujemy silniejszej gwarancji zawartości obiektu params, musimy skorzystać z kryptografii. Implikacje tego problemu są ogromne — co pewien czas pewien sklep internetowy popada w kłopoty z powodu przechowywania cen w polach ukrytych formularza i niekontrolowania ich w czasie składania zamówienia przez użytkownika. Ktoś z minimalną wiedzą na temat HTTP może nauczyć te sklepy zasady „nie ufaj klientowi”, zamawiając sobie kilka telewizorów plazmowych po złotówce.
Kontrola poprawności po stronie klienta Nadrzędną zasadą przetwarzania formularzy jest przeprowadzanie kontroli poprawności na serwerze. Nie zmniejsza to wagi kontroli poprawności na kliencie, ale aplikacja musi być tak zaprojektowana, aby była bezpieczna, niezależnie od tego, co zostanie przesłane na poziomie HTTP. Kontrola poprawności danych na kliencie jest jak najbardziej prawidłowa. Jest to przydatne w przypadku popełnienia błędu przez użytkownika, ponieważ pozwala zaoszczędzić czas potrzebny na przesłanie danych do serwera. Jeżeli jednak jedynym elementem kontrolującym nieprawidłowe dane jest fragment JavaScript, to złośliwy użytkownik może po prostu wyłączyć JavaScript w przeglądarce i wysłać dane. Te dwie metody kontroli poprawności reprezentują dwie różne perspektywy. Kontrola poprawności na kliencie (jeżeli jest używana) powinna poprawiać użyteczność, natomiast kontrola poprawności na serwerze — zapewniać bezpieczeństwo.
Cookies W Rails zazwyczaj nie ma potrzeby stosowania cookies. Abstrakcja sesji zapewnia mechanizmy do przechowywania danych w sposób podobny do cookie, ale któremu można zaufać. Magazyn sesji jest zwykle umieszczony na serwerze i złączony z sesją użytkownika za pomocą identyfikatora przechowywanego w cookie. Ponieważ identyfikatory sesji są niewielkie i trudne do odgadnięcia, można bezpiecznie założyć, że jeżeli użytkownik prezentuje ID sesji, to ma on do niej dostęp. Ponieważ do magazynu sesji ma dostęp tylko kod aplikacji, można zaufać, że odczytane dane będą identyczne jak te, które zapisaliśmy. Istnieje również nowa metoda przechowywania danych sesji, CookieStore, która jest obecnie wykorzystywana w najnowszych wersjach Rails oraz w Rails 2.0. Szereguje ona wszystkie dane do cookie zamiast parować sesje serwerowe z cookie po stronie klienta. W założeniu większość sesji jest niewielka i zwykle zawiera tylko ID użytkownika oraz komunikat flash. Problemy w apl kacj
|
135
Do tego celu cookies świetnie się nadają (mają one zwykle 4-kilobajtowy limit wielkości). Rails zapewnia integralność danych przez podpisanie cookie za pomocą kodu uwierzytelniania wiadomości (MAC) i zgłaszania wyjątku TamperedWithCookie, jeżeli dane zostały zmodyfikowane.
Dwukrotne sprawdzanie wszystkiego Istnieje jeszcze jedna pomyłka, którą łatwo popełnić w Rails. Ponieważ filozofia REST, na której bazuje Rails, zakłada stosowanie URI bazujących na zasobach (każdy URI reprezentuje określony zasób lub obiekt), bardzo łatwo zapomnieć o bezpieczeństwie. Dzieje się to często podczas wyszukiwania rekordu w bazie danych z użyciem klucza głównego — często nie jest sprawdzana własność rekordu. Poniższy przykład ilustruje ten problem: app/models/message.rb class Message < ActiveRecord::Base belongs_to :user end
app/controllers/messages_controller.rb class MessagesController < ApplicationController def show @message = Message.find params[:id] end end
W tym przykładzie każdy może odczytać dowolne wiadomości, nawet te, których właścicielem jest inny użytkownik. W takim przypadku najlepiej ograniczyć przeglądanie wiadomości do tych, których dany użytkownik jest właścicielem. Prawidłowy sposób realizacji takiego odczytu jest następujący: def show @message = Message.find_by_user_id_and_id(current_user.id, params[:id]) end
Daje nam to ochronę przed użytkownikami podglądającymi wiadomości, które do nich nie należą, ponieważ w takim przypadku zgłaszany jest wyjątek RecordNotFound.
Bezpieczne wycofanie Obecnie wiele aplikacji Rails w pewnym stopniu korzysta z AJAX, więc ważnym problemem jest wycofanie. W zależności od potrzeb użytkownika odpowiednim określeniem może być łagodna degradacja (rozpoczynanie od witryny o pełnych możliwościach, a następnie jej testowanie i dostosowywanie do starszych przeglądarek) lub progresywne rozszerzanie (rozpoczęcie od minimalnego zbioru funkcji i ich dodawanie dla nowszych przeglądarek). W obu przypadkach programowanie dla starszych przeglądarek wymaga obsługi wycofania, czyli użycia mniej zalecanej opcji, jeżeli zalecana się nie powiedzie. Ważne jest, aby wycofanie było bezpieczne — w przeciwnym razie napastnik wymusi takie wycofanie w celu wykorzystania jego słabości. Typowym przykładem wycofania na stronie WWW jest użycie zwykłego przesłania w przypadku nieudanego wysłania danych formularza za pomocą JavaScript:
136
|
Rozdz ał 5. Bezp eczeństwo
Gdy JavaScript jest włączony, wywoływana jest funkcja do_ajax_submit() i standardowe wysłanie formularza jest anulowane. Zwykle funkcja ta wykonuje serializację parametrów, wysyła je na serwer i wykonuje kilka innych akcji. Przy wykorzystaniu metod Rails respond_to można użyć tych samych akcji zarówno dla standardowych odpowiedzi HTML, jak i JavaScript, rozróżnianych za pomocą nagłówka Accept. Nie można tu przytoczyć żadnych specjalnych porad na temat bezpieczeństwa poza tym, że należy przeglądać kod w celu upewnienia się, że napastnik nie będzie w stanie ominąć naszego systemu bezpieczeństwa przy użyciu metod niekorzystających z AJAX. Zazwyczaj metody AJAX są najnowsze, najlepiej wspierane i testowane. Poświęca się im najwięcej uwagi, ale równie ważne jest poświęcenie uwagi interfejsom niekorzystającym z AJAX.
Unikanie zabezpieczeń przez zaciemnienie Jedna z zasad bezpieczeństwa mówi, że zabezpieczenia przez zaciemnianie to żadne zabezpieczenia. Bezpieczeństwo powinno być wbudowane w system i nie można liczyć na ignorancję napastnika. Zasada ta jest związana z zasadą Kerckhoffa w kryptografii — zabezpieczenia systemu powinny kłamać wyłącznie w sprawie klucza (ale nie algorytmu). Zasada ta może być przekształcona na aplikacje WWW — nasza aplikacja powinna być tak zaprojektowana, że pozostanie bezpieczna nawet wtedy, gdy nasz kod źródłowy, architektura i konfiguracja (oczywiście z wyjątkiem haseł i podobnych danych) będą opublikowane. Nie oznacza to, że powinniśmy publikować ścieżki i architekturę systemu — nie ma potrzeby pomagać napastnikowi. Dogłębna ochrona (posiadanie wielu nadmiarowych warstw zabezpieczeń) jest również ważną zasadą. Nigdy nie powinniśmy polegać na tajemnicach dla zachowania bezpieczeństwa — ta zasada powinna być wiodąca.
Zabezpieczanie komunikatów błędów Komunikaty błędów mogą wiele powiedzieć na temat konfiguracji serwera. Nawet domyślne komunikaty błędów Apache mogą odkrywać częściowo poufne informacje poprzez wiersz z sygnaturą serwera: Apache/1.3.36 Server at www.przyklad.pl Port 80
Nie warto podawać na tacy tych danych potencjalnemu napastnikowi. Dodatkowo nagłówek HTTP Server często zawiera bardziej szczegółowe informacje, w tym listę wszystkich modułów serwera oraz ich wersji. Można ograniczyć te informacje do minimum przy użyciu dwóch wierszy w konfiguracji Apache. Należy umieścić je na początku pliku konfiguracyjnego: ServerSignature Off ServerTokens Prod
W Rails można również przypadkowo wyświetlić zapis stosu oraz kod źródłowy fragmentu, który spowodował błąd, o ile nie upewnimy się, że będzie można określić żądania „lokalne”. Domyślnie w trybie programowania atrybut konfiguracyjny ActionController:: ´Base.consider_all_requests_local jest ustawiony na true. Oznacza to, że każdy nieprzechwycony wyjątek spowoduje wyświetlenie zapisu stosu (z kodem źródłowym), niezależnie od źródłowego adresu IP. Jest to przydatne przy programowaniu, ale niebezpieczne, jeżeli nasz serwer będzie dostępny w publicznym internecie. W trybie produkcyjnym dyrektywa consider_all_requests_local jest domyślnie wyłączona.
Problemy w apl kacj
|
137
Można nadpisać domyślną funkcję local_request? z ApplicationController i utworzyć dowolnie skomplikowane reguły określające żądania lokalne (na przykład publiczny adres internetowy komputera, na którym tworzymy aplikację): class ApplicationController LOCAL_ADDRS = %w(123.45.67.89 98.76.54.32) def local_request? LOCAL_ADDRS.include? request.remote_ip end end
W każdym z przypadków należy na publicznych serwerach spróbować wygenerować kilka wyjątków w tymczasowej akcji, takiej jak następująca: class UserController < ApplicationController def blam raise "Jeżeli można to przeczytać, serwer jest źle skonfigurowany!" end end
Wyjątek ten powinien być przechwycony i zarejestrowany w dzienniku programisty, ale klient powinien zobaczyć wyłącznie stronę błędu 500 Internal Server Error.
Udostępniaj, nie blokuj Zgodnie z ogólną zasadą bezpieczeństwa sieciowego białe listy (listy z elementami do udostępniania) są bardziej bezpieczne niż czarne listy (listy z elementami do zablokowania). Zasada ta wynika z filozofii domyślnego blokowania. Białe listy są efektem ostrożniejszego podejścia, w którym zakłada się, że każde nieznane działanie jest niebezpieczne. Zed Shaw, twórca serwera WWW Mongrel, jest zapalonym zwolennikiem tej filozofii3. Większość zabezpieczeń serwera Mongrel działa na zasadach domyślnego blokowania. W przypadku wykrycia naruszenia protokołu Mongrel zamyka gniazdo, zamiast próbować zgadnąć, co klient miał na myśli (być może próbując znaleźć słaby punkt procesu). Do tego zagadnienia wrócimy w punkcie „Kanonizacja — co to za nazwa?”, zamieszczonym w dalszej części rozdziału.
Problemy w sieci WWW Po zapoznaniu się z elementami architektury pozwalającymi na ochronę aplikacji przedstawimy kilka problemów związanych wyłącznie z siecią WWW.
Sesje Rails Większość frameworków sieciowych obsługuje pewną formę zarządzania sesjami — mechanizm magazynu po stronie serwera przechowujący dane specyficzne dla sesji przeglądania. Dokładny zakres „sesji przeglądania” zależy od szczegółów implementacji oraz metod śledzenia sesji. Najczęściej wykorzystywane są nietrwałe cookies, więc sesja obejmuje wszystkie wizyty na serwerze, aż do zamknięcia przeglądarki. Stosowane są również trwałe cookies 3
(z ustawioną datą wygaśnięcia) — w takim przypadku sesje mogą trwać również po zamknięciu przeglądarki. Jest to przydatne do zachowywania danych (takich jak zawartość koszyka) pomiędzy kolejnymi wizytami anonimowego użytkownika. Niektóre frameworki, na przykład Seaside, obsługują sesje bazujące na adresach URL (ciągu zapytania), dzięki czemu użytkownik może mieć kilka aktywnych sesji jednocześnie, otwartych w różnych oknach przeglądarki. Większość metod przechowywania danych sesji w Rails zapewnia następujące własności: Poufność Nikt oprócz serwera nie może odczytać danych zapisanych w sesji. Spójność Nikt oprócz serwera, w tym nawet klient, nie może modyfikować danych zapisanych w sesji w inny sposób niż przez usunięcie bieżącej sesji i otwarcie nowej. Wynika z tego wniosek, że tylko serwer powinien móc tworzyć prawidłowe sesje. Tradycyjne metody przechowywania danych sesji w Rails działają na serwerze — zapisują dane sesji na serwerze, generują losowy klucz i wykorzystują go jako identyfikator sesji. Identyfikator sesji nie jest związany z danymi innymi niż indeks, więc można go bezpiecznie prezentować w kliencie bez naruszania poufności. Rails przy tworzeniu identyfikatora sesji korzysta z kilku elementów losowych — używany jest odcisk MD5 bieżącego czasu, liczba losowa, identyfikator procesu, a dodatkowo stała. Jest to bardzo ważne, ponieważ możliwe do odgadnięcia identyfikatory sesji pozwalają na przejęcie sesji. W przypadku próby przejęcia sesji napastnik podsłuchuje lub zgaduje identyfikator uwierzytelnionego użytkownika i prezentuje go serwerowi jako własny, przez co może on działać identycznie jak uwierzytelniony użytkownik. Jest to podobne do ataku przewidywania numeru sekwencji w TCP i zabezpieczenie jest takie samo — identyfikator sesji powinien być jak najbardziej losowy.
Sesje bazujące na cookie Przechowywanie danych sesji na serwerze niesie ze sobą wiele problemów. Magazyny bazujące na plikach, w których sesje są szeregowane w lokalnym pliku serwera, nie są skalowalne — aby sesje takie mogły działać w klastrze, wymagane jest zastosowanie współdzielonego systemu plików lub wyrównywacz obciążenia obsługujący klejące sesje (kierujący wszystkie żądania z sesji do jednego serwera klastra). Może to być skomplikowane i często nieefektywne. Sesje przechowywane w bazie danych rozwiązują ten problem przez zapisywanie stanu sesji w centralnej bazie danych. Jednak ta baza danych może szybko stać się wąskim gardłem, ponieważ musi być używana w każdym żądaniu wymagającym danych sesji (na przykład dla uwierzytelnienia użytkownika). Magazyn sesji DRb nie jest powszechnie używany i wymaga uruchomienia jeszcze jednego procesu serwera. Istnieją jednak rozwiązania tego problemu. Większość sesji Rails jest lekka — zwykle zawiera niewiele więcej niż ID użytkownika (jeżeli jest uwierzytelniony) i być może krótki komunikat. Oznacza to, że mogą być one przechowywane na kliencie zamiast na serwerze. Zapewnia to CookieStore z Rails — zamiast przechowywania sesji na serwerze i przechowywania jej ID w cookie klienta, przechowywana jest w nim cała sesja.
Problemy w s ec WWW
|
139
Oczywiście, bezpieczeństwo musi być zachowane. Należy pamiętać o najważniejszej zasadzie — nigdy nie należy ufać klientowi. Jeżeli wykonamy szeregowanie danych do ciągu znaków i umieścimy je w cookie, nie będziemy w stanie uniemożliwić klientowi manipulowanie sesją. Użytkownik może po prostu wysłać cookie odpowiadające danym sesji dla user_id=1 i oszukać serwer, udając zalogowanego użytkownika z identyfikatorem 1. Aby temu zapobiec, CookieStore podpisuje każde cookie za pomocą HMAC (kod uwierzytelniający), który jest w istocie odciskiem danych cookie razem z tajnym kluczem. Klient nie może wykorzystać ani zmodyfikować sesji, ponieważ nie może wygenerować prawidłowej sygnatury dla zmodyfikowanych danych. Serwer sprawdza odcisk przy każdym żądaniu i zgłasza wyjątek TamperedWithCookie, jeżeli odcisk nie odpowiada danym. Jest to standardowa metoda przechowywania danych na niezaufanym kliencie z zachowaniem spójności. W Rails 2.0 CookieStore jest teraz domyślnym magazynem sesji. CookieStore wymaga zdefiniowania tajnego klucza lub frazy oraz klucza cookie sesji — jeżeli nie zostanie zdefiniowana którakolwiek z tych danych, zgłaszany jest wyjątek. Opcje te są ustawiane obok innych parametrów sesji w pliku config/environment.rb: config.action_controller.session = { :session_key => "_myapp_session", :secret => "Wygląda jak łasica." }
Mechanizm CookieStore ma kilka ograniczeń: • W większości przypadków cookie mają ograniczoną wielkość do 4 kB. CookieStore zgłasza
wyjątek CookieOverflow, jeżeli dane oraz HMAC przekroczą ten limit. Nie jest to błąd, na jaki chcemy napotykać w środowisku produkcyjnym (ponieważ do naprawienia wymaga zmian architektury), więc należy upewnić się, że dane sesji będą mniejsze niż ten limit. • Cała sesja oraz HMAC są wyliczane, przesyłane i weryfikowane w każdym żądaniu i od-
powiedzi. Mechanizm CookieStore jest na tyle inteligentny, że nie przesyła cookie, jeżeli jego dane nie zmieniły się od ostatniego żądania, ale klient musi wysyłać cookie w każdym żądaniu. • W przeciwieństwie do magazynów sesji umieszczonych na serwerze CookieStore po-
zwala na odczyt wszystkich danych przez klienta. Zwykle nie jest to problem, ale w niektórych przypadkach problem może powstać. Niektóre aplikacje wymagają, aby wrażliwe dane użytkownika (takie jak numery kont lub kart kredytowych) były ukryte, nawet po zalogowaniu użytkownika, aby zapewnić lepsze bezpieczeństwo. Należy pamiętać, że dane te będą zapisane jako czysty tekst w buforze przeglądarki na kliencie. Dane wrażliwe powinny być przechowywane na serwerze, a nie w sesji. • Mechanizm CookieStore jest wrażliwy na ataki odtworzenia — ponieważ cookie nie za-
wiera wartości jednorazowej4, użytkownik, który posiada prawidłową sesję, może ją odtworzyć w późniejszym czasie i oszukać serwer. Nigdy nie należy przechowywać w sesji danych zmiennych, takich jak bilans konta.
4
Wartość jednorazowa (ang. nonce) jest liczbą losową generowaną przez serwer, którą klient musi dołączać do żądania. Ponieważ wartość ta jest inna w każdym żądaniu, serwer może upewnić się, że to samo żądanie nie zostało przesłane dwa razy.
140
|
Rozdz ał 5. Bezp eczeństwo
Skrypty Cross Site Skrypty Cross Site (ang. Cross Site Scripting, czyli XSS, w odróżnieniu od Cascading Style Sheets oraz Content Scramble System) to jedno z najczęściej występujących narażeń ostatnio tworzonych aplikacji. Aplikacje w stylu „Web 2.0” są na nie szczególnie narażone z powodu przesunięcia nacisku na treści generowane przez użytkowników. Atak XSS jest zwykle możliwy z powodu nieodpowiedniego przetwarzania kodu wprowadzonego przez użytkownika, szczególnie w postach na blogach, komentarzach lub pozostałych treściach generowanych przez użytkowników. W przypadku ataku XSS napastnik wstawia kod, najczęściej w JavaScript, do obcej witryny (celu) w taki sposób, aby przeglądarka traktowała go jako część strony. W wielu przypadkach jest to korzystne — blog pozwala użytkownikom na komentowanie wpisów, w niektórych przypadkach z wykorzystaniem HTML. Może to spowodować powstanie słabego punktu — jeżeli znaczniki skryptu nie zostaną odfiltrowane przed wyświetleniem zawartości, będą one przedstawione użytkownikowi jako część witryny. Pozwala to pominąć zasady bezpieczeństwa przeglądarki, ponieważ przeglądarki ograniczają uprawnienia skryptów na podstawie pochodzenia kodu. Jeżeli kod wydaje się pochodzić z docelowej witryny, może odwoływać się do danych (np. cookies) należących do tej witryny.
Zabezpieczenia Obrona przed narażeniami XSS może być bardzo prosta lub bardzo skomplikowana. Jeżeli zabezpieczana aplikacja nie pozwala niezaufanym użytkownikom na wprowadzanie HTML, obrona przed XSS jest prosta. W takim przypadku każdy znak HTML musi być oznaczony przed wyświetleniem. Metoda Rails, h() oznacza dla nas wszystkie specjalne znaki HTML: <% @post.comments.each do |comment| %>
<%=h comment.text %>
<% end %>
Przy okazji — można spotkać się z dyskusją na temat tego, czy treść, która musi być oznaczana przed wyświetleniem, musi być zapamiętywana w postaci czystego tekstu czy oznaczonego. Zaletą przechowywania oznaczonych danych jest brak możliwości pominięcia procesu oznaczania przed wyświetleniem, natomiast zaletą przechowywania w postaci oryginalnej jest „naturalny” stan danych. Cal Henderson bardzo dobrze podsumował ten problem — nigdy nie wiemy, kiedy będziemy musieli oznaczyć dane w inny sposób przed wyświetleniem, więc dane powinny być przechowywane w postaci czystego tekstu. Wyjątkiem może być Unicode — zwykle ważne jest upewnienie się, że wszystkie dane zapisane w Unicode są prawidłowe, ponieważ powoduje to mniej problemów z przetwarzaniem. W takich sytuacjach zwykle najlepiej sprawdzać poprawność zarówno na wejściu, jak i na wyjściu. Oczywiście, nigdy nie będzie tak łatwo. Powodem, dla którego ataki XSS są tak często spotykane, jest pomoc niezaufanym użytkownikom w celu wprowadzenia dowolnych znaczników HTML, a nie tylko zabronionych znaczników i zabronionych atrybutów — tych, które mogą wykonywać skrypty. Odfiltrowanie ich jest trudniejsze, niż może się wydawać (patrz punkt „Kanonizacja — co to za nazwa?” w dalszej części rozdziału). Rails zapewnia pomoc, udostępniając metodę sanitize(). Metoda ta usuwa znaczniki formularza i skryptu, skracając atrybuty onOperacja i wycinając wszystkie URI o schemacie „javascript:”.
Problemy w s ec WWW
|
141
Domyślny zbiór zabronionych znaczników i atrybutów może być wystarczający do zablokowania dowolnego skryptu. Metody tej używa się podobnie jak h(): <% @post.comments.each do |comment| %>
<%=sanitize comment.html %>
<% end %>
Jednak należy bardzo, ale to bardzo uważać przy korzystaniu z tego typu blokowania. Istnieje zbyt wiele przypadków brzegowych, aby być całkowicie pewnym, że każdy element potencjalnie niebezpiecznego kodu jest zablokowany. Eksperci bezpieczeństwa Rails odradzają korzystanie z blokowania5.
Białe listy Stosowanie białych list jest zalecane. Rick Olson opracował wtyczkę white_list (http://svn.techno´weenie.net/projects/plugins/white_list), która obecnie stała się zalecana przy zabezpieczaniu przed atakami skryptów międzywitrynowych. Korzysta ona z bardziej solidnej filozofii (dopuszczamy tylko to, co jest jawnie udostępnione) i jest znacznie lepiej przetestowana niż moduły pomocnicze czarnych list. Podstawowe zastosowanie jest bardzo podobne do innych metod zabezpieczania — po zainstalowaniu wtyczki filtr udostępniający może być stosowany w następujący sposób: <%= white_list @post.body %>
Wtyczka white_list posiada domyślny zbiór udostępnionych znaczników, atrybutów i schematów URI, a znacznik