Jatkamme sovelluksen rakentamista siitä, mihin jäimme viikon 1 lopussa. Allaoleva materiaali olettaa, että olet tehnyt kaikki edellisen viikon tehtävät. Jos et tehnyt kaikkia tehtäviä, voit ottaa kurssin repositorioista edellisen viikon mallivastauksen. Jos sait suurimman osan edellisen viikon tehtävistä tehtyä, saattaa olla helpointa, että täydennät vastaustasi mallivastauksen avulla.
Jos otat edellisen viikon mallivastauksen tämän viikon pohjaksi, kopioi hakemisto pois kurssirepositorion alta (olettaen että olet kloonannut sen) ja tee sovelluksen sisältämästä hakemistosta uusi repositorio.
Huom: muutamilla Macin käyttäjillä oli ongelmia Herokun tarvitseman pg-gemin kanssa. Paikallisesti gemiä ei tarvita ja se määriteltiinkin asennettavaksi ainoastaan tuotantoympäristöön. Jos ongelmia ilmenee, voit asentaa gemit antamalla bundle install
-komentoon seuraavan lisämääreen:
bundle install --without production
Tämä asetus muistetaan jatkossa, joten pelkkä bundle install
riittää kun haluat asentaa uusia riippuvuuksia.
Haluamme laittaa sivulle modernien web-sivustojen tyyliin navigointipalkin eli sijoittaa sovelluksen kaikkien sivujen ylälaitaan linkit oluiden ja panimoiden listoihin.
Navigointipalkki saadaan generoitua helposti metodin link_to
ja polkuapumetodien avulla lisäämällä jokaiselle sivulle seuraavat linkit:
<%= link_to 'breweries', breweries_path %>
<%= link_to 'beers', beers_path %>
Tarkkasilmäisimmät saattoivat jo viime viikolla huomata, että näkymätemplatet eivät sisällä kaikkea sivulle tulevaa HTML-koodia. Esim. yksittäisen oluen näkymätemplate /app/views/beers/show.html.erb on seuraava:
<p id="notice"><%= notice %></p>
<p>
<strong>Name:</strong>
<%= @beer.name %>
</p>
<p>
<strong>Style:</strong>
<%= @beer.style %>
</p>
<p>
<strong>Brewery:</strong>
<%= @beer.brewery_id %>
</p>
<%= link_to 'Edit', edit_beer_path(@beer) %> |
<%= link_to 'Back', beers_path %>
Jos katsomme yksittäisen oluen sivun HTML-koodia selaimen view source code -toiminnolla, huomaamme, että sivulla on paljon muutakin kuin templatessa määritelty HTML (osa headin sisällöstä on poistettu):
<!DOCTYPE html>
<html>
<head>
<title>Ratebeer</title>
<link data-turbolinks-track="true" href="/assets/application.css?body=1" media="all" rel="stylesheet" />
<script data-turbolinks-track="true" src="/assets/jquery.js?body=1"></script>
<meta content="authenticity_token" name="csrf-param" />
<meta content="hZaC8o95xUbekA3PTsVZ+JmkVj9CCn5a4Kw8tF96WOU=" name="csrf-token" />
</head>
<body>
<p id="notice"></p>
<p>
<strong>Name:</strong>
Iso 3
</p>
<p>
<strong>Style:</strong>
Lager
</p>
<p>
<strong>Brewery:</strong>
1
</p>
<a href="/beers/1/edit">Edit</a> |
<a href="/beers">Back</a>
</body>
</html>
Sivu sisältää siis dokumentin tyypin määrittelyn, käytettävät tyylitiedostot ja javascript-tiedostot määrittelevän head-elementin ja sivun sisällön määrittelevän body-elementin (ks. lisää http://www.w3.org/community/webed/wiki/HTML/Training).
Oluen sivun näkymätemplate siis sisältää ainoastaan body-elementin sisälle tulevan HTML-koodin.
On tyypillistä, että sovelluksen kaikki sivut ovat body-elementin sisältöä lukuun ottamatta samat. Railsissa saadaankin määriteltyä kaikille sivuille yhteiset osat sovelluksen layoutiin, eli tiedostoon app/views/layouts/application.html.erb. Oletusarvoisesti tiedoston sisältö on seuraavanlainen:
<!DOCTYPE html>
<html>
<head>
<title>Ratebeer</title>
<%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %>
<%= javascript_include_tag "application", "data-turbolinks-track" => true %>
<%= csrf_meta_tags %>
</head>
<body>
<%= yield %>
</body>
</html>
Head-elementin sisällä olevat apumetodit määrittelevät sovelluksen käyttämät tyyli- ja javascript-tiedostot, apumetodi csrf_meta_tags
lisää sivulle CSRF-hyökkäykset eliminoivan logiikan
(ks. tarkemmin esim. täältä). Kuten arvata saattaa, body-elementin sisällä olevan komennon yield
-kohdalle renderöityy kunkin sivun oman näkymätemplaten määrittelemä sisältö.
Saamme navigointipalkin näkyville kaikille sivuille muuttamalla sovelluksen layoutin body-elementtiä seuraavasti:
<body>
<div class="navibar">
<%= link_to 'breweries', breweries_path %>
<%= link_to 'beers', beers_path %>
</div>
<%= yield %>
</body>
Navigointipalkki on laitettu luokan navibar sisältävän div-elementin sisällä, joten sen ulkoasua voidaan halutessa muotoilla css:n avulla.
Lisää tiedostoon app/assets/stylesheets/application.css seuraava:
.navibar {
padding: 10px;
background: #EFEFEF;
}
Kun reloadaan sivun, huomaat, että sovelluksesi antama vaikutelma on jo melko professionaali.
Railsin Routing-komponentin (ks. http://api.rubyonrails.org/classes/ActionDispatch/Routing.html, http://guides.rubyonrails.org/routing.html) vastuulla on ohjata eli reitittää sovellukselle tulevien HTTP-pyyntöjen käsittely sopivan kontrollerin metodille.
Tieto siitä miten eri URLeihin tulevat pyynnöt tulee reitittää, konfiguroidaan tiedostoon config/routes.rb
. Tässä vaiheessa tiedoston sisältö on seuraavanlainen:
Ratebeer::Application.routes.draw do
resources :beers
resources :breweries
end
Tutustumme myöhemmin resources
-metodin lisäämiin reitteihin.
Aloitetaan sillä, että tehdään panimoiden listasta sovelluksen oletusarvoinen kotisivu. Tämä tapahtuu lisäämällä routes-tiedostoon rivi
root 'breweries#index'
Nyt osoite http://localhost:3000/ ohjautuu kaikki panimot näyttävälle sivulle.
Edellinen on oikeastaan hieman tyylikkäämpi tapa sanoa:
get '/', to: 'breweries#index'
eli reititä polulle '/' tuleva HTTP GET -pyyntö käsiteltäväksi luokan BreweriesController
metodille index
.
Englanninkielistä kirjallisuutta lukiessa kannattaa huomata, että Railsin terminologiassa kontrollereiden metodeja nimitetään usein actioneiksi. Käytämme kuitenkin kurssilla nimitystä kontrollerimetodi tai kontrollerin metodi.
Voisimme vastaavasti lisätä routes.rb:hen rivin
get 'kaikki_bisset', to: 'beers#index'
jolloin URLiin http://localhost:3000/kaikki_bisset tulevat GET-pyynnöt vievät kaikkien oluiden sivulle. Kokeile että tämä toimii.
Mielenkiintoinen yksityiskohta routes.rb-tiedostossa on se, että vaikka tiedosto näyttää tekstimuotoiselta konfiguraatiotiedostolta, on koko tiedoston sisältö Rubya. Tiedoston rivit ovat metodikutsuja. Esim. rivi
get 'kaikki_bisset', to: 'beers#index'
kutsuu get-metodia parametreinaan merkkijono '/kaikki_bisset' ja hash to: 'beers#index'
. Hashin yhteydessä on käytetty uudempaa syntaksia, eli vanhaa syntaksia käyttäen reitityksen kohteen määrittelevä hash kirjoitettaisiin :to => 'beers#index'
, ja routes.rb:n rivi olisi:
get 'kaikki_bisset', :to => 'beers#index'
voisimme käyttää metodikutsussa myös sulkuja, ja määritellä hashin käyttäen aaltosulkuja, eli kömpelöimmässä muodossa reitti voitaisiin määritellä seuraavasti:
get( 'kaikki_bisset', { :to => 'beers#index' } )
Rubyn joustava syntaksi (yhdessä kielen muutamien muiden piirteiden kanssa) mahdollistaakin luonnollisen kielen sujuvuutta tavoittelevan ilmaisutavan sovelluksen konfigurointiin ja ohjelmointiin. Tyyli tunnetaan englanninkielisellä termillä Internal DSL ks. http://martinfowler.com/bliki/InternalDslStyle.html
Lisätään seuraavaksi ohjelmaan mahdollisuus antaa oluille "reittauksia" eli pisteytyksiä skaalalla 0-50. Emme käytä viime viikolta tuttua generaattoria (rails generate scaffold...
) vaan teemme kaiken itse.
Haluamme että kaikki reittaukset ovat osoitteessa http://localhost:3000/ratings. Kokeillaan nyt selaimella mitä tapahtuu kun urliin yritetään mennä.
Seurauksena on virheilmoitus No route matches [GET] "/ratings"
eli osoitteeseen tehtyä HTTP GET -pyyntöä ei vastannut mikään määritelty "reitti".
Lisätään reitti kirjoittamalla routes-tiedostoon seuraava:
get 'ratings', to: 'ratings#index'
Määrittelemme siis Rails-konventiota mukaillen, että kaikkien reittausten sivun 'ratings' hoitaa RatingsController-luokan metodi index.
Huom: suunnilleen samaa tarkoittaisi myös match 'ratings' => 'ratings#index'
. Kuten niin tyypillistä Railsille, voi routes.rb:ssäkin käyttää saman asian määrittelemiseen monia erilaisia tapoja.
Kokeile nyt sivua uudelleen selaimella.
Virheilmoitus muuttuu muotoon uninitialized constant RatingsController
eli määritelty reitti yrittää ohjata ratings-osoitteeseen tulevan GET-kutsun RatingsController
-luokassa määritellyn kontrollerin metodin index
-käsiteltäväksi.
Määritellään kontrolleri tiedostoon /app/controllers/ratings_controller.rb.
class RatingsController < ApplicationController
def index
end
end
Huomioi nimeämiskäytännöt ja tiedoston sijainti, Rails etsii kontrolleria nimenomaan hakemistosta /app/controllers. Jos sijoitat kontrollerin muualle, ei Rails löydä sitä.
Kokeile nyt sivua selaimella vielä kerran.
Seurauksena on uusi virheilmoitus
Missing template ratings/index, application/index with {:locale=>[:en], :formats=>[:html], :handlers=>[:erb, :builder, :raw, :ruby, :jbuilder, :coffee]}. Searched in: * "/Users/mluukkai/kurssirepot/wadror/ratebeer/app/views"
joka taas johtuu siitä, että Rails yrittää renderöidä kontrollerin metodia vastaavan oletusarvoisen, hakemistossa /app/views/ratings/index.html.erb olevan näkymätemplaten, mutta sellaista ei löydy.
Luodaan tiedosto /app/views/ratings/index.html.erb jolla on seuraava sisältö:
<h2>List of ratings</h2>
<p>To be completed...</p>
ja nyt sivu toimii!
Huomaa taas Railsin konventiot, tiedoston sijainti on tarkasti määritelty, eli koska kyseessä on näkymätemplate jota kutsutaan ratings-kontrollerista (joka siis on täydelliseltä nimeltään RatingsController), sijoitetaan se hakemistoon /views/ratings.
Muistutuksena vielä viime viikosta: kontrollerimetodi index
renderöi oletusarvoisesti suorituksensa lopuksi (oikeassa hakemistossa olevan) index-nimisen näkymän. Eli koodi
class RatingsController < ApplicationController
def index
end
end
tekee oikeastaan siis saman asian kuin seuraava:
class RatingsController < ApplicationController
def index
render :index # renderöin näkymätemplate /app/views/ratings/index.html
end
end
Eksplisiittinen render-metodin kutsu jätetään kuitenkin yleensä pois jos renderöidään oletusarvoinen, eli kontrollerimetodin kanssa samanniminen template.
Yhteen olueeseen liittyy useita reittauksia, eli oliomalli pitää päivittää seuraavanlaiseksi:
Tarvitsemme siis tietokantataulun ja vastaavan model-olion.
Railsissa muutokset tietokantaan, esim. uuden taulun lisääminen, kannattaa tehdä aina migraatioiden avulla. Migraatiot ovat siis hakemistoon db/migrate sijoitettavia tiedostoja, joihin kirjoitetaan Rubyllä tietokantaa muokkaavat operaatiot. Tutustumme migraatioihin tarkemmin vasta myöhemmin ja käytämme modelin luomiseen nyt Railsin valmista model-generaattoria, joka luo model-olion lisäksi automaattisesti tarvittavan migraation.
Reittauksella on kokonaislukuarvoinen score
sekä vierasavain, joka linkittää sen reitattuun olueeseen. Railsin konvention mukaan vierasavaimen nimen tulee olla beer_id
.
Model ja tietokannan generoiva migraatio saadaan luotua antamalla komentoriviltä komento:
rails g model Rating score:integer beer_id:integer
ja luodaan tietokantataulu suorittamalla komentoriviltä migraatio
rake db:migrate
Toisin kuin viime viikolla käyttämämme scaffold-generaattori, model-generaattori ei luo ollenkaan kontrolleria eikä näkymätemplateja.
Muistutuksena viime viikolta: railsin generaattorien (scaffold, model, ...) luomat tiedostot on mahdollista poistaa komennolla destroy:
rails destroy model Rating
Jos olet suorittanut jo migraation ja huomaat että generaattorin luoma koodi onkin tuohottava, on erittäin tärkeää ensin perua migraatio komennolla
rake db:rollback
Jotta yhteydet saadaan myös oliotasolle (muistutuksena viime viikon materiaali), tulee luokkia päivittää seuraavasti
class Beer < ActiveRecord::Base
belongs_to :brewery
has_many :ratings
end
class Rating < ActiveRecord::Base
belongs_to :beer
end
Eli jokaiseen olueeseen liittyy useita reittauksia ja reittaus kuuluu aina täsmälleen yhteen olueeseen.
Käynnistetään Rails-konsoli antamalla komentoriviltä komento rails c
. Huomaa, että jos konsolisi oli jo auki, saat lisätyn koodin konsolin käyttöön komennolla reload!
. Luodaan muutama reittaus:
2.2.1 :001 > b = Beer.first
2.2.1 :002 > b.ratings.create score:10
2.2.1 :003 > b.ratings.create score:21
2.2.1 :004 > b.ratings.create score:17
Reittaukset siis lisätään ensimmäisenä kannasta löytyvälle oluelle. Huomaa luontitapa, saman asian olisi ajanut monimutkaisempi tapa
b.ratings << Rating.create(score:15)
Konsolin käyttörutiini on Rails-kehittäjälle äärimmäisen tärkeää. Tee seuraavat asiat konsolista käsin:
luo uusi panimo "BrewDog", perustamisvuosi 2007
lisää panimolle kaksi olutta
- Punk IPA (tyyli IPA)
- Nanny State (tyyli lowalcohol) lisää molemmille oluille muutama reittaus
Kertaa tarvittaessa edellisen viikon materiaalista konsolia käsittelevät osuudet.
Palauta tämä tehtävä lisäämällä sovelluksellesi hakemisto exercises ja sinne tiedosto exercise1, joka sisältää copypasten konsolisessiosta
Nyt tietokannassamme on reittauksia, ja haluamme saada ne listattua kaikkien reittausten sivulle.
Listataan kaikki reittaukset ratings-sivulla. Ota mallia esim. panimokontrollerin
index
-metodista ja sitä vastaavasta templatesta. Tee reittauksen lista ensin esim. seuraavaan tyyliin<ul> <% @ratings.each do |rating| %> <li> <%= rating %> </li> <% end %> </ul>Lisää sivulle myös tieto reittausten yhteenlasketusta lukumäärästä
Tässä vaiheessa sivun pitäisi näyttää suunnilleen seuraavalta
Reittaus renderöityy hiukan ikävässä muodossa. Tämä johtuu siitä, että li-elementin sisällä on pelkkä olion nimi, ja koska emme ole määritelleet Ratingille olion merkkijonomuotoa määrittelevää to_s
-metodia, käytössä on kaikkien luokkien yliluokalta Objectilta peritty oletusarvoinen to_s
.
Määrittelemme hetken kuluttua reittauksille metodin to_s
, tutkitaan ensin kuitenkin muutamaa asiaa liittyen olion metodien määrittelyyn.
Tutkitaan hetki luokkaa Brewery
:
class Brewery < ActiveRecord::Base
has_many :beers
end
Panimoilla on nimi name
ja perustamisvuosi year
. Konsolista käsin pääsemme näihin käsiksi tuttuun tyyliin:
irb(main):001:0> b = Brewery.first
irb(main):002:0> b.name
=> "Koff"
irb(main):003:0> b.year
=> 1897
irb(main):004:0>
Teknisesti ottaen esim. b.year
on metodikutsu. Rails luo model-olioon jokaiselle vastaavan tietokantataulun skeeman määrittelemälle sarakkeelle kentän eli attribuutin ja metodit attribuutin arvon lukemista ja arvon muuttamista varten. Nämä automaattisesti generoidut metodit ovat sisällöltään suunilleen seuraavat:
class Brewery < ActiveRecord::Base
# ..
def year
read_attribute(:year)
end
def year=(value)
write_attribute(:year, value)
end
end
Metodit siis mahdollistavat olion attribuutin arvon lukemisen ja muuttamisen. Arvoa muuttava metodi ei kuitenkaan vielä tee muutosta tietokantaan, muutos tapahtuu vasta kutsuttaessa metodia save
, kyseessä ovatkin siis automaattisesti generoituvat 'getterit ja setterit'.
Olion ulkopuolelta olion attribuutteihin päästään käsiksi 'pistentotaatiolla':
b.year
entä olion sisältä? Tehdään panimolle metodi, joka demonstroi panimon attribuuttien käsittelyä panimon sisältä:
class Brewery < ActiveRecord::Base
has_many :beers
def print_report
puts name
puts "established at year #{year}"
puts "number of beers #{beers.count}"
end
end
eli olion sisältä metodeja (myös beers
on metodi!) voidaan kutsua kuten esim. Javassa, metodin nimellä.
Ja esimerkki metodin käytöstä:
irb(main):001:0> b = Brewery.first
irb(main):002:0> b.print_report
Koff
established at year 1897
number of beers 2
Metodeja olisi voitu kutsua olion sisältä myös käyttäen Rubyn 'thissiä' eli olion self
-viitettä:
def print_report
puts self.name
puts "established at year #{self.year}"
puts "number of beers #{self.beers.count}"
end
Tehdään sitten panimolle metodi, jonka avulla panimon voi 'uudelleenkäynnistää', tällöin panimon perustamisvuosi muuttuu vuodeksi 2016:
def restart
year = 2016
puts "changed year to #{year}"
end
kokeillaan
irb(main):024:0> b = Brewery.first
irb(main):025:0> b.year
=> 1897
irb(main):026:0> b.restart
changed year to 2016
irb(main):027:0> b.year
=> 1897
irb(main):028:0>
eli huomaamme, että vuoden muuttaminen ei toimikaan odotetulla tavalla! Syynä tähän on se, että year = 2016
metodin restart
sisällä ei kutsukaan metodia
def year=(value)
joka sijoittaisi attribuutille uuden arvon, vaan luo metodille paikallisen muuttujan nimeltään year
johon arvo 2016 sijoitetaan.
Jotta sijoitus onnistuu, on metodia kutsuttava self
-viitteen kautta:
def restart
self.year = 2016
puts "changed year to #{year}"
end
ja nyt toiminnallisuus on odotetun kaltainen:
irb(main):029:0> b = Brewery.first
irb(main):030:0> b.year
=> 1897
irb(main):031:0> b.restart
changed year to 2016
irb(main):032:0> b.year
=> 2016
irb(main):033:0>
HUOM: Rubyssä olioiden instanssimuuttujat määritellään @
-alkuisina. Instanssimuuttujat eivät kuitenkaan ole sama asia kuin ActiveRecordin avulla tietokantaan talletettavat olioiden attribuutit. Eli seuraavakaan metodi ei toimisi odotetulla tavalla:
def restart
@year = 2016
puts "changed year to #{@year}"
end
Panimon sisällä year
siis on ActiveRecordin tietokantaan tallentama attribuutti, kun taas @year
on olion instanssimuuttuja. Railsin modeleissa instanssimuutuujia ei juurikaan käytetä. Instanssimuuttujia käytetään Railsissa lähinnä tiedonvälitykseen kontrollereilta näkymille.
Tee sitten luokalle Rating metodi
to_s
, joka palauttaa oliosta paremman merkkijonoesityksen, esim. muodossa "karhu 35", eli ensin reitatun oluen nimi ja sen jälkeen reittauksen pistemäärä.Merkkijonon muodostamisessa myös seuraavasta voi olla apua https://github.com/mluukkai/WebPalvelinohjelmointi2016/blob/master/web/rubyn_perusteita.md#merkkijonot
Tehtävän jälkeen reittausten sivujen tulisi näyttää suunnilleen seuraavalta:
Huom: kun kirjoitat sovelluksellesi uutta koodia, useimmiten on järkevämpää tehdä kokeiluja konsolista käsin. Seuraavassa kokeillaan reittauksen oletusarvoista to_s
-metodin palauttamaa arvoa:
irb(main):024:0> r = Rating.last
irb(main):025:0> r.to_s
=> "#<Rating:0x007f8054b1cb10>"
irb(main):026:0>
Määritellään reittaukselle to_s
-metodi:
class Rating < ActiveRecord::Base
belongs_to :beer
def to_s
"tekstiesitys"
end
end
ja kokeillaan uudelleen konsolista:
irb(main):026:0> r.to_s
=> "#<Rating:0x007f8054b1cb10>"
Muutos ei kuitenkaan vaikuta tulleen voimaan, missä vika?
Jotta muutettu koodi tulisi voimaan, on uusi koodi ladattava konsolin käyttöön komennolla reload!
ja käytettävä uudestaan kannasta haettua olioa:
irb(main):027:0> reload!
Reloading...
=> true
irb(main):028:0> r.to_s
=> "#<Rating:0x007f8054b1cb10>"
irb(main):029:0> r = Rating.last
irb(main):030:0> r.to_s
=> "tekstiesitys"
irb(main):031:0>
Eli kuten yllä näemme, ei pelkkä koodin uudelleenlataaminen vielä riitä, sillä muuttujassa r
olevassa oliossa on käytössä edelleen vanha koodi.
Lisää luokalle
Beer
metodiaverage_rating
, joka laskee oluen ratingien keskiarvon. Lisää keskiarvo yksittäisen oluen sivulle jos oluella on ratingejaNäkymätemplatessa voi tehdä tuotettavasta sisällöstä ehdollisen seuraavasti
<% if @beer.ratings.empty? %> beer has not yet been rated! <% else %> beer has some ratings <% end %>
Tehtävän jälkeen oluen sivun tulisi näyttää suunnilleen seuraavalta (huom: edellisen viikon jäljiltä sivullasi saattaa näkyä panimon nimen sijaan panimon id. Jos näin on, muuta näkymäsi vastaamaan kuvaa):
Moduuli enumerable (ks. http://ruby-doc.org/core-2.1.0/Enumerable.html) sisältää runsaasti oliokokoelmien läpikäyntiin tarkoitettuja apumetodeja.
Oliokokoelmamaiset luokat voivat sisällyttää moduulin enumerable toiminnallisuuden itselleen, ja tällöin ne perivät moduulin tarjoaman toiminnallisuuden.
Tutustu nyt
map
- jainject
-metodeihin (ks. esim. http://ruby-doc.org/core-2.1.0/Enumerable.html#inject http://ruby-doc.org/core-2.1.0/Enumerable.html#map ja etsi googlella lisää ohjeita) ja muuta (tarvittaessa) oluen reittausten keskiarvon laskeva metodi käyttämään map:ia tai injectiäKeskiarvon laskeminen onnistuu tässä tapauksessa myös helpommin hyödyntämällä ActiveRecordin metodeja, ks. http://api.rubyonrails.org/classes/ActiveRecord/Calculations.html
Lisätään konsolista jollekin vielä reittaamattomalle oluelle yksi reittaus. Oluen sivu näyttää nyt seuraavalta:
Sivulla on pieni, mutta ikävä kielioppivirhe:
beer has 1 ratings
Tutustu Railsissa valmiina olevaan
pluralize
-apumetodiin http://apidock.com/rails/ActionView/Helpers/TextHelper/pluralize ja tee oluen sivusta metodin avulla kieliopillisesti oikeaoppinen (eli yhden reittauksen tapauksessa tulee tulostua 'beer has 1 rating')
Tehdään nyt sovellukseen mahdollisuus reittausten luomiseen www-sivulta käsin.
Railsin konventioiden mukaan Rating-olion luontiin tarkoitetun lomakkeen tulee löytyä osoitteesta ratings/new, ja lomakkeeseen pääsyn hoitaa ratings-kontrollerin metodi new
.
Luodaan vastaava reitti routes.rb:hen
get 'ratings/new', to:'ratings#new'
Lisäämme siis ratings-kontrolleriin (joka siis täydelliseltä nimeltään on RatingsController) metodin new
, joka huolehtii lomakkeen renderöinnistä. Metodi on yksinkertainen:
def new
@rating = Rating.new
end
Metodi ainoastaan luo uuden Rating-olion ja välittää sen @rating
-muuttujan avulla oletusarvoisesti renderöitävälle näkymätemplatelle new.html.erb. Olio luodaan new
-komennolla eli sitä ei talleteta tietokantaan.
Luodaan nyt seuraava näkymä eli tiedosto /app/views/ratings/new.html.erb:
<h2>Create new rating</h2>
<%= form_for(@rating) do |f| %>
beer id: <%= f.number_field :beer_id %>
score: <%= f.number_field :score %>
<%= f.submit %>
<% end %>
Mene nyt lomakkeen sisältävälle sivulle eli osoitteeseen http://localhost:3000/ratings/new
Näkymän avulla muodostuva HTML-koodi näyttää (suunnilleen) seuraavalta (näet koodin menemällä sivulle ja valitsemalla selaimesta view page source):
<form action="/ratings" method="post">
beer id: <input name="rating[beer_id]" type="number" />
score: <input name="rating[score]" type="number" />
<input name="commit" type="submit" value="Create Rating" />
</form>
eli generoituu normaali HTML-lomake (ks. tarkemmin http://www.w3.org/community/webed/wiki/HTML/Training#Forms).
Lomakkeen lähetystapahtuman kohdeosoite on /ratings ja käytettävä HTTP-metodi GET:in sijasta POST. Lomakkeessa on kaksi numeromuotoista kenttää ja niiden arvot lähetetään vastaanottajalle POST-kutsun mukana muuttujien rating[beer_id]
ja rating[score]
arvoina.
Railsin metodi form_for
siis muodostaa automaattisesti oikeaan osoitteeseen lähetettävän, oikeanlaisen formin, jossa on syöttökentät kaikille parametrina olevan tyyppisen olion attribuuteille.
Lisää lomakkeiden muodostamisesta form_for
-metodilla osoitteessa
http://guides.rubyonrails.org/form_helpers.html#dealing-with-model-objects
Jos yritämme luoda reittauksen aiheutuu virheilmoitus No route matches [POST] "/ratings"
eli joudumme luomaan tiedostoon config/routes.rb reitin:
post 'ratings', to: 'ratings#create'
Uuden olion luonnista vastaava metodi on Railsin konvention mukaan nimeltään create
, luodaan sen pohja:
def create
raise
end
Tässä vaiheessa metodi ei tee muuta kuin aiheuttaa poikkeuksen (metodikutsu raise
).
Kokeillaan nyt lähettää lomakkeella tietoa. Kontrollerin metodissa heittämä poikkeus aiheuttaa virheilmoituksen. Rails lisää virhesivulle erilaista diagnostiikkaa, mm. HTTP-pyynnön parametrit sisältävän hashin, joka näyttää seuraavalta:
{"utf8"=>"✓",
"authenticity_token"=>"1OfMRb9BTZzTnM5PfpFUupImkdIbLbwWi0FB90XBSqs=",
"rating"=>{"beer_id"=>"1", "score"=>"2"},
"commit"=>"Create Rating"}
Hashin sisällä on siis välittynyt lomakkeen avulla lähetetty tieto.
Parametrit sisältävä hash on kontrollerin sisällä talletettu muuttujaan params
.
Uuden ratingin tiedot ovat hashissa avaimen :rating
arvona, eli pääsemme niihin käsiksi komennolla params[:rating]
joka taas on hash jonka arvo on {"beer_id"=>"1", "score"=>"2"}
. Eli esim. pistemäärään päästäisiin käsiksi komennolla params[:rating][:score]
.
Tutkitaan hieman asiaa kontrollerista käsin Railsin debuggeria hyödyntäen
Jos olet luonut sovelluksesi railsin versiolla 4.2 on sovelluksesi käyttöön jo konfiguroitu debuggeri byebug (ja railsin web-konsoli jota tarkastelemme hieman myöhemmin).
Jos olet luonut sovelluksesi vanhemmalla Railsin versiolla, lisää tiedostoon Gemfile seuraavat:
group :development, :test do
gem 'byebug'
gem 'web-console', '~> 2.0'
end
ja suorita komentoriviltä komento bundle install
. Käynnistä nyt Rails-sovellus uudelleen, eli paina ctrl+c Railsia suorittavassa terminaalissa ja anna komento rails s
uudelleen. Uudelleenkäynnistys on syytä suorittaa aina uusia gemejä asennettaessa.
Lisätään kontrollerin alkuun, eli sille kohtaan koodia jota haluamme tarkkailla, komento byebug
def create
byebug
raise
end
Kun luot lomakkeella uuden reittauksen, sovellus pysähtyy komennon byebug
kohdalle. Terminaaliin josta Rails on käynnistetty, avautuu nyt interaktiivinen konsolinäkymä:
Started POST "/ratings" for 127.0.0.1 at 2016-01-17 17:43:18 +0200
Processing by RatingsController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"ZsDeRgbMt4qOW3bB49hqHO7f0SHFfoAqADJMmjowwAs=", "rating"=>{"beer_id"=>"1", "score"=>"20"}, "commit"=>"Create Rating"}
[5, 14] in /Users/mluukkai/kurssirepot/ratebeer/app/controllers/ratings_controller.rb
5:
6: def new
7: @rating = Rating.new
8: end
9:
10: def create
11: byebug
=> 12: raise
13: end
14: end
(byebug)
Nuoli kertoo seuraavana vuorossa olevan komennon. Tutkitaan nyt params
-muuttujan sisältöä:
(byebug) params
{"utf8"=>"✓", "authenticity_token"=>"ZsDeRgbMt4qOW3bB49hqHO7f0SHFfoAqADJMmjowwAs=", "rating"=>{"beer_id"=>"1", "score"=>"20"}, "commit"=>"Create Rating", "action"=>"create", "controller"=>"ratings"}
(byebug) params[:rating]
{"beer_id"=>"1", "score"=>"20"}
(byebug) params[:rating][:score]
"20"
Debuggerin konsolissa voi tarpeen vaatiessa suorittaa mitä tahansa koodia Rails-konsolin tavoin.
Debuggerin tärkeimmät komennot lienevät step, next, continue ja help. Step suorittaa koodista seuraavan askeleen, edeten mahdollisiin metodikutsuihin. Next suorittaa seuraavan rivin kokonaisuudessaan. Continue jatkaa ohjelman suorittamista normaaliin tapaan.
Lisätietoa byebugista seuraavassa http://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-byebug-gem
Kontrollerin sisällä params[:rating]
siis sisältää kaiken tiedon, joka uuden reittauksen luomiseen tarvitaan. Ja koska kyseessä on hash, joka on muotoa {"beer_id"=>"1", "score"=>"30"}
, voi sen antaa suoraan metodin create
parametriksi, eli reittauksen luonnin pitäisi periaatteessa onnistua komennolla:
Rating.create params[:rating] # joka siis tarkoittaa samaa kuin Rating.create beer_id:"1", score:"30"
Muuta siis kontrollerisi koodi seuraavanlaiseksi:
def create
Rating.create params[:rating]
end
Kokeile nyt luoda reittaus. Vastoin kaikkia odotuksia, luomisoperaatio epäonnistuu ja seurauksena on virheilmoitus
ActiveModel::ForbiddenAttributesError
Mistä on kyse?
Jos olisimme tehneet reittauksen luovan komennon muodossa
Rating.create beer_id: params[:rating][:beer_id], score: params[:rating][:score]
joka siis periaatteessa tarkoittaa täysin samaa kuin ylläoleva muoto (sillä params[:rating]
on sisällöltään täysin sama hash kuin beer_id:params[:rating][:beer_id], score:params[:rating][:score]
), ei virheilmoitusta olisi tullut. Tietoturvasyistä Rails ei kuitenkaan salli mielivaltaista params
-muuttujasta tapahtuvaa "massasijoitusta" (engl. mass assignment eli kaikkien parametrien antamista hashina) olion luomisen yhteydessä.
Rails 4:stä lähtien kontrollerin on lueteltava eksplisiittisesti mitä hashin params
sisällöstä voidaan massasijoittaa olioiden luonnin yhteydessä. Tähän kontrolleri käyttää params
:in metodeja require
ja permit
.
Periaatteena on, että ensin requirella otetaan paramsin sisältä luotavan olion tiedot sisältävä hash:
params.require(:rating)
tämän jälkeen luetellaan permitillä ne kentät, joiden arvon massasijoitus sallitaan:
params.require(:rating).permit(:score, :beer_id)
Kontrollerimme on siis seuraava:
def create
Rating.create params.require(:rating).permit(:score, :beer_id)
end
Lisää tietoa lomakkeiden parametrien käsittelystä http://edgeguides.rubyonrails.org/action_controller_overview.html luvusta 4.5 Strong parameters
Kokeile nyt reittauksen luomista. HUOM: kun luot lomakkeella reittausta, tarkista, että lomakkeelle syöttämä oluen id vastaa jonkun tietokannassa olevan oluen id:tä!
Reittausten luominen onnistuu jo (tarkista tilanne konsolista tai kaikkien reittausten sivulta), mutta aiheuttaa virheilmoituksen, sillä metodi yrittää renderöidä oletusarvoisesti näkymätemplaten /views/ratings/create.html.erb jota ei ole.
Voisimme luoda templaten, mutta päätämmekin, että uuden reittauksen luomisen jälkeen käyttäjän selain uudelleenohjataan kaikki reittaukset sisältävälle sivulle, eli muutetaan kontrollerin koodi muodoon:
def create
Rating.create params.require(:rating).permit(:score, :beer_id)
redirect_to ratings_path
end
ratings_path
on Railsin tarjoama polkuapumetodi, joka tarkoittaa samaa kuin "/ratings"
Jos olet luonut reittauksia joihin liittyvä beer_id
ei vastaa olemassa olevan oluen id:tä, saat nyt todennäköisesti virheilmoituksen. Voit tuhota konsolista (rails console
) käsin nämä ratingit seuraavasti
Rating.last # näyttää viimeksi luodun ratingin, tarkasta onko siinä oleva beer_id virheellinen
Rating.last.delete # poistaa viimeksi luodun ratingin
Saat tuhottua oluettomat ratingit myös seuraavalla "onelinerilla":
Rating.all.select{ |r| r.beer.nil? }.each{ |r| r.delete }
Select luo taulukon, johon sisältyy ne läpikäydyn kokoelman alkiot, joille koodilohkossa oleva ehto on tosi. r.beer.nil?
palauttaa true
jos olio r.beer
on nil
.
Edellisen komennon voi kirjottaa myös hieman lyhemmässä muodossa
Rating.all.select{ |r| r.beer.nil? }.each(&:delete)
Mitä kontrollerissa käytetty komento redirect_to ratings_path
oikeastaan tekee? Normaalistihan kontrolleri renderöi sopivan näkymätemplaten ja näin aikaansaatu HTML-koodi palautetaan selaimelle, joka renderöi sivun näytölle.
Uudelleenohjauksessa palvelin lähettää selaimelle statuskoodilla 302 varustetun vastauksen, joka ei sisällä ollenkaan HTML:ää. Vastaus sisältää ainoastaan osoitteen, mihin selaimen tulee automaattisesti tehdä uusi HTTP GET -pyyntö. Uudelleenohjautuminen on huomaamatonta selaimen käyttäjän kannalta.
Kokeile mitä tapahtuu kun laitat uuden reittauksen luomisen jälkeiseksi uudelleenohjaukseksi esim. redirect_to "http://www.cs.helsinki.fi"
!
http://en.wikipedia.org/wiki/Post/Redirect/Get
Olisi ollut teknisesti mahdollista olla käyttämättä uudelleenohjausta ja renderöidä kaikkien reittausten sivu suoraan uuden reittauksen luovasta kontrollerista:
def create
Rating.create params.require(:rating).permit(:score, :beer_id)
@ratings = Rating.all
render :index
end
Vaikka aikaansaannos näyttää sivuston käyttäjälle täsmälleen samalta, tämä ei ole kuitenkaan järkevää muutamastakaan syystä. Ensinnäkin kaikki metodissa index
oleva koodi, joka tarvitaan näkymän muodostamiseen on kopioitava create
-metodiin (nyt kopioitavaa koodia ei ole paljon, mutta tilanne ei ole aina yhtä yksinkertainen). Toinen syy liittyy selaimen käyttäytymiseen. Jos kontrollerimme käyttäisi sivun renderöintiä ja selaimen käyttäjä refreshaisi sivun uuden oluen luomisen jälkeen, kävisi seuraavasti:
eli selain kysyy käyttäjältä lähetetäänkö lomakkeen tiedot uudelleen, sillä edellinen selaimen toiminto jonka refreshaus suorittaa on nimenomaan lomakkeen tietojen lähetyksen hoitanut HTTP POST. Redirectauksen yhteydessä vastaavaa ongelmaa ei ole, sillä POST-komennon jälkeen seuraava käyttäjälle näkyvä sivu saadaan aikaan redirectauksen aikaansaamalla HTTP GET:illä.
Nyrkkisääntönä (ei vaan Railsissa vaan Web-ohjelmoinnissa yleensäkin, ks. http://en.wikipedia.org/wiki/Post/Redirect/Get) onkin käyttää lomakkeista huolehtivien HTTP POST -metodien käsittelevässä kontrollerissa aina uudelleenohjausta (ellei kontrollerin suorittama operaatio epäonnistu esim. lomakkeella lähetetyn tiedon virheellisyyden vuoksi).
Nostetaan vielä esiin tämä tärkeä ero:
- kun kontrollerimetodi päättyy komentoon
render :jotain
(joka siis tapahtuu usein implisiittisesti) generoi Rails-sovellus HTML-sivun, jonka palvelin lähettää selaimelle renderöitäväksi - kun kontrollerimetodi päättyy komentoon
redirect_to osoite
lähettää palvelin selaimelle statuskoodissa 302 varustetun uudelleenohjauspyynnön, jossa se pyytää selainta tekemään automaattisesti HTTP GET -pyynnön kontrollerimetodin määrittelemään osoitteeseen, selaimen käyttäjän kannalta uudelleenohjaus on huomaamaton toimenpide
Jokaisen Web-ohjelmoijan on syytä ymmärtää edellinen!
Rails on sisältänyt versiosta 4.2 alkaen oletusarvoisesti debuggerin tapaan toimivan web-konsolin. Konsolinäkymä avautuu automaattisesti jos ohjelmassa syntyy poikkeus.
Poikkeuksen voi "aiheuttaa" esim. kirjoittamalla mihin tahansa kohtaan koodia raise
kuten teimme jo hieman aiemmin. Palautetaan raise reittauskontrollerin metodiin create:
class RatingsController < ApplicationController
def create
raise
Rating.create params.require(:rating).permit(:score, :beer_id)
redirect_to ratings_path
end
end
Kun nyt luot reittauksen, renderöityy tuttu virhesivu. Virhesivun alalaidassa olevassa konsolinäkymässä voi nyt suorittaa ruby-komentoja täsmälleen samalla tavalla kuin debuggeria käytettäessä:
Aivan kuten debuggeria käytettäessä, web-konsolin näkymä avautuu siihen kontekstiin, jossa virhe tapahtuu, eli esim. muuttuja params
on viitattavissa, samoin voidaan suorittaa kaikkia komentoja, joita konrollerimetodista käsin voitaisiin suorittaa, esim. hakea reittauksia tietokannasta modelin Rating
avulla.
Web-konsoli on varsin kätevä työkalu. Valitettavasti web-konsoli ei kuitenkaan toimi optimaalisesti näkymätemplateihin liittyvissä virhetilanteissa.
Luodaan sovellukseen olut, johon ei liity panimoa:
2.2.1 :024 > Beer.create name:"crap beer", style:"lowalcohol"
=> #<Beer id: 13, name: "crap beer", style: "lowalcohol", brewery_id: nil, created_at: "2016-01-17 16:11:49", updated_at: "2016-01-17 16:11:49">
luodun olion vierasavaimen brewery_id
arvoksi siis tulee nil
.
Kaikkien oluiden sivulle meneminen aiheuttaa nyt virheen:
Näemme virheilmoituksesta rivin, joka aiheuttaa ongelman ja ongelman syyn:
undefined method `name' for nil:NilClass
eli näkymätemplate yrittää kutsua metodia name
olemattomalle oliolle. Virhellinen rivi sisältää komennon
beer.brewery.name
eli näkymätemplaten paikallisen muuttujan beer
kentässä brewery
olevalle oliolle yritetään kutsua metodia, mutta olioa ei ole olemassa.
Toisin kuin kontrollereissa tai modeleissa tapahtuvissa virheissä, emme valitettavasti pääse web-konsolissa käsiksi näkymien muuttujiin.
Debuggerin avulla tämä kuitenkin onnistuu. Tiedämme, että ongelmallinen tilanne syntyy jos vastaan tulee olut johon liittyvä panimo on nil. Lisätään näkymätemplateen debuggerin käynnistyskomento byebug
tälläistä tilannetta varten:
<tbody>
<% @beers.each do |beer| %>
<% byebug if beer.brewery.nil? %>
<tr>
...
</tr>
<% end %>
</tbody>
Lisäsimme siis templaten renderöinnin sekaan komennon <% byebug if beer.brewery.nil? %>
, joka käynnistää debuggerin jos annettu ehto on tosi. Käyttämämme komento on rybymainen tapa kirjoittaa
if beer.brewery == nil
byebug
end
Kun nyt menemme kaikkien oluiden sivulle, debuggeri käynnistyy ja pääsemme tarkastelemaan näkymätemplaten paikallisen muuttujan arvoa eli ongelman aiheuttanutta olutta:
[11, 20] in /Users/mluukkai/kurssirepot/ratebeer/app/views/beers/index.html.erb
11: </thead>
12:
13: <tbody>
14: <% @beers.each do |beer| %>
15: <% byebug if beer.brewery.nil? %>
=> 16: <tr>
17: <td><%= link_to beer.name, beer %></td>
18: <td><%= beer.style %></td>
19: <td><%= link_to beer.brewery.name, beer.brewery %></td>
20: <td><%= link_to 'Edit', edit_beer_path(beer) %></td>
(byebug) beer
#<Beer id: 13, name: "crap beer", style: "lowalcohol", brewery_id: nil, created_at: "2016-01-17 16:11:49", updated_at: "2016-01-17 16:11:49">
(byebug)
Voimme poistaa tai korjata ongelman aiheuttaneen olion suoraan debuggerista:
(byebug) beer.delete
SQL (1.3ms) DELETE FROM "beers" WHERE "beers"."id" = 13
#<Beer id: 13, name: "crap beer", style: "lowalcohol", brewery_id: nil, created_at: "2016-01-17 16:11:49", updated_at: "2016-01-17 16:11:49">
(byebug) c
Eli poistimme oluen ja jatkoimme ohjelman suorittamista komennolla c
.
Ongelman korjauduttua poistetaan debuggerin käynnistyskomento näkymätemplatesta.
Debuggerin käytöstä on niin paljon iloa, että siihen (sekä luonnollisesti myös Rails-konsolin käyttöön) kannattaa totutella välittömästi. Debuggerin etu tietyissä tilanteissa pelkkään Rails-konsoliin verrattuna on se, että debuggerisession saa avattua haluttuun kontekstiin, esim. kontrolleriin tai renderöitävissä olevaan näkymään ja näin pääsee tarkastelemaan esim. mitä muuttujien arvoja ohjelmalla on suorituksen aikana.
Railsiin on tarjolla myös oletusarvoista web-konsolia paremmin näyttöjen renderöinnissä olevissa virhetilanteissa toimivan web-konsolin tarjoava better_errors gem. Lisätään tiedostoon Gemfile seuraava
group :development, :test do
gem "better_errors"
#...
end
Suoritetaan komentoriviltä bundle install
ja käynnistetään sovellus uudelleen. Luodaan jälleen olut, johon ei liity panimoa ja mennään kaikkien oluiden sivulle.
Better_errors on muokannut virhetilanteesta kertovan sivun täysin erilaiseksi:
Better_errorsin konsolista pääsee käsiksi suoraan näkymän muuttujiin ja ongelma aiheuttava olut löytyy välittömästi.
Rails luo automaattisesti kaikille tiedostoon routes.rb määritellyille reiteille ns. polkumetodit (engl. path helper), joita hyödyntämällä sovelluksessa ei ole tarvetta kovakoodata eri sivujen osoitteita.
Esim. uuden reittauksen jälkeisen uudelleenohjauksen osoite olisi voitu ratings_path
-apufunktion sijaan kovakoodata:
def create
Rating.create params.require(:rating).permit(:score, :beer_id)
redirect_to 'ratings'
end
Kuten yleensäkin, kovakoodaus ei ole järkevää osoitteidenkaan suhteen.
Tarjolla olevia automaattisesti generoituja polkuja pääsee tarkastelemaan komentoriviltä komennolla rake routes
➜ ratebeer git:(master) ✗ rake routes
Prefix Verb URI Pattern Controller#Action
beers GET /beers(.:format) beers#index
POST /beers(.:format) beers#create
new_beer GET /beers/new(.:format) beers#new
edit_beer GET /beers/:id/edit(.:format) beers#edit
beer GET /beers/:id(.:format) beers#show
PATCH /beers/:id(.:format) beers#update
PUT /beers/:id(.:format) beers#update
DELETE /beers/:id(.:format) beers#destroy
breweries GET /breweries(.:format) breweries#index
POST /breweries(.:format) breweries#create
new_brewery GET /breweries/new(.:format) breweries#new
edit_brewery GET /breweries/:id/edit(.:format) breweries#edit
brewery GET /breweries/:id(.:format) breweries#show
PATCH /breweries/:id(.:format) breweries#update
PUT /breweries/:id(.:format) breweries#update
DELETE /breweries/:id(.:format) breweries#destroy
root GET / breweries#index
ratings GET /ratings(.:format) ratings#index
ratings_new GET /ratings/new(.:format) ratings#new
POST /ratings(.:format) ratings#create
Esim alimmat 3 reittiä kertovat seuraavaa:
- metodikutsu
ratings_path
generoi linkin, joka vie osoitteeseen "ratings" ja ohjautuu ratings-kontrollerin metodilleindex
. - metodikutsu
ratings_new_path
generoi linkin, joka vie osoitteeseen "ratings/new" ja ohjautuu ratings-kontrollerin metodillenew
. Tämä taas renderöi reittauksentekoformin ** huom. kuten ylempänä olevia reittejä vertailemalla huomaamme, eiratings_new_path
ole samanlainen kuin esim uusien oluiden luontipolku, asia korjataan myöhemmin - POST-kutsu osoitteeseen "ratings" ohjataan ratings-kontrollerin metodille
create
Kuten olemme jo huomanneet Rails 4:ssä komennon rake routes
informaatio tulee myös virhetilanteissa renderöityvälle web-sivulle. Sivu jopa tarjoaa interaktiivisen työkalun, jonka avulla voi kokeilla miten sovellus reitittää syötetyn esimerkkipolun:
Lisää kaikkien reittausten sivulle linkki uuden reittauksen tekemiseen. Lisää sovelluksen navigointipalkkiin linkki kaikkien reittausten listalle
Uuden reittauksen luominen on nyt hieman ikävää, sillä reittaajan pitää tietää oluen id. Muutetaan reittaamista siten, että käyttäjä voi valita reitattavan oluen listalta.
Jotta uuden reittauksen luontilomake pystyisi muodostamaan listan, on lomakkeen näyttämisestä huolehtivan kontrollerin haettava lista kannasta ja talletettava se muuttujaan, eli laajennetaan kontrolleria seuraavasti:
class RatingsController < ApplicationController
def new
@rating = Rating.new
@beers = Beer.all
end
# ...
end
Sivua http://guides.rubyonrails.org/form_helpers.html#making-select-boxes-with-ease konsultoimalla ja hieman kokeiluja tekemällä päädytään siihen että reittauksen luovaa lomaketta tulee muuttaa seuraavasti:
<%= form_for(@rating) do |f| %>
<%= f.select :beer_id, options_from_collection_for_select(@beers, :id, :name) %>
score: <%= f.number_field :score %>
<%= f.submit %>
<% end %>
eli lomakkeen beer_id
:n arvo generoidaan HTML lomakkeen select-elementillä, jonka valintavaihtoehdot muodostetaan näkymäapumetodilla options_from_collection_for_select
@beers
-muuttujassa olevasta oluiden listasta siten, että arvoksi otetaan oluen id ja lomakkeen käyttäjälle näytetään oluen nimi.
Huom: näkymäapumetodeja on mahdollista testata myös konsolista. Metodeja voi kutsua helper
-olion kautta:
2.2.1 :026 > beers = Beer.all
2.2.1 :027 > helper.options_from_collection_for_select(beers, :name, :id)
=> "<option value=\"Iso 3\">1</option>\n<option value=\"Karhu\">2</option>\n<option value=\"Tuplahumala\">3</option>\n<option value=\"Huvila Pale Ale\">4</option>\n<option value=\"X Porter\">5</option>\n<option value=\"Hefezeizen\">6</option>\n<option value=\"Helles\">7</option>\n<option value=\"Punk IPA\">11</option>\n<option value=\"Nanny State\">12</option>"
2.2.1 :028 >
Tee oluelle
to_s
-metodi, jonka muodostamassa tekstuaalisessa esityksessä on sekä oluen, että sen panimon nimiMuuta reittauksen luovaa lomaketta siten, että valittavista oluista näytetään nimikentän arvon sijaan olion
to_s
-metodin palauttama tekstuaalinen esitys
Tee vastaava muutos oluiden luomisesta huolehtivaan lomakkeeseen (tiedostossa views/beers/_form.html.erb) ja sen näyttämisestä vastaavaan kontrolleriin (beers#new), eli sen sijaan että luotavan oluen panimo määritellään antamalla id käsin, valitsee käyttäjä panimon listalta.
Muuta uuden oluen luomisen hoitavaa kontrolleria (beers#create) siten, että uuden oluen luomisen jälkeen selain uudelleenohjataan kaikkien oluiden listan sisältävälle sivulle (jonka osoite kannattaa generoida polkuapumetodilla). Oletusarvoisesti uudelleenohjaus tapahtuu luodun oluen sivulle komennolla
redirect_to @beer
, eli muutos tulee tähän.Scaffoldingin automaattisesti luoma lomake sisältää mm. virheiden raportointiin tarkoitettua koodia, johon tutustumme tarkemmin myöhemmin.
Tällä hetkellä luotavan oluen tyyli annetaan merkkijonona. Tulemme myöhemmin muokkaamaan sovellusta siten, että myös oluttyylit talletetaan tietokantaan.
Tehdään ensin välivaiheen ratkaisu, eli muuta sovellustasi siten, että luotavan oluen tyyli valitaan listalta, joka muodostetaan kontrollerin välittämän taulukon perusteella. Olutkontrollerin
new
-metodin koodi muuttuu siis seuraavasti:Kontrolleri
def new @beer = Beer.new @breweries = Brewery.all @styles = ["Weizen", "Lager", "Pale ale", "IPA", "Porter"] endNäkymän tulee siis generoida lomakkeeseen valintavaihtoehdot taulukon
@styles
perusteella. Vaihtoehtojen generointiin kannattaa nyt metodinoptions_from_collection_for_select
sijaan käyttää metodiaoptions_for_select
, ks. http://api.rubyonrails.org/classes/ActionView/Helpers/FormOptionsHelper.html#method-i-options_for_select
Näiden muutosten jälkeen oluen tietojen editointi ei yllättäen enää toimi. Syynä tälle on se, että uuden oluen luominen ja oluen tietojen editointi käyttävät molemmat samaa lomakkeen generoivaa näkymätemplatea (app/views/beers/_form.html.erb) ja muutosten jälkeen näkymän toiminta edellyttää, että muuttuja @breweries
sisältää panimoiden listan ja muuttuja @styles
sisältää oluiden tyylit. Oluen tietojen muutossivulle mennään kontrollerimetodin edit
suorituksen jälkeen, ja joudummekin muuttamaan kontrolleria seuraavasti korjataksemme virheen:
def edit
@breweries = Brewery.all
@styles = ["Weizen", "Lager", "Pale ale", "IPA", "Porter"]
end
Onkin hyvin tyypillistä, että kontrollerimetodit new
ja edit
sisältävät paljon samaa koodia. Olisikin ehkä järkevä ekstraktoida yhteinen koodi omaan metodiinsa.
REST (representational state transfer) on HTTP-protokollaan perustuva arkkitehtuurimalli erityisesti web-pohjaisten sovellusten toteuttamiseen. Taustaidea on periaatteessa yksinkertainen: osoitteilla määritellään haettavat ja muokattavat resurssit, pyyntömetodit kuvaavat resurssiin kohdistuvaa operaatiota, ja pyynnön rungossa on tarvittaessa resurssiin liittyvää dataa.
Lue nyt http://guides.rubyonrails.org/routing.html kohtaan 2.5 asti. Rails siis tekee helpoksi REST-tyylisen rakenteen noudattamisen. Jos kiinnostaa, RESTistä voi lukea lisää esim. täältä
Muutetaan reittauksen polut tiedostoon routes.rb siten, että käytetään valmista resources
-määrittelyä:
# kommentoi tai poista entiset määrittelyt
#get 'ratings', to: 'ratings#index'
#get 'ratings/new', to: 'ratings#new'
#post 'ratings', to: 'ratings#create'
resources :ratings, only: [:index, :new, :create]
Koska emme tarvitse reittejä delete, edit ja update, käytämme :only
-tarkennetta, jolla valitsemme vain tarvitsemamme reitit. Katsotaan nyt komentoriviltä rake routes
-komennolla (tai virheellisen urlin omaavalta web-sivulta) sovellukseen määriteltyjä polkuja:
ratings GET /ratings(.:format) ratings#index
POST /ratings(.:format) ratings#create
new_rating GET /ratings/new(.:format) ratings#new
Tulos on muuten sama kuin edellä, mutta apumetodin ratings_new_path
nimi on nyt Railsin konvention mukainen new_rating_path
.
Korvaa vielä templatessa app/views/ratings/index.erb.html käytetty vanha polkumetodikutsu uudella.
Lisätään ohjelmaan vielä mahdollisuus poistaa reittauksia. Lisätään ensin vastaava reitti muokkaamalla routes.rb:tä:
resources :ratings, only: [:index, :new, :create, :destroy]
Lisätään sitten reittauksien listalle linkki, jonka avulla kunkin reittauksen voi poistaa:
<ul>
<% @ratings.each do |rating| %>
<li> <%= rating %> <%= link_to 'delete', rating_path(rating.id), method: :delete %> </li>
<% end %>
</ul>
Railsin käyttämän konvention mukaan olion tuhoaminen tehdään HTTP:n DELETE-metodilla. Esim. jos tuhottavana on rating, jonka id on 5, tapahtuu nyt linkkiä klikkaamalla HTTP DELETE -kutsu osoitteeseen ratings/5.
Kuten jo aiemmin mainittiin, voi rating_path(rating.id)
-kutsun sijaan link_to
:n parametrina olla suoraan olio, jolle kutsu kohdistuu, eli edellinen hieman lyhemmässä muodossa:
<ul>
<% @ratings.each do |rating| %>
<li> <%= rating %> <%= link_to 'delete', rating, method: :delete %> </li>
<% end %>
</ul>
Jotta saamme poiston toimimaan, tulee vielä määritellä kontrollerille poiston suorittava metodi destroy
.
Metodiin johtava url on muotoa ratings/[tuohottavan olion id]. metodi pääsee Railsin konvention mukaan käsiksi tuhottavan olion id:hen params
-olion kautta. Tuhoaminen tapahtuu hakemalla olio tietokannasta ja kutsumalla sen metodia delete
:
def destroy
rating = Rating.find(params[:id])
rating.delete
redirect_to ratings_path
end
Lopussa suoritetaan uudelleenohjaus takaisin kaikkien reittausten sivulle. Uudelleenohjaus siis aiheuttaa sen, että selain lähettää sovellukselle uudelleen GET-pyynnön osoitteeseen /ratings, ja ratings#index-metodi suoritetaan tämän takia uudelleen.
Reittauksen poisto on nyt siinä mielessä ikävä, että herkkäsorminen sivuston käyttäjä saattaa vahinkoklikkauksella tuhota reittauksia.
Katso esim. kaikki oluet listaavan sivun templatesta /app/views/beers/index.html.erb mallia ja tee ratingin tuhoamisesta sellainen, että käyttäjältä kysytään varmistus reittauksen tuhoamisen yhteydessä.
Jos sovelluksesta poistetaan olut, jolla on reittauksia, käy niin että poistettuun olueeseen liittyvät reittaukset jäävät tietokantaan, todennäköisesti tämä aiheuttaa virheen reittausten sivun renderöinnissä.
Poista jokin olut, jolla on reittauksia ja mene reittausten sivulle. Seurauksena on virheilmoitus
undefined method `name' for nil:NilClass
Virhe taas aiheutuu siitä, että reittaus-olion
to_s
-metodissa kutsutaanbeer.name
Poista orvoksi jääneet reittaukset konsolista käsin. Yritä keksiä ensin itse komento/komennot, joiden avulla saat muodostettua orpojen reittauksen listan. Jos et keksi vastausta, ylempänä tällä sivulla on tehtävään valmis vastaus.
Olueeseen liittyvät reittaukset saadaan helposti poistettua automaattisesti. Merkitään oluen modelin koodiin has_many :ratings
yhteyteen että reittaukset ovat oluesta riippuvaisia, ja että ne tuhotaan oluen tuhoutuessa:
class Beer < ActiveRecord::Base
belongs_to :brewery
has_many :ratings, dependent: :destroy
# ...
end
Nyt orpojen ongelma poistuu.
Tee vastaava muutos panimoihin, eli kun panimo poistetaan, tulee panimoon liittyvien oluiden poistua.
Tee panimo jolla on vähintään yksi olut jolla on reittauksia. Poista panimo ja varmista, että panimoon liittyvät oluet ja niihin liittyvät reittaukset poistuvat.
Sovelluksessamme panimoon liittyy oluita ja oluisiin liittyy reittauksia. Kuhunkin panimoon siis liittyy epäsuorasti joukko reittauksia. Rails tarjoaa helpon keinon päästä panimoista suoraan käsiksi reittauksiin:
class Brewery < ActiveRecord::Base
has_many :beers
has_many :ratings, through: :beers
end
eli yhteys määritellään kuten "tietokantatasolla" oleva yhteys, mutta yhteyteen lisätään tarkenne, että se muodostuu toisten oluiden kautta. Nyt panimoilla on reittaukset palauttava metodi ratings
Lisää yhteys koodiisi ja kokeile seuraavaa konsolista (muista ensin reload!
):
2.2.1 :033 > k = Brewery.find_by name:"Koff"
2.2.1 :034 > k.ratings.count
=> 5
2.2.1 :035 > k.ratings
=> #<ActiveRecord::Associations::CollectionProxy [#<Rating id: 1, score: 10, beer_id: 1, created_at: "2016-01-17 13:09:31", updated_at: "2016-01-17 13:09:31">, #<Rating id: 2, score: 21, beer_id: 1, created_at: "2016-01-17 13:09:33", updated_at: "2016-01-17 13:09:33">, #<Rating id: 3, score: 17, beer_id: 1, created_at: "2016-01-17 13:09:35", updated_at: "2016-01-17 13:09:35">, #<Rating id: 10, score: 22, beer_id: 1, created_at: "2016-01-17 15:51:02", updated_at: "2016-01-17 15:51:02">, #<Rating id: 11, score: 34, beer_id: 1, created_at: "2016-01-17 15:51:52", updated_at: "2016-01-17 15:51:52">]>
Lisää yksittäisen panimon tiedot näyttävälle sivulle tieto panimon oluiden reittausten määrästä sekä keskiarvosta. Lisää tätä varten panimolle metodi
average_rating
reittausten keskiarvon laskemista varten.Tee reittausten yhteenlasketun määrän "kieliopillisesti moitteeton" tehtävän 6 tyyliin. Jos reittauksia ei ole, älä näytä keskiarvoa.
Panimon sivun tulisi näyttää muutoksen jälkeen suunnilleen seuraavalta (Kuvassa oluiden lista on muutettu ul-elementin avulla toteutetuksi bulletlistaksi, sivulta on myös poistettu scaffoldingin luoma 'back'-linkki. Voit halutessasi tehdä muutokset myös omaan koodiisi):
Huomaamme, että oluella ja panimolla on täsmälleen samalla tavalla toimiva ja vieläpä saman niminen metodi average_rating
. Ei ole hyväksyttävää jättää koodia tähän tilaan.
Ruby tarjoaa keinon jakaa metodeja kahden luokan välillä moduulien avulla, ks. https://github.com/mluukkai/WebPalvelinohjelmointi2016/blob/master/web/rubyn_perusteita.md#moduuli
Moduleilla on useampia käyttötarkoituksia, niiden avulla voidaan mm. muodostaa nimiavaruuksia. Nyt olemme kuitenkin kiinnostuneita modulien avulla toteutettavasta mixin-perinnästä.
Tutustu nyt riittävällä tasolla moduleihin ja refaktoroi koodisi siten, että metodi
average_rating
siirretään moduuliin, jonka luokatBeer
jaBrewery
sisällyttävät.
- sijoita moduuli lib-kansioon
- HUOM: lisää tiedostoon
config/application.rb
luokanApplication
määrittelyn sisälle riviconfig.autoload_paths += Dir["#{Rails.root}/lib"]
, jotta Rails lataisi moduulin koodin sovelluksen luokkien käyttöön. Rails server (ja konsoli) tulee käynnistää uudelleen lisäyksen jälkeen. Tämä johtuu siitä, että Rails lukee (tai suorittaa) konfiguraatiotiedostot vain käynnistyessään ja muutokset niissä (toisin kuin sovelluskoodissa) ei oteta livenä huomioon.- HUOM2: jos moduulisi nimi on ao. esimerkin tapaan
RatingAverage
tulee se Rubyn nimentäkonvention takia sijaita tiedostossarating_average.rb
, eli vaikka luokkien nimet ovat Rubyssä isolla alkavia CamelCase-nimiä, noudattavat niiden tiedostojen nimet snake_case.rb-tyyliä.
Tehtävän jälkeen esim. luokan Brewery tulisi siis näyttää suunnilleen seuraavalta (olettaen että tekemäsi moduulin nimi on RatingAverage):
class Brewery < ActiveRecord::Base
include RatingAverage
has_many :beers
has_many :ratings, through: :beers
end
ja metodin average_rating
tulisi edelleen toimia entiseen tyyliin:
irb(main):001:0> b = Beer.first
irb(main):002:0> b.average_rating
=> #<BigDecimal:7fa4bbde7aa8,'0.17E2',9(45)>
irb(main):003:0> b = Brewery.first
irb(main):004:0> b.average_rating
=> #<BigDecimal:7fa4bfbf7410,'0.16E2',9(45)>
irb(main):005:0>
Jos sovelluksessa on moduuli, jota tarvitaan ainoastaan modeleissa, on lib-hakemistoa parempi sijoituspaikka app/models/concerns. Hakemiston sisältämä koodi ladataan oletusarvoisesti modelien käyttöön eli muutosta muuttujaan config.autoload_paths
ei tarvita. Hakemistossa app/models/concerns oleviin moduuleihin on lisättävä määritelmä extend ActiveSupport::Concern
module RatingAverage
extend ActiveSupport::Concern
# ...
end
Koska määrittelemäämme moduulia ei käytetä kuin modeleissa, on sen oikeaoppinen sijoituspaikka juuri concerns-hakemisto.
Siirrä moduuli models/concerns-hakemistoon.
Lisää conserneista, ks. http://api.rubyonrails.org/classes/ActiveSupport/Concern.html ja http://stackoverflow.com/questions/14541823/how-to-use-concerns-in-rails-4
Haluamme viikon lopuksi tehdä sovelluksesta sellaisen, että ainoastaan ylläpitäjä pystyy poistamaan painimoita. Toteutamme viikolla 3 kattavamman tavan autentikointiin, teemme nyt nopean ratkaisun http basic -autentikaatiota hyödyntäen. Ks. http://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Basic.html
Tutustumme samalla nopeasti Railsin kontrollerien filtterimetodeihin ks. http://guides.rubyonrails.org/action_controller_overview.html#filters, joiden avulla voidaan helposti määritellä toiminnallisuutta, mikä suoritetaan esim. ennen (before_action) tietyn kontrollerin joidenkin metodien suorittamista.
Määrittelemme ensin panimokontrolleriin (private
-näkyvyydellä varustetun) filtterimetodin nimeltään authenticate
, joka suoritetaan ennen jokaista panimokontrollerin metodia:
class BreweriesController < ApplicationController
before_action :set_brewery, only: [:show, :edit, :update, :destroy]
before_action :authenticate
# HUOM: älä kirjoita private-määrettä tiedostoon ennen kontrollerimetodeja (index, new, ...)
private
def authenticate
raise "toteuta autentikointi"
end
end
Filtterimetodi aiheuttaa poikkeuksen, joten mennessä minne tahansa panimoita käsitteleville sivuille aiheutuu poikkeus. Varmista tämä selaimella.
Rajoitetaan sitten filtterimetodin suoritus koskemaan ainoastaan panimon poistoa:
class BreweriesController < ApplicationController
before_action :set_brewery, only: [:show, :edit, :update, :destroy]
before_filter :authenticate, only: [:destroy]
# ...
private
def authenticate
raise "toteuta autentikointi"
end
end
Varmistetaan jälleen selaimella muut sivut toimivat, mutta panimon poisto aiheuttaa virheen.
Toteutetaan sitten http-basicauth-autentikointi (ks. tarvittaessa lisää esim. täältä)
Kovakoodataan käyttäjätunnukseksi "admin" ja salasanaksi "secret":
class BreweriesController < ApplicationController
before_action :set_brewery, only: [:show, :edit, :update, :destroy]
before_filter :authenticate, only: [:destroy]
# ...
private
def authenticate
authenticate_or_request_with_http_basic do |username, password|
username == "admin" and password == "secret"
end
end
end
Ja sovellus toimii haluamallamme tavalla!
HUOM: kun olet kerran antanut oikean käyttäjätunnus-salasanaparin, ei selain kysy uusia tunnuksia mennessäsi sivulle uudelleen. Avaa uusi incognito-ikkuna jos haluat testata kirjautumista uudelleen!
Toimintaperiaatteena metodissa authenticate_or_request_with_http_basic
on se, että sovellus pyytää selainta lähettämään käyttäjätunnuksen ja salasanan, jotka sitten välitetään do
:n ja end
:in välissä olevalle koodilohkolle parametrien username
ja password
avulla. Jos koodilohkon arvo on tosi, näytetään sivu käyttäjälle.
HTTP Basic -autentikaatio on kätevä tapa yksinkertaisiin sivujen suojaamistarpeisiin, mutta monimutkaisemmissa tilanteissa ja parempaa tietoturvaa edellytettäessä kannattaa käyttää muita ratkaisuja.
Kannattaa huomata, että HTTP Basic -autentikaatiota ei tulisi käyttää kuin suojatun HTTPS-protokollan yli sillä käyttäjätunnus ja salasana lähtetään Base64-enkoodattuna, eli käytännössä kuka tahansa voi headereihin käsiksi päästyään selvittää salasanan. Hieman parempi vaihtoehto on Digest-autentikaatio, jossa käyttäjätunnuksen ja salasanan sijaan tunnistautuminen tapahtuu yksisuuntaisella funktiolla laskettavan tunnisteen avulla. Digest-autentikaation käyttäminen Railsissa on helppoa, ks. http://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Digest.html
Laajenna ratkaisua siten, että ohjelma hyväksyy myös muita kovakoodattuja käyttäjätunnus-salasana-pareja. Käytössä olevat tunnukset on kovakoodattu metodissa määriteltyyn hashiin. Metodin tulee toimia mielivaltaisen kokoisilla tunnukset sisältävillä hasheilla.
def authenticate admin_accounts = { "admin" => "secret", "pekka" => "beer", "arto" => "foobar", "matti" => "ittam"} authenticate_or_request_with_http_basic do |username, password| # do something here end endTestatessasi toiminnallisuutta, muista että joudut käyttämän incognito-selainta jos haluat kirjautua uudelleen annettuasi kertaalleen oikean käyttäjätunnus/salasanaparin.
Viikon lopuksi on taas aika deployata sovellus herokuun.
Navigoitaessa reittausten sivulle syntyy pahaenteinen virheilmoitus:
Tuotantomoodissa pyörivän sovelluksen virheiden jäljittäminen on aina hiukan vaikeampaa kuin kehitysmoodissa, jossa Rails tarjoaa sovellusohjelmoijalle monia mahdollisuuksia virheiden selvittämiseen.
Tuotantomoodissa virheiden syy täytyykin kaivaa sovelluksen lokista. Kuten viime viikolla jo mainittiin, herokussa olevan sovelluksen lokiin pääsee käsiksi komennolla heroku logs
.
Tälläkin kertaa virheen syy paljastuu:
➜ ratebeer git:(master) heroku logs
2016-01-17T17:47:20.918154+00:00 heroku[router]: at=info method=GET path="/ratings" host=enigmatic-eyrie-1511.herokuapp.com request_id=2f37ab40-cfb4-4ba3-800f-05da4e9f7ca8 fwd="87.92.42.254" dyno=web.1 connect=2ms service=19ms status=500 bytes=1754
2016-01-17T17:47:20.912789+00:00 app[web.1]: Rendered ratings/index.html.erb within layouts/application (3.0ms)
2016-01-17T17:47:20.903311+00:00 app[web.1]: Started GET "/ratings" for 87.92.42.254 at 2016-01-17 17:47:20 +0000
2016-01-17T17:47:20.912110+00:00 app[web.1]: LINE 1: SELECT "ratings".* FROM "ratings"
2016-01-17T17:47:20.912112+00:00 app[web.1]: ^
2016-01-17T17:47:20.912107+00:00 app[web.1]: PG::UndefinedTable: ERROR: relation "ratings" does not exist
2016-01-17T17:47:20.912114+00:00 app[web.1]: : SELECT "ratings".* FROM "ratings"
2016-01-17T17:47:20.914488+00:00 app[web.1]: LINE 1: SELECT "ratings"
.* FROM "ratings"
2016-01-17T17:47:20.914483+00:00 app[web.1]:
2016-01-17T17:47:20.914486+00:00 app[web.1]: ActionView::Template::Error (PG::UndefinedTable: ERROR: relation "ratings" does not exist
Tietokantataulua ratings siis ei ole olemassa. Ongelma korjaantuu suorittamalla migratiot:
heroku run rake db:migrate
Generoidaan seuraavaksi tilanne, jossa tietokanta joutuu hieman epäkonsistenttiin tilaan.
Käynnistä heroku-konsoli komennolla heroku run console
ja luo sovellukseen olut johon ei liity mitään panimoa
irb(main):002:0> Beer.create name:"crap beer", style:"lager"
=> #<Beer id: 4, name: "crap beer", style: "lager", brewery_id: nil, created_at: "2016-01-17 17:58:43", updated_at: "2016-01-17 17:58:43">
ja olut johon liittyvää panimoa ei ole olemassa (eli viiteavaimena oleva panimon id on virheellinen):
irb(main):003:0> Beer.create name:"shitty beer", style:"lager", brewery_id: 123
=> #<Beer id: 5, name: "shitty beer", style: "lager", brewery_id: 123, created_at: "2016-01-17 17:59:50", updated_at: "2016-01-17 17:59:50">
irb(main):004:0>
Kun menet nyt kaikkien oluiden on seurauksena jälleen ikävä ilmoitus "We're sorry, but something went wrong.". Jälleen kerran ongelmaa on etsittävä lokeista:
2016-01-17T18:01:30.233677+00:00 app[web.1]: Started GET "/beers" for 87.92.42.254 at 2016-01-17 18:01:30 +0000
2016-01-17T18:01:30.247271+00:00 app[web.1]: Rendered beers/index.html.erb within layouts/application (10.4ms)
2016-01-17T18:01:30.247375+00:00 app[web.1]: Completed 500 Internal Server Error in 12ms
2016-01-17T18:01:30.248723+00:00 app[web.1]:
2016-01-17T18:01:30.248726+00:00 app[web.1]: ActionView::Template::Error (undefined method `name' for nil:NilClass):
2016-01-17T18:01:30.248728+00:00 app[web.1]: 15: <tr>
2016-01-17T18:01:30.248730+00:00 app[web.1]: 16: <td><%= link_to beer.name, beer %></td>
2016-01-17T18:01:30.248731+00:00 app[web.1]: 17: <td><%= beer.style %></td>
2016-01-17T18:01:30.248733+00:00 app[web.1]: 18: <td><%= link_to beer.brewery.name, beer.brewery %></td>
2016-01-17T18:01:30.248735+00:00 app[web.1]: 19: <td><%= link_to 'Edit', edit_beer_path(beer) %></td>
2016-01-17T18:01:30.248736+00:00 app[web.1]: 20: <td><%= link_to 'Destroy', beer, method: :delete, data: { confirm: 'Are you sure?' } %></td>
2016-01-17T18:01:30.248738+00:00 app[web.1]: 21: </tr>
2016-01-17T18:01:30.248740+00:00 app[web.1]: app/views/beers/index.html.erb:18:in `block in _app_views_beers_index_html_erb__1382470185882805202_69826514779900'
Syy löytyy:
undefined method `name' for nil:NilClass
virheen aiheuttanut rivi on
<td><%= link_to beer.brewery.name, beer.brewery %></td>
eli on olemassa olut, jonka kentässä brewery
on arvona nil
. Tämä voi johtua joko siitä että oluen brewery_id
on nil
tai brewery_id
:n arvona on virheellinen (esim. poistetun panimon) id.
Kun virheen syy paljastuu, on etsittävä syylliset. Eli avataan heroku-konsoli komennolla heroku run console
ja haetaan panimottomat oluet:
irb(main):006:0> Beer.all.select{ |b| b.brewery.nil? }
=> [#<Beer id: 4, name: "crap beer", style: "lager", brewery_id: nil, created_at: "2016-01-17 17:58:43", updated_at: "2016-01-17 17:58:43">, #<Beer id: 5, name: "shitty beer", style: "lager", brewery_id: 123, created_at: "2016-01-17 17:59:50", updated_at: "2016-01-17 17:59:50">]
irb(main):007:0>
Seuraavana toimenpiteenä on virheen aiheuttavien olioiden korjaaminen. Koska loimme ne nyt itse testaamista varten, poistamme oliot (otamme ensin _
-muuttujassa olevat edellisen operaation palauttamat oliot talteen muuttujaan):
irb(main):007:0> bad_beer = _
=> [#<Beer id: 4, name: "crap beer", style: "lager", brewery_id: nil, created_at: "2016-01-17 17:58:43", updated_at: "2016-01-17 17:58:43">, #<Beer id: 5, name: "shitty beer", style: "lager", brewery_id: 123, created_at: "2016-01-17 17:59:50", updated_at: "2016-01-17 17:59:50">]
irb(main):008:0> bad_beer.each{ |bad| bad.delete }
irb(main):009:0> Beer.all.select{ |b| b.brewery.nil? }
=> []
irb(main):010:0>
Useimmiten tuotannossa vastaan tulevat ongelmat johtuvat siitä, että tietokantaskeeman muutosten takia jotkut oliot ovat joutuneet epäkonsistenttiin tilaan, eli ne esim. viittaavat olioihin joita ei ole tai viitteet puuttuvat. Sovellus kannattaakin deployata tuotantoon mahdollisimman usein, näin tiedetään että mahdolliset ongelmat ovat juuri tehtyjen muutosten aiheuttamia ja korjaus on helpompaa.
Koska kyseessä on tuotannossa oleva ohjelma, tietokannan resetointi (rake db:drop
) ei ole missään tapauksessa hyväksyttävä keino "korjata" epäkonsistenttia tietokantaa sillä tuotannossa olevaa dataa ei saa hävittää. Opettele siis heti alusta asti lukemaan lokeja ja selvittämään ongelmat kunnolla.
Commitoi kaikki tekemäsi muutokset ja pushaa koodi Githubiin. Deployaa myös uusin versio Herokuun.
Tehtävät kirjataan palautetuksi osoitteeseen http://wadrorstats2016.herokuapp.com/courses/1