Ruby:Introduksjon til Rails

Fra CodeWiki

Gå til: navigasjon, søk

I denne introduksjonen skal vi se hvordan vi enkelt kan bruke web-rammeverket Ruby on Rails til å lage database-drevene sider. For å kunne følge guiden bør du helst ha erfaring med Ruby, og det er også en fordel å ha kjennskap til webutvikling fra andre språk eller rammeverk.

Innhold

MVC (Model, View, Controller)

MVC er et design-pattern som tar sikte på å skille forettningslogikk, brukergrensesnitt og brukerinteraksjon. Oppgaven til modellen er å representere data. I Rails sitt tilfelle er dette gjerne en database. Oppgaven til brukergrensesnittet (viewet) er å vise denne dataen. Kontrolleren knytter modellen og viewet. Dette innbærer blant annet at den har ansvar for å utføre operasjoner på modellene og hente data til viewet.

Mer om modeller

Når man lager en modell (eng. model), definerer man en Ruby-klasse som arver fra ActiveRecord::Base. Dette gjør at ActiveRecord genererer et sett med metoder som gjør at klassen blir et grensesnitt mot en databasetabell. Dette gir deg tilgang til kolonnene gjennom vanlige accessor-metoder:

user = User.new(:name => 'Ola', :email => 'ola@norge.no') 
user.name    # => "Ola" 
user.email   # => "ola@norge.no"

Mer om kontrollere

En kontroller (eng. controller) defineres på en liknende måte som en modell. Man lager en klasse som arver fra ActionController::Base. Man kan tenke seg en kontroller som en logisk enhet som inneholder relatert funksjonalitet. En tenkt side kan f.eks. ha en UsersController som inneholder et sett med "actions"; new, create, update, delete, edit, osv. Man lager et action ved å definere en metode i kontrolleren:

class UsersController < ActionController::Base 
  def index 
    @users = User.find(:all) 
  end 
end

I dette tilfellet lager vi en index-action som finner alle brukere. Merk at dette skjer ved å kalle metoden User.find som ActiveRecord::Base implementerer for oss.

Mer om views

Et view viser data som kontrolleren har hentet. Instans-variabler i en kontroller er tilgjengelig i det respektive viewet:

<% @users.each do |user| %>
User name: <%= user.name %>
<% end %>

En enkel blog

Vi har nå tatt for oss en noen av de mest elementære delene av Rails, og vi kan bruke dette til å skrive et veldig minimalistisk blog-system.

Det første vi gjør, er å starte et nytt Rails-prosjekt:

$ rails weblog

Vi definerer en route

I Rails er en "route" det som knytter en URL som http://website.com/books til en kontroller. Når Rails får en forespørsel, sjekker den conf/routes.rb og, og bestemmer utifra denne hvilken kontroller (og action, som vi kommer tilbake til) som får håndtere forespørselen. Jo tidligere i fila routen er, jo høyere prioritet har den.

Vi begynner med å legge til en "named route" i config/routes.rb, slik at fila ser slik ut:

ActionController::Routing::Routes.draw do |map|
  map.resources :posts
 
  map.connect ':controller/:action/:id'
  map.connect ':controller/:action/:id.:format'
end

map.resources knytter HTTP forespørsler til respektive actions:

HTTP-verb URL Action
GET /posts index
GET /posts/new new
POST /posts create
GET /posts/1 show(:id => 1)
GET /posts/edit/1 edit(:id => 1)
PUT /posts update
DELETE /posts/1 delete(:id => 1)


Dette gir oss en måte å definere såkalte ressurser på en RESTful måte. map.resources setter også opp et sett med "route helpers", slik at istedenfor å skrive:

<%= link_to :controller => 'posts', :action => 'index' %>

... kan du isteden skrive:

<%= link_to posts_url %>

Modellen

Vi begynner med å lage en Post-model ved å skrive følgende kommando:

$ script/generate model post
     exists  app/models/
     exists  test/unit/
     exists  test/fixtures/
     create  app/models/post.rb
     create  test/unit/post_test.rb
     create  test/fixtures/posts.yml
     exists  db/migrate
     create  db/migrate/20080812202856_create_posts.rb

For øyeblikket trenger vi bare å konsentrere oss om to filer; app/models/post.rb og db/migrate/[dato]_create_posts.rb. app/models/post.rb er som navnet tilsier modellen, og db/migrate/[dato]_create_posts.rb er noe som kalles for en migrasjon.

Migrasjoner

En migrasjon (eng. migration) er Ruby-kode som definerer en endring i databasen. Siden man ikke skriver SQL-kode direkte, fungerer den samme koden uavhengig av hvilket databasesystem som benyttes. Vi åpner migrasjonen, og endrer den slik at den ser ut som dette:

class CreatePosts < ActiveRecord::Migration
  def self.up
    create_table :posts do |t|
      t.string :title
      t.text :content
      t.timestamps
    end
  end
 
  def self.down
    drop_table :posts
  end
end


Deretter går vi tilbake til terminalen, og skriver følgende kommando:

$ rake db:migrate

Dette migrerer databasen til siste versjon, og lager en tabell som heter posts som har 4 kolonner: title (string), content (text), created_at (date) og updated_at (date).

View og kontroller

Nå har vi nå har fått på plass databasetabellen, er det på tide å lage kontrolleren:

$ script/generate controller posts
     exists  app/controllers/
     exists  app/helpers/
     create  app/views/posts
     exists  test/functional/
     create  app/controllers/posts_controller.rb
     create  test/functional/posts_controller_test.rb
     create  app/helpers/posts_helper.rb


Få en liste over postene!

Vi åpner så fila app/controllers/posts_controlle.rb og legger til en index-action:

class PostsController < ApplicationController
  def index
    @posts = Post.find(:all)
    respond_to do |format|
      format.html
      format.xml { render :xml => @posts.to_xml }
    end
  end
end

Det meste av koden er relativt selvforklarende. Vi finner alle postene i databasen ved å kalle Post.find (som Post-modellen vår har arvet fra ActiveRecord), og lagrer resultatet i instans-variabelen @posts. respond_to gjør det enkelt å bygge Rails applikasjoner som web services, og betyr at man kan sende en respons utifra hva klienten ønsker. (Dette bestemmes utifra HTTP Accept headeren som klienten sender.) Dette betyr at om en klient vil ha responsen i HTML, renderer Rails viewet som vi kommer til å definere straks, men hvis klienten vil ha XML, sendes en XML representasjon av @posts til klienten.

Nå kan vi bevege oss tilbake til terminalen, og skrive følgende kommando:

$ script/server

Åpne så en nettleser og gå til http://localhost:3000. Her skal du se en velkomstmelding alà «Welcome aboard. You're riding Ruby on Rails!» Vel og bra. For å se kontrolleren som vi nettopp har laget, gå til http://localhost:3000/posts. Her vil du se en melding som sier «Template is missing». Dette betyr at actionet vi prøver å vise ikke har et view. Så, da lager vi et:

app/views/posts/index.html.erb:

<h1>Listing blog posts</h1>
 
<ul>
  <% @posts.each do |post| %>
    <li><%= post.title %></li>
  <% end %>
</ul>

Vi legger til nye poster

Når vi endelig kan vise poster oppdager vi at vi har et aldri så lite problem. Vi har ingen poster, og vi har heller ingen måte å legge til poster på. Tilbake til kontrolleren:

def new
  @post = Post.new
end

Her har vi lagt til et action som lager et nytt Post-objekt. Dette actionet gjør ikke annet enn å rendere (vise) en form som inneholder felter hvor vi kan skrive inn tittelen på posten og innholdet. Over til viewet:

app/views/posts/new.html.erb

<% form_for @post do |form| %>
  <p>Enter post title: <%= form.text_field :title, :size => 45 %></p>
  <p><%= form.text_area :content, :size => "75x20" %></p>
  <p><%= submit_tag 'Create post' %></p>
<% end %>

Her bruker vi form_for-helperen til å generere en form-tag for oss, og bruker form-objektet som form_for gir oss til å generere tekstbokser og submit-knappen. Ganske rett frem. Siden new bare viser en form, trenger vi et action som faktisk lagrer posten. Vi lager en create-action:

app/controllers/posts_controller.rb

def create
  @post = Post.new(params[:post])
  respond_to do |format|
    if @post and @post.save
      flash[:notice] = 'Post successfully created'
      format.xml { head :ok }
    else
      flash[:error] = 'Something went wrong'
      format.xml { render :xml => @post.errors }
    end
 
    format.html { redirect_to posts_path }
  end
end

Her lager vi et nytt Post-objekt og initialiserer det med params[:post]. Alle parametere, enten de kommer fra GET eller POST, er tilgjengelig gjennom params-metoden. Denne returnerer en Hash som inneholder parametrene og de respektive verdiene. Legg merke til at istedenfor å rendere app/views/posts/create.html.erb (som jo ikke finnes), "redirecter" vi til index-siden i create-metoden.

Dette er et eksempel på parametre som ble sendt da jeg testet create lokalt:

{ :commit => "Create post", 
:post => { :title => "Min post", :content => "Falleri og faller av :(" },
:authenticity_token => "baf016e72f6bfb322240e6dea3ba2dc737b3f749", 
:action => "create", 
:controller =>"posts" }

Nå kan du åpne nettleseren din og skrive inn http://localhost:3000/posts/new for å test at alt fungerer som det skal. Nå har vi en liste over hvilke poster vi har lagt til.

... viser postene

Men, vi har fortsatt ingen måte å vise hver enkelt post på. Vi åpner først app/views/posts/index.html.erb:

app/views/posts.index.html.erb

<h1>Listing blog posts</h1>
 
<% @posts.each do |post| %>
  <div id="post">
    <%= link_to post.title, post_url(post) %>     
  </div>
<% end %>
 
<%= link_to 'New post', new_post_url %>

Her har vi gjort slik at hver post har en link til en show-action samt. byttet ut listen med div-er slik at det hele blir litt mer "blogg-aktig". Nå åpner vi Post-kontrolleren og legger til en show-action:

app/controllers/posts_controller.rb

def show
  @post = Post.find(params[:id])
end

Denne er ganske grei; vi finner posten som skal vises og lagrer den i @post. Nå må vi definere et view til å vise posten, så vi åpner app/views/posts/show.html.erb og legger til:

<h1><%= @post.title %></h1>
 
<p><%= @post.content %></h1>

... og oppdaterer dem!

Nå kan vi legge til og vise poster. Dette er i og for seg kult, men det er ofte greit å kunne endre ting uten å slette hele databasen og legge inn alt på nytt. På tide å legge til en edit og update-action:

app/controllers/posts_controller.rb:

def edit
  @post = Post.find(params[:id])
end
 
def update
  @post = Post.find(params[:id])
  respond_to do |format|
    if @post and @post.update_attributes(params[:post])
      flash[:notice] = 'Post updated'
      format.xml { head :ok }
    else
      flash[:error] = 'Unable to update post'
      format.xml { render :xml => @post.errors.to_xml }
    end
 
    format.html { redirect_to posts_path }
  end
end

I edit finner vi den posten som skal endres ved å kalle Post.find med params[:id] som argument. Vi vil derfor kunne redigere en gitt post ved å skrive inn http://localhost:3000/edit/[post-nummer] i nettleseren vår. I kontrolleren vår likner update-actionet litt på create. Men, istedenfor å lage et nytt objekt og kalle Post.save, finner vi objektet som skal oppdateres vha find og kaller update_attributes med POST-dataen som argument. Over til viewet:

app/views/posts/edit.html.erb:

<% form_for @post, :method => :put do |form| %>
  <p>Enter post title: <%= form.text_field :title, :size => 45 %></p>
  <p><%= form.text_area :content, :size => "75x20" %></p>
  <p><%= submit_tag 'Update post' %></p>
<% end %>

Dette viewet er nesten identisk med new-viewet, men siden vi skal oppdatere siden (PUT og ikke POST) må vi spesifisere HTTP-verbet vi skal bruke slik at forespørselen blir sent til update. (POST blir brukt hvis ikke annet er spesifisert, derfor trengte vi ikke å skrive noe i new-viewet.) Dette gjør vi ved å gi form_for argumentet ":method => :put".


Vi implementerer skadebegrensning

Vi har nå muligheten til å lage nye poster, vise dem og redigere dem. Veldig kult, men selv den beste blogger vil før eller siden komme til skade for å legge ut materiale som resten av verden kanskje kunne blitt spart for. La oss legge til en mulighet for å slette poster! Vi finner frem kontrolleren vår:

app/controllers/posts_controller.rb

def destroy
  @post = Post.find(params[:id])
  respond_to do |format|
    if @post and @post.destroy
      flash[:notice] = 'Post destroyed'
      format.xml { head :ok }
    else
      flash[:notice] = 'Unable to destroy post'
      format.xml { render :xml => @post.errors.to_xml }
      end
 
    format.html { redirect_to posts_path }
  end
end

Igjen så begynner vi med å finne posten. Deretter sjekker vi om posten eksisterer. Hvis den gjør det, så sletter vi den, og redirecter til indexen!

Layouts og stylesheets

Nå har vi en snasen liten webapplikasjon som vi stolt viser frem til venner og bekjente. Alle virker nokså imponerte over prestasjonen, bortsett fra Timothy. Han påpeker at svart tekst på hvit bakgrunn som ligger kræsja oppe i venstre hjørnet på nettsiden, ikke akkurat er sukkertøy for øyet. Vi tyr til layouts og stylesheets!


Layoutene bestemmer oppsettet på siden. Vi finner layoutene i app/views/layouts, og hvis man ikke spesifiserer annet i kontrolleren sin brukes app/views/layouts/application.html.erb som standard layout. Et enkelt layout kan se omtrent slikt ut:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
 
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
  <head>
    <title>Killer web application!</title>
    <%= stylesheet_link_tag 'style' %>
    <%= yield :page_stylesheets %>
  </head>
 
  <body>
    <div id="header"></div>
 
    <div id="content">
      <%= "<div class='notice'>#{flash[:notice]}</div>" if flash[:notice] %>
      <%= "<div class='error'>#{flash[:error]}</div>" if flash[:error] %>
 
      <%= yield %>
    </div>
 
    <div id="footer"></div>
  </body>
</html>

Her har vi lagt til en felles stylesheet for alle sidene i head-tagen. Denne har vi inkludert ved å bruke stylesheet_link_tag-helperen. På neste linje har vi et yield-statment. Dette gjør at vi senere (i et view, for eksempel), kan bruke content_for-metoden til å sette inn ytterligere stylesheets. (Det må selvfølgelig ikke være stylesheets, men det vil i dette tilfellet være mest naturlig ettersom vi har valgt navnet "page_stylesheets".)

Man kan spesifisere hvilket layout man vil bruke slik:

class SomeController < ApplicationController
  layout 'some_layout'
end

Så vil vi gjerne legge til noen stylesheets. Vi finner stylesheetene i public/stylesheets. Hvis vi bare trengte én stylesheet for hele siden vår, hadde vi i vårt tilfelle bare trengt å opprette fila style.css i public/stylesheets og redigert denne som vi vanligvis ville gjort. Hvis vi på en annen side ønsket å legge til flere stylesheets som bare enkelte views inkluderte, kunne vi skrevet:

app/views/some_controller/index.html.erb:

<% content_for :page_stylesheets do %>
<%= stylesheet_link_tag  'foobar' %>
<% end %>
 
...

Oppsummering

Da har vi fått på plass et (veldig) minimalistisk blog-system og implementert grunnleggende CRUD-funksjoner (Create, Read, Update, Delete). Det er på ingen måte fullstendig, og dekker ikke på langt nær alle mulighetene og funksjonene som Rails tilbyr. Men, det illustrerer allikevel noen viktige aspekter som forhåpentligvis gjør det mulig å eksperimentere med Rails på egenhånd.

Eksterne lenker

Rails Framework Documentation
Ruby API
Wikipedia om REST

Personlige verktøy
dataprogrammering
generelt