UWAGA: ten tutorial służył jako pomoc dla początkujących podczas sesji hands-on na pierwszym spotkaniu KRUG, odnosi się więc do Rails w wersji z września 2006.

Prerekwizyty:

  • zainstalowana baza danych mysql
  • zainstalowane railsy

Tworzenie projektu

Tworzymy prostą aplikację do zarządzania zbiorami domowej biblioteki.

W przypadku Eclipse z pluginem RadRails:

Wybieramy File->New->Project, z listy projektów wybieramy Rails -> Rails Project. W oknie dialogowym wpisujemy nazwę aplikacji – ‘library’, zostawiamy zaznaczone checkboxy ‘Generate Rails application skeleton’ i ‘Create a WEBrick server’ oraz odznaczone ‘Disable table pluralization’.

W innym przypadku:

Aby założyć nowy projekt uruchamiamy polecenie: rails library

Aby zaimportowac do eclipsa można utworzyć nowy `General Project`, a następnie skorzystać z opcji Import -> General -> File System.

Uruchamianie aplikacji:

Następnie przechodzimy do nowo utworzonego katalogu ‘library’ i uruchamiamy aplikację:

cd library
script/server &

W przeglądarce wpisujemy adres: http://localhost:3000. Powinna pojawić się strona:

`Welcome aboard You’re riding the Rails!`

Struktura katalogu aplikacji

Railsy wygenerowały drzewo katalogów aplikacji. Zgodnie z zasadą `convention over configuration`, dzięki umieszczaniu plików źródłowych i niezbędnych danych konfiguracyjnych w przewidzianych do tego miejscach, railsy znajdują je same.

Funkcje najważniejszych katalogów:

  • app: pliki źródłowe aplikacji
  • config: pliki konfiguracyjne
  • db: pliki związane z bazą danych
  • lib: dodatkowe biblioteki
  • log: logi
  • public: javascript’y, css’y, obrazki oraz konfiguracja webservera
  • test: testy
  • vendor: pluginy

Organizacja plików źródłowych

W railsach aplikacje tworzy się według wzorca projektowego Model-View-Controller. Źródła aplikacji w katalogu app są zorganizowane w sposób odzwierciedlający ten podział:

  • controllers: kontrolery, łączą widok z modelem
  • models: modele, zawierają logikę
  • views: widoki, zorganizowane w podkatalogach o nazwach odpowiadającym poszczególnym kontrolerom
  • helpers: funkcje pomocnicze dla widoku

Tworzenie bazy danych

Za pomocą wybranego klienta mysql tworzymy pustą bazę danych o nazwie library_development:

create database library_development;

Konfigurujemy połączenie z bazą danych edytując plik config/database.yml. Odszukujemy w nim sekcję ‘development’ i uzupelniamy dane takie jak nazwa uzytkownika i hasło do łączenia się z bazą:

development:
  adapter: mysql
  database: library_development
  username: some_user
  password: some_pass
  host: localhost

W przypadku błędu dotyczącego pliku /tmp/mysql.sock przy próbie połączenia, najłatwiej poradzić sobie zmieniając pole host z localhost na 127.0.0.1. Edycja schematu bazy danych – migracje

Aby utworzyć pierwszą tabelę w katalogu ‘library’ wpisujemy polecenie:

script/generate migration create_books

W katalogu db pojawił się nowy plik ‘001_create_books.rb’. Przechodzimy do jego edycji. Jest to plik tzw. migracji, służący do zmiany definicji struktury bazy danych w sposób przyrostowy; kolejne zmiany bazy umieszczane są w kolejno numerowanych plikach. Jest to mechanizm ułatwiający współpracę wielu programistów nad schematem bazy danych.

W pliku migracji znajdują się dwie metody – up i down (a dokładnie self.up i self.down, co oznacza metody klasowe). W up definiujemy zmianę bazy danych, w down sposób odwrócenia tej zmiany. W metodach tych można korzystać ze specjalnej składni rubiego do definiowania schematu bazy, lub ze zwykłego sqla. Pierwsza migracja służy utworzeniu tabeli z książkami:

class CreateBooks < ActiveRecord::Migration
  def self.up
    sql=<<-END_SQL
      CREATE TABLE books(
        id int(11) AUTO_INCREMENT PRIMARY KEY,
        title varchar(256),
        author varchar(256)
      )
    END_SQL
    execute sql
  end

  def self.down
    execute 'DROP TABLE books'
  end
end

Metoda execute przyjmuje napis (string) zawierający polecenia sql. W metodzie up skorzystaliśmy ze składni umożliwiającej przypisanie do zmiennej sql wieloliniowego stringa. W metodzie down zamiast tego skorzystaliśmy z najprostszej postaci stringa w rubim.

Następnie uruchamiamy w katalogu library polecenie:

rake migrate

i jeśli poprawnie skonfigurowaliśmy połączenie z bazą i nie ma błędów sql w migracji, powinniśmy zobaczyć:

== CreateBooks: migrating =====================================================
-- execute(`      CREATE TABLE books(
        title varchar(256),
        author varchar(256)
      )
`)
   -> 0.0477s
== CreateBooks: migrated (0.0483s) ============================================

a w bazie danych powinna istnieć tabela books.

Scaffold

Utworzymy prosty interfejs do zarządzania książkami w bibliotece, wykorzystując do tego mechanizm generowania scaffoldu – `rusztowania`.

W katalogu library uruchamiamy:

script/generate scaffold book

a następnie wpisujemy w przeglądarce adres: http://localhost:3000/books

Pokazuje się strona z pustą listą książek. Gdy dodamy pierwszą książkę okaże się, że scaffold obsługuje również edycję, kasowanie i widok szczegółów książki. Cały odpowiedzialny za to kod można znaleźć w katalogu naszej aplikacji:

  • app/controllers/books_controller: zawiera definicje akcji dostępnych z www. Jak widać ich nazwy odpowiadają url’om w następujący sposób: nazwa_kontrolera/nazwa_akcji
  • app/models/book: model, klasa obsługująca tabelę books. Jak widać jest pusta – wszystkie metody do operowania na danych są generowane dynamicznie
  • app/views/books: widoki dla wszystkich akcji. Warto zwrócić uwagę na sposób współdzielenia fragmentów widoku poprzez mechanizm partiali – w tym wypadku widoki new i edit korzystają z wspólnego formularza _form.rhtml.

Relacja wiele do jeden

Utworzona tabela z książkami ma poważnż wadę – imię i nazwisko autora wpisywane jest w jednym polu, na dodatek dla każdej książki napisanej przez tego samego autora trzeba je wprowadzać ponownie. Poprawimy to tworząc nową tabelę authors, oraz definiując odpowiednią relację między autorami i książkami.

script/generate migration create_authors

Edytujemy plik migracji:

class CreateAuthors < ActiveRecord::Migration
  def self.up
    sql=<<-END_SQL
      CREATE TABLE authors(
        id int(11) AUTO_INCREMENT PRIMARY KEY,
        name varchar(128),
        surname varchar(128)
      )
    END_SQL
    execute sql
  end

  def self.down
    execute 'DROP TABLE authors'
  end
end
Uruchamiamy migrację:
rake migrate

Przy okazji można zaobserwować, że w naszej bazie danych istnieje mała tabela schema_info, o jednym polu – version. Po pierwszej migracji pole to miało wartość jeden, po drugiej zostało zwiększone – pole to oznacza aktualną wersję migracji w danej bazie.

Tworzymy scaffold dla tabeli authors:

script/generate scaffold author

i pod adresem http://localhost:3000/authors mamy dostępny interfejs do zarządzania autorami.

Teraz należy zmienić definicję tabeli books, zamieniając w niej pole author na identyfikator autora. Robimy to oczywiście poprzez nową migrację: script/generate migration remove_author_and_add_author_id_to_books

Nazwa może się wydawać długa, ale lepiej stosować nazwy opisujące co dana migracja robi oraz aby uniknąć duplikacji nazw migracji, co powoduje błędy.

class RemoveAuthorAndAddAuthorIdToBooks < ActiveRecord::Migration
  def self.up
    execute 'ALTER TABLE books DROP COLUMN author'
    execute 'ALTER TABLE books ADD COLUMN author_id int(11) REFERENCES authors(id)'
  end

  def self.down
    execute 'ALTER TABLE books DROP COLUMN author_id'
    execute 'ALTER TABLE books ADD COLUMN author varchar(256)'
  end
end

W migracji moglibyśmy zdefiniować sposób postępowania z danymi z poprzedniej struktury,tak aby ich nie utracić. Nie robimy tego w tym przykładzie, więc najlepiej te dane wyrzucić z bazy, bo będą powodowały błędy.

Po wykonaniu migracji:

rake migrate

przejdźmy do edycji modelu book. Dopiszmy jedną linijkę:

class Book < ActiveRecord::Base
  belongs_to :author
end

Definiujemy w ten sposób relację wiele do jeden między książkami a autorem.

Aby móc wybrać autora książki przy jej tworzeniu, musimy wprowadzić zmiany do widoku i kontrolera.

W kontrolerze books dodajemy jedną linię do akcji new:


  def new
    @authors = Author.find_all
    @book = Book.new
  end
W pliku views/books/_form zamieniamy fragment:
<%= text_area 'book', 'author' %>
na:
<%= select 'book', 'author_id', @authors.collect{|a| [a.name+' '+a.surname,a.id]} %>
Po tych drobnych zmianach możemy utworzyć nową książkę przypisując jej autora z pola wyboru. Autor książki nie pokazuje się jednak w widoku szczegółów. Edytujemy plik views/show.rhtml do następującej postaci:
<p>
    <b>Title:</b> <%= h @book.title %>
</p>
<p>
    <b>Author:</b> <%= h @book.author.name+' '+@book.author.surname %>
</p>

<%= link_to 'Edit', :action => 'edit', :id => @book %> |
<%= link_to 'Back', :action => 'list' %>

Teraz wyświetla się imię i nazwisko autora. Warto tu zwrócić uwagę na następujące szczegóły:

  • obiekt @book ma metod? author – jest ona generowana dynamicznie na podstawie zdefiniowanej relacji belongs_to
  • metoda h w widoku służy do wyświetlania napisów czyszcząc je z potencjalnie szkodliwych elementów, jakimi mogą być na przykład złośliwy html czy javascript. Powinna być stosowana zawsze przy wypisywaniu danych wprowadzonych przez użytkownika.

Można też zauważyć, że po raz drugi do wypisania imienia i nazwiska autora skorzystaliśmy z konkatenacji stringów (wcześniej przy tworzeniu książki), przez co wprowadziliśmy duplikację. Możemy to naprawić na kilka sposobów, najprostszy to utworzenie w modelu author nowego wirtualnego atrybutu full_name:

  def full_name
    name+' '+surname
  end
Teraz w obu widokach możemy wypisać dane autora za pomocą full_name. W new.rhtml zamieniamy fragment:
<%= select 'book', 'author_id', @authors.collect{|a| [a.name+' '+a.surname,a.id]} %>
na:
<%= select 'book', 'author_id', @authors.collect{|a| [a.full_name,a.id]} %>
a w show.rhtml zamieniamy:
<%= h @book.author.name+' '+@book.author.surname %>
na:
<%= h @book.author.full_name %>

Relacja jeden do wielu

Gdybyśmy chcieli, aby w widoku szczegółów autora ukazywała się lista książek przez niego napisanych, musielibyśmy zdefiniować relację `w drugą stronę`, czyli jeden do wielu, w modelu autor:

class Author < ActiveRecord::Base
  has_many :books

  def full_name
    name+' '+surname
  end
end

Dzięki temu obiekty klasy Author zyskują dodatkowe metody dotyczące powiązanych z nimi książek, między innymi metodę books zwracającą książki tego autora. Można więc dodać w /views/authors/show.rhtml następujący fragment:

<p><b>Books:</b></p>
<ul>
<% @author.books.each do |b| -%>
<li><%= b.title%>
<% end -%>
</ul>

Walidacje

Obecnie nasza aplikacja nie protestuje przy tworzeniu autorów i książek zostawiając puste pola formularza. Skutkuje to tworzeniem pustych obiektów w bazie. Aby temu zapobiec, można skorzystać z mechanizmu walidacji w railsach.

Ponieważ nasze modele są minimalistyczne, wszystkie pola powinny być obowiązkowe:


class Author < ActiveRecord::Base
  has_many :books

  def full_name
    name+' '+surname
  end

  validates_presence_of :name, :surname
end

class Book < ActiveRecord::Base
  belongs_to :author

  validates_presence_of :title, :author_id
end

Próba submitu pustego formularza skutkuje teraz wypisaniem listy błędów dotyczących poszczególnych pól. Rails udostępnia również wiele innych podstawowych walidacji, jak validates_uniqueness_of czy validates_format_of.

Podsumowanie

W zamyśle powyższy mini tutorial ma na celu zaprezentowanie najczęściej wykorzystywanych elementów frameworku rails i dostarczenie pewnej ilości bazowego kodu do samodzielnego eksperymentowania. Powodzenia!

Podstawowe źródła:

  • http://api.rubyonrails.org/
  • http://wiki.rubyonrails.org/rails