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 AddPartNumberToProductsTa 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:stringwygeneruje 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:stringgeneruje
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:decimalwygeneruje 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
- September 14, 2008: initial version by Frederick Cheung