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

Migracje

Migracje są wygodnym sposobem, by w zorganizowany sposób modyfikować bazę danych. Mógłbyś oczywiście edytować fragmenty kodu SQL ręcznie, ale musiałbyś potem samemu zatroszczyć się o to, by poinformować innych projektantów o konieczności wykonania potrzebnych zmian. Musiałbyś też pilnować jakie zmiany należy wprowadzić na serwerze, na którym działa ostateczna wersja tworzonego serwisu.

Moduł Active Record śledzi migracje, które zostały już wykonane, więc jedyne co musisz wykonać ze swojej strony, to wprowadzenie zmian w kodzie i wykonanie polecenia rake db:migrate. Moduł Active Record sam zdecyduje, które migracje powinny zostać wykonane. Zaktualizuje on też schemat bazy danych zawarty w pliku db/schema.rb tak, by odpowiadał on aktualnej strukturze Twojej bazy.

Migracje pozwalają na opisanie zmian bazy danych w Rubim. Dzięki temu, podobnie jak większość funkcji modułu Active Record, nie zależą one od używanego przez Ciebie systemu zarządzania bazą danych. Nie musisz więc martwić się o dokładną składnię komendy CREATE TABLE ani zastanawiać się nad różnymi wariacjami polecenia SELECT *. Możesz na przykład użyć SQLite3 podczas tworzenia, a MySQL w gotowym, udostępnionym serwisie.

W poniższym przewodniku dowiesz się wszystkiego o migracjach. W szczególności, tekst opisuje:

1 Anatomia migracji

Zanim zagłębimy się w detale migracji, przedstawiam kilka przykładów ilustrujących ich możliwości:

class CreateProducts < ActiveRecord::Migration def self.up create_table :products do |t| t.string :name t.text :description t.timestamps end end def self.down drop_table :products end end

Ta migracja tworzy tabelę products z kolumną o nazwie name typu string oraz kolumną tekstową description. Zostanie również utworzona domyślna kolumna ID, bedąca kluczem głównym tabeli, ale ponieważ jest to domyślna kolumna każdej tabeli, nie musimy jej nawet definiować. Kolumny zawierające znaczniki czasu (timestamp) created_at i updated_at również zostaną automatycznie dodane przez moduł Active Record. Odwrócenie takiej migracji to po prostu usunięcie tabeli.

Możliwości migracji nie ograniczają się do zmiany schematu bazy danych. Przy ich pomocy można także poprawić błędne dane lub uzupełniać nowe pola:

class AddReceiveNewsletterToUsers < ActiveRecord::Migration def self.up change_table :users do |t| t.boolean :receive_newsletter, :default => false end User.update_all ["receive_newsletter = ?", true] end def self.down remove_column :users, :receive_newsletter end end

Ta migracja dodaje kolumnę receive_newsletter do tabeli users. Pole to mówi nam o przypisaniu użytkownika na listę odbiorców aktualności. Chcemy, by domyślnie zawierało ono wartość false dla nowych użytkowników, ale użytkownicy juz zarejestrowani są zgłoszeni do otrzymywania aktualności, więc możemy użyć modelu User do ustawienia dla nich flagi reveive_newsletter jako true.

Przeczytaj uwagi dotyczące używania modeli w migracjach.

1.1 Migracje są klasami

Migracja jest podklasą klasy ActiveRecord::Migration, która posiada zaimplementowane dwie metody: up (wykonaj żądane transformacje) i down (wycofaj je).

Moduł Active Record udostępnia metody, służące do typowych operacji na bazach danych w sposób niezależny od typu używanej bazy (ich dokładniejszy opis będzie zamieszczony w dalszej części):

  • create_table
  • change_table
  • drop_table
  • add_column
  • remove_column
  • change_column
  • rename_column
  • add_index
  • remove_index

Jeśli chcesz wykonać zadanie specyficzne tylko dla Twojego typu bazy danych (np. utworzyć klucz obcy), możesz do tego celu wykorzystać funkcję execute, która umożliwia Ci wykonanie kodu SQL. Migracja jest zwykłą klasą Ruby, więc nie musisz ograniczać się do tych funkcji. Przykładowo, możesz po dodaniu kolumny dopisać kod odpowiedzialny za przypisanie jej odpowiedniej wartości we wszystkich istniejących rekordach.

W bazach danych obsługujących transakcje zmieniające schemat bazy danych (takich jak PostgreSQL), migracje są realizowane jako transakcje. Jeśli baza danych nie posiada takiej funkcjonalności (np. MySQL i SQLite), w przypadku niepowodzenia migracji, etapy już wykonane nie zostaną automatycznie wycofane. Wymaga to ręcznego usunięcia już wprowadzonych zmian.

1.2 Co kryje się w nazwie

Każda klasa migracji jest przechowywana w oddzielnym pliku w katalogu db/migrate. Nazwa pliku posiada format YYYYMMDDHHMMSS_create_products.rb, zawierającym znacznik czasowy (w czasie UTC) oraz – po podkreślniku – nazwę migracji. Nazwa taka musi się zgadzać z migracją, którą plik zawiera, np: 20080906120000_create_products powinna definiować migrację CreateProducts, a 20080906120001_add_details_to_products – migrację AddDetailsToProducts. Jeśli z jakichś przyczyn zmienisz nazwę pliku, MUSISZ poprawić także nazwę klasy – w przeciwnym wypadku Railsy będą informowały Cię o braku wymaganej klasy.

Railsy wykorzystują jedynie numer migracji (czyli jej znacznik czasowy) do jej identyfikacji. Wersje wcześniejsze niż 2.1 numerowały migracje liczbami naturalnymi począwszy od 1, przypisując każdej nowej migracji kolejną liczbę. W przypadku pracy w zespołach kolizje oznaczeń były nieuniknione, co wymagało wycofania zmian i przenumerowania wszystkich migracji. Od wersji 2.1 rozwiązano ten problem identyfikując migrację poprzez datę jej utworzenia. Można przywrócić starszy sposób numeracji, ustawiając w pliku environment.rb zmienną config.active_record.timestamped_migrations na wartość false.

Dzięki nowemu sposobowi numeracji oraz monitorowaniu które migracje już zostały wykonane, Railsy rozwiązują wiele częstych problemów powstających przy pracy zespołowej nad jednym projektem.

Przykładowo, Ania tworzy migracje 20080906120000 i 20080906123000, a Bartek tworzy 20080906124500 i ją wykonuje. Ania kończy wprowadzać zmiany, a Bartek wycofuje swoją migrację. Railsy wiedzą, że migracje Ani nie zostały wykonane, więc rake db:migrate wykona je (pomimo tego, że późniejsza migracja stworzona przez Bartka już została wykonana). Analogicznie, polecenie wycofania migracji pominie niewykonane jeszcze migracje Ani.

Oczywiście nie zastąpi to w pełni komunikacji wewnątrz zespołu. Na przykład, jeśli migracja Ani usunęła tabelę wykorzystywaną przez migrację Bartka, problem i tak wystąpi.

1.3 Zmiany w migracjach

Czasem popełnisz błąd podczas tworzenia migracji. Jeśli już ją wykonałeś, nie możesz po prostu jej wyedytować i wykonać ją ponownie: Railsy uznają, że została już wykonana i kolejne wpisanie komendy rake db:migrate nie zaowocuje wprowadzeniem poprawek. Musisz w tym celu wycofać migrację (np. komendą rake db:rollback), poprawić błędy w migracji i ponownie ją wykonać poleceniem rake db:migrate.

Ogólnie rzecz biorąc, edycja istniejących migracji nie jest dobrym pomysłem: dodajesz tym samym pracy sobie i swoim współpracownikom. Może to być przyczyną poważnych kłopotów, jeżeli Twoja migracja została już uruchomiona na docelowym serwerze. Lepszym rozwiązaniem jest stworzenie nowej migracji, która wprowadzi wymagane zmiany. Edycja świeżo utworzonej migracji, która nie została jeszcze przekazana na serwer docelowy jest stosunkowo bezpieczna.

2 Tworzenie migracji

2.1 Tworzenie modelu

Generatory modelu i rusztowania (scaffold) utworzą migrację odpowiednią dla tworzonego modelu. Migracja taka zawiera od razu instrukcje potrzebne do utworzenia powiązanej z danym modelem tablicy. Jeśli powiesz Railsom jakie kolumny powinny znaleźć się w tej tabeli, do migracji zostaną dodane odpowiednie formuły. Przykładowo, wykonanie polecenia

ruby script/generate model Product name:string description:text

spowoduje stworzenie następującej migracji:

class CreateProducts < ActiveRecord::Migration def self.up create_table :products do |t| t.string :name t.text :description t.timestamps end end def self.down drop_table :products end end

Możesz dołączyć tyle par kolumna/typ ile potrzebujesz. Domyślnie zostaną dodane znaczniki czasu t.timestamps (które powodują automatyczne utworzenie przez moduł Active Record kolumn updated_at i created_at).

2.2 Tworzenie samodzielnej migracji

Jeśli tworzysz migracje dla innych celów (np. aby dodać kolumnę do istniejącej już tabeli), możesz skorzystać z generatora migracji:

ruby script/generate migration AddPartNumberToProducts

Ta komenda utworzy pustą, lecz odpowiednio nazwaną i skonstruowaną migrację:

class AddPartNumberToProducts < ActiveRecord::Migration def self.up end def self.down end end

Jeśli nazwa podana w poleceniu generatora migracji jest formatu AddXXXToYYY lub RemoveXXXFromYYY i po niej wymieniona jest lista kolumn i ich typów, to w wyniku utworzona zostanie migracja z odpowiednimi poleceniami dodania/usunięcia kolumny, przykładowo:

ruby script/generate migration AddPartNumberToProducts part_number:string

wygeneruje migrację:

class AddPartNumberToProducts < ActiveRecord::Migration def self.up add_column :products, :part_number, :string end def self.down remove_column :products, :part_number end end

Analogicznie, komenda

ruby script/generate migration RemovePartNumberFromProducts part_number:string

generuje

class RemovePartNumberFromProducts < ActiveRecord::Migration def self.up remove_column :products, :part_number end def self.down add_column :products, :part_number, :string end end

Przy tworzeniu migracji nie musisz ograniczać się do jednej kolumny, na przykład:

ruby script/generate migration AddDetailsToProducts part_number:string price:decimal

wygeneruje migrację:

class AddDetailsToProducts < ActiveRecord::Migration def self.up add_column :products, :part_number, :string add_column :products, :price, :decimal end def self.down remove_column :products, :price remove_column :products, :part_number end end

Jak zwykle, wygenerowana migracja jest dopiero punktem startowym Twojej pracy. Możesz ją dalej modyfikować, dodając lub usuwając elementy.

3 Pisanie migracji

Jeśli już utworzyłeś migrację przy pomocy jednego z generatorów, najwyższy czas zabrać się do pracy!

3.1 Tworzenie tabeli

create_table jest jedną z najbardziej podstawowych komend. Jej typowe użycie ilustruje przykład:

create_table :products do |t| t.string :name end

Przykład ten generuje tabelę products z kolumną o nazwie name (i, jak to omówimy w dalszej części, domyślną kolumną id).

Obiekt przekazany (yielded) do bloku pozwala na tworzenie kolumn tabeli. Są na to dwa sposoby. Pierwszy z nich wygląda tak:

create_table :products do |t| t.column :name, :string, :null => false end

Drugi sposób, tzw. “seksowne” migracje, nie korzysta z nadmiarowej metody column. Zamiast niej wykorzystać można metody string, integer itp., które tworzą kolumny odpowiednich typów. Pozostałe parametry są identyczne do parametrów metody column.

create_table :products do |t| t.string :name, :null => false end

Domyślnie create_table stworzy klucz główny nazwany id. Możesz zmienić jego nazwę korzystając z opcji :primiary_key (nie zapomnij poprawić powiązanego z tabelą modelu) lub, jeśli nie chcesz w ogóle klucza głównego (np. dla tabeli HABTM, realizującej relację typu wiele-do-wielu) możesz dodać id => false. Jeśli chcesz dodać opcję specyficzną dla konkretnego typu bazy danych, możesz umieścić fragment kodu SQL wewnątrz parametru :options. Na przykład:

create_table :products, :options => "ENGINE=BLACKHOLE" do |t| t.string :name, :null => false end

Opcja ta doda ENGINE=BLACKHOLE do kodu SQL użytego do tworzenia tabeli (dla MySQL domyślny parametr to “ENGINE=InnoDB”).

Typy obsługiwane przez moduł Active Record to :primary_key, :string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean.

Zostaną one zrealizowane poprzez odpowiednie dla bazy danych typy danych, np. w MySQL :string zostanie przetłumaczony na VARCHAR(255). Możesz tworzyć kolumny innych niż wymienione typów pod warunkiem, że nie używasz “seksownej” składni, przykładowo:

create_table :products do |t| t.column :name, 'polygon', :null => false end

Może to jednak uniemożliwić poprawne przeniesienie migracji na inny typ bazy danych.

3.2 Zmienianie tabel

Bliskim kuzynem create_table jest change_table. Komenda ta służy do zmiany istniejących już tabel i używana jest w analogiczny sposób jak create_table, jednak w jej wypadku obiekt przekazany do bloku daje o wiele większe możliwości. Przykładowo:

change_table :products do |t| t.remove :description, :name t.string :part_number t.index :part_number t.rename :upccode, :upc_code end

Ta komenda usuwa kolumnę description, dodaje kolumnę part_number i tworzy na niej indeks. Ostatnia metoda powoduje zmianę nazwy kolumny upccode na upc_code. Ten sam efekt można osiągnąć również następującym sposobem:

remove_column :products, :description remove_column :products, :name add_column :products, :part_number, :string add_index :products, :part_number rename_column :products, :upccode, :upc_code

Stosując pierwszą składnię nie musisz powtarzać przy każdej komendzie nazwy tabeli – grupujesz wszystkie wykonane na niej operacje w jedną komendę. Dodatkowo nazwy transformacji są krótsze: remove_column zastępuje samo remove a add_index – po prostu index.

3.3 Specjalne helpery

Moduł Active Record zapewnia kilka skrótów do najczęściej używanych opcji. Bardzo częste jest na przykład dodawanie kolumn created_at i updated_at, więc stworzono specjalną metodę, która to ułatwia:

create_table :products do |t| t.timestamps end

Ten zapis spowoduje stworzenie nowej tabeli products z oboma wspomnianymi kolumnami, podczas gdy:

change_table :products do |t| t.timestamps end

spowoduje dodanie tych kolumn do istniejącej tabeli.

Inny helper nazywa się references (można też używać nazwy belongs_to). W najprostrzej postaci, poprawia on po prostu czytelność:

create_table :products do |t| t.references :category end

Ten zapis stworzy kolumnę category_id odpowiedniego typu. Zwróc uwagę na fakt, że w kodzie podajemy nazwę modelu, a nie kolumny. Moduł Active Record sam dodaje przyrostek _id. Jeśli potrzebujesz polimorficznych asocjacji, to references doda obie potrzebne kolumny:

create_table :products do |t| t.references :attachment, :polymorphic => {:default => 'Photo'} end

Ten zapis spowoduje dodanie kolumny attachment_id oraz drugiej kolumny attachment_type typu string, o domyślnej wartości Photo.

Helper references nie tworzy <<foreign_key,kluczy obcych>>. Aby je dodać, musisz użyć metody execute lub skorzystać z wtyczki poszerzającej funkcjonalność modułu Active Record.

Jeśli helpery udostępniane przez moduł Active Record nie wystarczają do Twoich potrzeb, możesz użyc funkcji execute by wywołać kod SQL. Aby zapoznać się z detalami i przykładami tworzenia własnych metod polecam dokumentację API, zwłaszcza ActiveRecord::ConnectionAdapters::SchemaStatements (opis metod dostępnych w metodach up i down), ActiveRecord::ConnectionAdapters::TableDefinition (metody dostępne na obiekcie przekazanym przy pomocy komendy create_table) i ActiveRecord::ConnectionAdapters::Table (metody na obiekcie przekazanym przez change_table).

3.4 Tworzenie metody down

Metoda down Twojej migracji powinna odwrócić transformacje wywołane przez metodę up. Innymi słowy, baza nie powinna ulec zmianie, jeśli wywołamy metodę up a po niej metodę down. Na przykład, jeśli w metodzie up tworzymy tabelę, to w metodzie down powinniśmy ją usunąć. Rozsądnie jest w metodzie down odwoływać wszystkie polecenia w dokładnie odwrotnej kolejności niż zostały one wywołane. Na przykład:

class ExampleMigration < ActiveRecord::Migration def self.up create_table :products do |t| t.references :category end #add a foreign key execute "ALTER TABLE products ADD CONSTRAINT fk_products_categories FOREIGN KEY (category_id) REFERENCES categories(id)" add_column :users, :home_page_url, :string rename_column :users, :email, :email_address end def self.down rename_column :users, :email_address, :email remove_column :users, :home_page_url execute "ALTER TABLE products DROP FOREIGN KEY fk_products_categories" drop_table :products end end

Czasem Twoja migracja wykona transformację, która jest nieodwracalna, na przykład usunie jakieś dane. W takim przypadku, możesz wywołać IrreverisbleMigration ze swojej metody down. W przypadku próby odwrócenia Twojej migracji wyświetlony zostanie komunikat informujący o braku możliwości cofnięcia migracji.

4 Wykonywanie migracji

Railsy zapewniają zestaw zadań Rake służących do działań na migracjach, sprowadzających się do wykonania określonych zestawów migracji. Prawdopodobnie najważniejszym zadaniem Rake jest db:migrate. W swojej najbardziej podstawowej formie wykonuje ona metodę up dla każdej migracji, która nie została jeszcze wywołana. Jeśli nie ma takich migracji, polecenie po prostu kończy swoje działanie.

Warto zauważyć, że wywołanie db:migrate powoduje również uruchomienie zadania db:schema:dump, które zmienia zawartość pliku db/schema.rb tak, by odpowiadał on aktualnej strukturze bazy.

Jeśli sprecyzujesz docelową wersję migracji, moduł Active Record wykona (lub odwoła) migracje wymagane do osiągnięcia żądanej przez Ciebie wersji. Pod pojęciem wersji rozumiemy prefiks nazwy pliku migracji, zawierający stempel czasowy jego utworzenia. Na przykład, aby migrować do wersji 20080906120000, wykonamy polecenie:

rake db:migrate VERSION=20080906120000

Jeśli podana wersja jest późniejsza niż obecna, polecenie spowoduje wywołanie metod up kolejnych migracji aż do migracji 20080906120000 włącznie. W przeciwnym wypadku, wszystkie późniejsze niż wskazana migracje zostaną wycofane metodą down.

4.1 Wycofywanie migracji

Częstym zadaniem jest wycofanie ostatniej migracji, na przykład po to, by poprawić znaleziony w niej błąd. Zamiast wpisywać numer poprzedniej wersji, możemy po prostu wycofać ostatnią zmianę poleceniem:

rake db:rollback

Wykonana zostanie metoda down ostatniej migracji. Jeśli potrzebujesz wycofać więcej migracji, możesz dodać parametr STEP:

rake db:rollback STEP=3

Tym sposobem wykonasz metodę down dla trzech ostatnich migracji.

Zadanie db:migrate:redo jest skrótowym zapisem wycofania i ponownego wykonania migracji. Podobnie jak w przypadku db:rollback, możesz dodać parametr STEP jeśli chcesz powtórzyć więcej niż jedną migrację, np:

rake db:migrate:redo STEP=3

Żadne z powyższych zadań Rake nie wykonuje niczego, czego nie można osiągnąć przy pomocy podstawowego polecenia db:migrate. Są to po prostu wygodniejsze sposoby pracy na migracjach, nie wymagają bowiem podawania wprost docelowej wersji migracji.

Ostatnim poleceniem jest db:reset. Służy ono do usunięcia całej bazy danych, ponownego jej stworzenia i przywrócenia obecnego schematu.

Polecenie to nie jest idenyczne z wprowadzeniem wszystkich migracji – szczegóły znajdziesz w sekcji schema.rb.

4.2 Wywoływanie konkretnej migracji

Jeśli chcesz wywołać metodę up lub down konkretnej migracji, możesz w tym celu użyć komend db:migrate:up i db:migrate:down. Wystarczy podać dokładną wersję migracji, a jej metoda up albo down zostanie wykonana. Na przykład

rake db:migrate:up VERSION=20080906120000

wywoła metodę up dla migracji 20080906120000. Zadanie to sprawdza najpierw czy podana migracja została wcześniej wykonana, więc np. komenda db:migrate:up VERSION=20080906120000 nie spowoduje żadnych zmian, jeśli moduł Active Record uzna, że ta migracja jest już wywołana.

4. Komunikaty

Domyślnie migracja informuje Cię dokładnie o wykonywanej obecnie transformacji oraz o czasie który był potrzebny do wykonania ukończonych zadań. Migracja tworząca tabelę i indeks może wygenerować na przykład taki komunikat:

20080906170109 CreateProducts: migrating -- create_table(:products) -> 0.0021s -- add_index(:products, :name) -> 0.0026s 20080906170109 CreateProducts: migrated (0.0059s)

Istnieje kilka metod służących do kontroli komunikatów zwrotnych:

  • suppress_messages zawiesza wszystkie komunikaty generowane przez dany blok
  • say wyświetla tekst (drugi parametr służy do określenia czy tekst ma być w nowej linii czy nie)
  • say_with_time wyświetla wskazany tekst oraz czas potrzebny na wykonanie bloku. Jeśli blok zwróci liczbę typu integer, metoda traktuje ją jako liczbę wierszy zmodyfikowanych przez ten blok.

Na przykład migracja:

class CreateProducts < ActiveRecord::Migration def self.up suppress_messages do create_table :products do |t| t.string :name t.text :description t.timestamps end end say "Created a table" suppress_messages {add_index :products, :name} say "and an index!", true say_with_time 'Waiting for a while' do sleep 10 250 end end def self.down drop_table :products end end

wygeneruje następujący komunikat:

20080906170109 CreateProducts: migrating -- Created a table -> and an index! -- Waiting for a while -> 10.0001s -> 250 rows 20080906170109 CreateProducts: migrated (10.0097s)

Jeśli chcesz po prostu zupełnie “uciszyć” moduł Active Record, możesz wykorzystać komendę rake db:migrate VERBOSE=false.

5 Użycie modeli w migracjach

Podczas tworzenia czy aktualizacji danych zawsze kusi wykorzystanie jednego z modeli. W zasadzie po to przecież tworzymy modele, by ułatwić dostęp do danych. Można to wykonać, zachowując jednak pewien warunek.

Wyobraźmy sobie na przykład migrację korzystającą z modelu Product w celu aktualizacji wiersza powiązanej z nim tabeli. Następnie Ania zmienia model Product, dodając nową kolumnę i walidację dla niej. Gdy Bartek wraca z wakacji, aktualizuje kod źródłowy i wpisując komendę rake db:migrate wykonuje wszystkie migracje, włącznie z migracją korzystającą z modelu Product. Ponieważ Bartek zaktualizował kod źródłowy, model Product zawiera nową walidację stworzoną przez Anię, podczas gdy baza nie posiada nowej kolumny. Spowoduje to błąd – walidacja będzie się odnosić do nieistniejącej kolumny.

Często chcę po prostu zaktualizować wiersze danych w bazie bez konieczności ręcznego wpisywania kodu SQL; nie wykorzystuję żadnych specyficznych dla danego modelu cech. Jednym z rozwiązań jest w takim wypadku stworzenie kopii modelu wewnątrz samej migracji, na przykład:

class AddPartNumberToProducts < ActiveRecord::Migration class Product < ActiveRecord::Base end def self.up ... end def self.down ... end end

Migracja posiada swoją własną kopię modelu Product i nie odnosi się już do modelu zdefiniowanego w aplikacji.

5.1 Praca na zmieniających się modelach

Dla poprawy wydajności, informacja o kolumnach w modelu jest cache’owana. Na przykład, jeśli dodasz kolumnę do tabeli a potem spróbujesz użyć powiązanego modelu w celu dodania nowego rekordu, może on użyć starej informacji o kolumnach. Możesz zmusić moduł Active Record do ponownego odczytania informacji o kolumnach korzystając z metody reset_column_information. Na przykład:

class AddPartNumberToProducts < ActiveRecord::Migration class Product < ActiveRecord::Base end def self.up add_column :product, :part_number, :string Product.reset_column_information ... end def self.down ... end end

6 Zrzuty schematów

6.1 Po co tworzyć pliki schematów?

Migracje, jakkolwiek potężnym narzędziem by były, nie są wiarygodnym źródłem wiedzy o schemacie bazy. Ta rola należy do pliku schema.rb lub pliku SQL, generowanym przez moduł Active Record poprzez odpytywanie bazy danych. Nie zostały one stworzone z myślą o edycji, ich zadanie to po prostu reprezentacja aktualnego schematu bazy.

Aby utworzyć nową kopię aplikacji nie musimy powtarzać wszystkich wykonanych migracji. Znacznie szybciej i prościej jest wczytać do bazy opis docelowego schematu.

Przykładowo, w celu utworzenia bazy do testów, należy pobrać zrzut aktualnego schematu bazy (do pliku schema.rb lub development.sql) a następnie wczytać go do bazy testowej.

Pliki ze schematami są także przydatne, by szybko sprawdzić atrybuty obiektu Active Record. Ta informacja nie jest zapisana w kodzie modelu, często wynika z w wielu kolejnych migracji, ale jej podsumowanie jest dostępne w pliku schematu. Warta polecenia jest wtyczka annotate_models, dodająca na górze każdego modelu komentarze podsumowujące jego schemat.

6.2 Typy zrzutów schematu

Są dwa sposoby na zapisanie zrzutu schematu bazy. Który z nich zostanie wykorzystany, zależy od zapisu w pliku konfiguracyjnym config/environment.rb. Odpowiedzialna za to jest zmienna config.active_record.schema_format, przyjmująca wartości :sql lub :ruby.

Jeśli zmienna ma przypisaną wartość :ruby, schemat bazy jest zapisywany w pliku db/schema.rb. Jeśli przyjrzeć się temu plikowi, można odnieść wrażenie, że to zapis jednej, wielkiej migracji.

ActiveRecord::Schema.define(:version => 20080906171750) do create_table "authors", :force => true do |t| t.string "name" t.datetime "created_at" t.datetime "updated_at" end create_table "products", :force => true do |t| t.string "name" t.text "description" t.datetime "created_at" t.datetime "updated_at" t.string "part_number" end end

Pod wieloma względami tak właśnie jest. Ten plik jest tworzony poprzez odpytywanie bazy danych i zapisanie jej struktury za pomocą kolejnych poleceń create_table, add_index itd. Plik taki może zostać wczytany do dowolnej bazy danych, ponieważ polecenia te są niezmienne dla wszystkich obsługiwanych przez moduł Active Record baz. To może być bardzo użyteczne, jeśli chcesz stworzyć aplikację działającą na kilku bazach danych.

Wykorzystanie pliku schema.rb ma też swoje wady. Nie są w nim zapisywane informacje o funkcjach specyficznych tylko dla danego typu bazy, takich jak klucze opcje, triggery czy procedury składowane. O ile możesz ich używać podczas migracji, nie da się ich odtworzyć w zrzucie schematu. Jeśli korzystasz z takich opcji, powinieneś zmienić zawartość zmiennej konfiguracyjnej na :sql.

W takim przypadku, zamiast korzystać z wbudowanego narzędzia modułu Active Record, schematy zapisywane są przez narzędzia stworzone dla danego typu bazy danych (poprzez polecenie db:structure:dump) do pliku db/#\{RAILS_ENV\}_structure.sql. Na przykład dla bazy PostgreSQL używane jest narzędzie pg_dump, a dla MySQL w pliku zapisywane są wyniki pytań SHOW CREATE TABLE dla kolejnych tabel. Wczytanie tak zapisanego schematu bazy sprowadza się do wykonania kodu SQL zapisanego we wskazanym pliku.

Z założenia taki zrzut schematu umożliwia stworzenie idealnej kopii bazy danych. Niestety zwykle uniemożliwia to przeniesienie schematu na bazę innego typu.

6.3 Kontrola wersji zrzutów schematów

Ponieważ pliki schematu powinny być wiarygodnym źródłem wiedzy o bazie danych, zalecane jest włączenie ich do systemu kontroli wersji, by umożliwić wgląd w historię zmian bazy.

7 Active Record i integralność referencyjna

Korzystanie z modułu Active Record powoduje przeniesienie części logiki z bazy danych do modeli. W związku z tym narzędzie takie jak triggery czy klucze obce, które tworzą dodatkową logikę wewnątrz bazy danych nie są zbyt często używane.

Walidacje takie jak validates_uniqueness_of są jednym ze sposobów na wymuszenie integralności danych przez model. Opcja asocjacji :dependent pozwala modelowi automatycznie niszczyć obiekty potomne w przypadku usunięcia ich rodzica. Jak wszystkie opcje działające na poziomie aplikacji, wspomniane sposoby nie gwarantują zachowania integralności referencyjnej. Jest przyczyna dla której niektórzy wspierają je dodatkowo kluczami obcymi zawartymi w bazie danych.

Chociaż moduł Active Record nie zapewnia narzędzi do bezpośredniej obsługi kluczy obcych, można w tym celu wykorzystać metodę execute. Są także dostępne różne wtyczki, takie jak redhillonrails, które poszerzają możliwości modułu Active Record o obsługę kluczy obcych (m.in. umożliwiają zapisywanie ich w schemacie bazy w pliku schema.rb).

8 Changelog

Lighthouse ticket