Websiteentwicklung: Ruby on Rails/ Druckversion
Dieser Text ist sowohl unter der „Creative Commons Attribution/Share-Alike“-Lizenz 3.0 als auch GFDL lizenziert.
Eine deutschsprachige Beschreibung für Autoren und Weiternutzer findet man in den Nutzungsbedingungen der Wikimedia Foundation.
Vorwort
[Bearbeiten]Lieber Leser, liebe Leserin,
Ruby on Rails (RoR) ist ein mächtiges Framework für die Entwicklung von Webapplikationen. Dieses Buch bietet einen schnellen Einblick in RoR - wenn man sich mit HTML, CSS, Javascript und einer serverseitigen Sprache wie z.B. PHP schon auskennt. Diese Version des Buches beschreibt Rails 3.2.6
Die Beispiel-Applikation, die im Laufe des Buches entwickelt wird, ist auch online:
Es gibt noch ein wesentlich umfangreicheres Wikibook Ruby on Rails für die Vertiefung.
Viel Spaß beim Lernen und Ausprobieren!
Was ist Ruby on Rails
"Ruby on Rails" besteht aus zwei Teilen: Ruby ist eine Open Source Skriptsprache, ähnlich wie Perl oder Python. Ruby kann in verschiedenen Bereichen verwendet werden. Rails ist ein umfangreiches Web-Framework für diese Sprache.
Was zeichnet Ruby aus?
[Bearbeiten]- dynamische Sprache
- hoch-dynamische Community
Was zeichnet Rails aus?
[Bearbeiten]DRY - Don't Repeat Yourself
[Bearbeiten]Rails versucht Code-Verdopplungen zu vermeiden. Man schreibt immer sehr kurze Codes, und man kann viel damit erreichen.
Convention over Configuration
[Bearbeiten]Rails hat viele, viele Konventionen. z.B. wo die Dateien gespeichert werden, wie die Datenbanken heißen, u.s.w.
Wenn man sich an die Konventionen hält ist das Leben einfach, man hat wenig Arbeit, und alles funktioniert. (Wenn man sich nicht an die Konventionen halten will, kann man sich das Leben beliebig schwierig machen.)
Model - View - Controller
[Bearbeiten]Die Idee von Model - View - Controller wird in vielen Web-Applikationen verwendet. Es handelt sich um einen Pattern, der schon lange Zeit für die Erstellung von Benutzer-Interfaces verwendet wird.
Was MVC für ein eine Web-Applikation bedeutet, sei an einem konkreten Beispiel erklärt: Wir wollen ein Kursverwaltungssystem programmieren, in dem Leute Kurse buchen können.
- Das Model
- ist ein Modell unseres Problemfelds. Für die Kursverwaltung brauchen wir also Kurse, User, Buchungen, etc. Diese sind in einer Datenbank gespeichert, in Tabellen namens kurse, user, buchungen. Für die Programmierung werden diese Datenbank-Tabellen auf Klassen umgelegt: in der Kurs-Klasse gibt es mehrere Kurs-Objekte. Dies erfolgt automatisch; wir brauchen - im Idealfall - keine einzige Zeile SQL zu schreiben.
- Die View
- behandelt alle Probleme der Darstellung. Die View ist also eine Datei die hauptsächlich HTML, eventuell CSS und Javascript. Ein bisschen Ruby-Code sorgt für die Details der Anzeige, z.B. eine Schleife über alle User oder eine bedingte Anzeige: nur zukünftige Kurse.
- Der Controller
- nimmt die Daten aus der URL und/oder die Parameter entgegen, ruft das richtige Modell auf, bereitet die richtigen Daten für die View vor... er hält also alles zusammen. Achtung: der Controller kann weder HTML noch SQL enthalten - das ist nicht seine Aufgabe!
Tests
[Bearbeiten]Rails unterstützt Dich beim Schreiben von automatisierten Tests. Damit kann man sehr gut Test Driven Development (TDD) machen: erst den Test schreiben, dann erst das Programm dazu.
Wer verwendet Ruby on Rails?
[Bearbeiten]Scribd und Slideshare sind wohl die berühmtesten Rails-Applikationen. In Europa verwendet XING (in Hamburg) Rails.
Warum sollte ich 'nicht' RoR verwenden?
[Bearbeiten]Für sehr kleine Web-Applikationen ist der Einsatz eines großen Frameworks wie Rails nicht sinnvoll.
Die Sprache Ruby
Ruby ist eine Skriptsprache.
Der Interpreter
[Bearbeiten]Du kannst den Ruby-Interpreter direkt aufrufen und Befehle testen:
D:\>irb irb(main):001:0> 1+1 => 2 irb(main):002:0> exit
Ein einfaches Skript
[Bearbeiten]Ruby-Programme speichert man in Dateien mit der Endung .rb Hier ein einfaches Programm, nenne die Datei hallo.rb
puts "Hallo Welt"
Nun kannst Du das Programm auf der Kommendozeile starten:
d:/>ruby hallo.rb hallo Welt
Datentypen, Kontrollstrukturen, Klassen
[Bearbeiten]Wie in Skriptsprachen üblich verwendet man ein und dieselbe Variable für verschiedene Datentypen.
Im Unterschied zu anderen Skriptsprachen führt Ruby kaum automatische Typ-Konvertierung durch.
....
Ein paar einfache Beispiele:
https://gist.github.com/3462049
Eine Ruby on Rails Applikationen von Außen
Woran erkennt man eine Rails-Applikation?
Betrachten wir dazu die Demo-Site von Redmine, einer Projektmanagement-Applikation die in Ruby geschrieben ist.
URLs
[Bearbeiten]Die URLs einer Rails-Applikation haben meist keine Endung, oft keine Parameter. Statt dessen ist die Information direkt im Pfad:
http://demo.redmine.org/projects/learnrails/boards http://demo.redmine.org/projects/learnrails/boards/453 http://demo.redmine.org/projects/learnrails/boards/454 http://demo.redmine.org/projects/learnrails/boards/455
Dahinter stecken aber nicht Ordner in einem Filesystem, sondern diese URL wird auf Controller, Actions und Parameter umgeschrieben.
der Kommandozeilen-Befehl rake routes zeigt die Regeln dafür an:
GET /projects/:project_id/boards {:controller=>"boards", :action=>"index" } GET /projects/:project_id/boards/:id {:controller=>"boards", :action=>"show" }
Die action index zeigt dabei eine Liste (der Boards) an, die action show zeigt ein einzelnes Board an.
Für fortgeschrittene ProgrammiererInnen
[Bearbeiten]REST
[Bearbeiten]Diese Struktur der URLs ist aber nur der offensichtlichste Teil eines tiefergehenden Plans. Nicht nur GET, sondern auch die anderen HTTP-Methoden werden verwendet:
GET /dings {:controller=>"dings", :action=>"index" } # zeigt eine liste aller dinge POST /dings {:controller=>"dings", :action=>"create" } # erzeugt ein neues ding (id steht noch nicht fest) GET /dings/new {:controller=>"dings", :action=>"new" } # zeigt ein Eingabeformular für ein ding GET /dings/:id/edit {:controller=>"dings", :action=>"edit" } # zeigt ein Bearbeitungsformular für ein bestehendes ding GET /dings/:id {:controller=>"dings", :action=>"show" } # zeigt ein bestimmtes ding an PUT /dings/:id {:controller=>"dings", :action=>"update" } # speichert/ändert ein bestehendes ding DELETE /dings/:id {:controller=>"dings", :action=>"destroy"} # löscht ein ding
Das ist die Grundstruktur die für verschiedene Ressourcen immer wieder gleiche verwendet werden kann. Die Idee dahinter ist REST[1].
Webservice
[Bearbeiten]Mit der Endung .xml kann die Rails-Applikation auch XML als Output liefern:
GET /dings.xml POST /dings.xml GET /dings/new.xml GET /dings/:id/edit.xml GET /dings/:id.xml PUT /dings/:id.xml DELETE /dings/:id.xml
Damit bietet die Web-Applikation automatisch eine REST Webservice Schnittstelle an. Das kann man für AJAX verwenden oder als API für die Applikation.
(Oder man ignoriert es erst mal.)
Erste Rails App
Die Rails Applikation erzeugen
[Bearbeiten]Bei der Arbeit mit Ruby on Rails spielt nicht nur der eigentliche Source-Code, sondern auch die Kommandozeile eine wichtige Rolle. Schon der erste Schritt passiert auf der Kommandozeile: um die Grund-Struktur der Rails-Applikation zu erzeugen wird der rails-Befehl aufgerufen:
rails new hallowelt cd hallowelt
Damit wird eine Reihe von Ordnern und Dateien erzeugt:
Die Kommandozeile wird auch weiterhin (mehrfach) gebraucht. (Ab jetzt sind alle Befehle im Hauptordner der App abzusetzen.)
Das Programm bundle installiert die richtigen Gems
bundle install
Das Programm rake führt Befehle aus, die im Rakefile definiert sind. Bevor wir starten können, müssen wir die Datenbank mit folgendem Befehl erzeugen:
rake db:migrate
Nun können wir den Webserver auf der Kommandozeile starten:
rails server
Nun kann ich mir die App am localhost auf port 3000 im Browser ansehen:
http://localhost:3000/
Scaffolding
[Bearbeiten]Nach dem Model-View-Controller Prinzip müssen wir für unsere Datenbank der Hallo-Welt Meldungen
- eine Tabelle in der Datenbank anlegen
- ein Model erzeugen
- einen Controller (mit eventuell verschiedenen Actions) erzeugen
- für die Actions jeweils Views erzeugen
Rails hilft uns, das Alles auf einmal zu machen.
Scaffold erzeugen
[Bearbeiten]Dazu verwenden wir das generate-script und erzeugen damit ein scaffold - ein Gerüst:
rails generate scaffold Hallo von:string meldung:text farbe:string
Das erste Argument hallo ist der Name des Modells, danach folgen die Eigenschaften und Datentypen des Modells.
In diesem Fall will ich zu jedem "hallo" abspeichern: von dem die Meldung ist, den Text der Meldung und welche Farbe die Meldung haben soll.
Nun müssen wir noch die Datenbank-Tabelle wirklich anlegen, das geschieht mit
rake db:migrate
Die Datenbank
[Bearbeiten]Mit dem Befehl
rails dbconsole
startet man eine shell für die Datenbank. Hier kann man z.B. mit
.schema hallos
die gerade angelegte Tabelle betrachten:
CREATE TABLE "hallos" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "von" varchar(255), "meldung" text, "farbe" varchar(255), "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL );
Als Name für das Modell hatten wir Hallo gewählt, rails hat automatisch die Mehrzahl für die Bezeichnung der Tabelle gewählt.
Ein Primärschlüssel namens id und die beiden Felder created_at und updated_at wurden ebenfalls automatisch angelegt.
Die Datentypen wurden auch von Ruby auf Datenbank übersetzt: aus string wurde varchar(255).
Das Modell in app/models/hallo.rb
[Bearbeiten]Die Klassen-Datei für das Modell wurde angelegt, ist aber noch leer.
Hier könnten wir Methoden eintragen die die innere Logik des Modells abbilden.
Für die Validierung der Daten bietet Rails viele Praktische Kurz-Schreibweisen an. Wenn wir z.B. sicherstellen wollen, dass als Farbe nur ein HTML-Farbcode wie #FF0033 gespeichert werden kann, dann reicht dazu eine Zeile im Modell:
validates :farbe, :format => { :with => /^#......$/, :message => "nur Farb-Code mit # und 6 Stellen (hexadezimal) erlaubt" }
Dazu gibt es einen Rails Guide [2] Validations |
Das erste Argument ist der Name der Spalte / der Eigenschaft, mit dem benannten Argument :with gibt man eine Regular Experssion zur Überprüfung an, und mit :message eine passende Fehlermeldung.
Der Controller in app/controllers/hallos_controller.rb
[Bearbeiten]Diese Datei ist umfangreich: sie enthält 7 vorgefertigte Actions:
- index - listet alle Meldungen auf
- show - zeigt eine bestimmte meldung an (ID notwendig!)
- new - zeigt ein Eingabeformular für eine neue Meldung an (weiter bei create)
- edit - zeigt eine Meldung in einem Bearbeitungsformular an (ID notwendig!, weiter bei update)
- create - erzeugt eine neue Meldung
- update - verändert eine Meldung
- destroy - löscht eine Meldung (ID notwendig!)
Betrachten wir die einfachste Action: show zeigt eine bestimmte Meldung an. Beim Aufruf dieser Action muss eine id als Parameter übergeben werden:
def show @hallo = Hallo.find(params[:id]) respond_to do |format| format.html # show.html.erb format.json { render json: @hallo } end end
Über den Parameter id wird der Datensatz Hallo als Objekt geladen und in der Instanzvariable @hallo des Controllers gespeichert.
Die Action funktioniert aber nicht nur für den Fall, dass Sie HTML als Output liefern soll. Auch JSON als Output ist vorgesehen. Dafür sind 4 weiter Zeile Code notwendig:
def show @hallo = Hallo.find(params[:id]) respond_to do |format| format.html # show.html.erb format.json { render json: @hallo } end end
Routing - von der URL zur Action
[Bearbeiten]Mit dem Kommandozeilen-Befehl rake routes kann man sehen wie URLs, Controller, Actions zusammen hängen. Dies wird in der Datei config/routes.rb festgelegt, wurde aber durch das Scaffolding automatisch erledigt. Hier der Output von rake routes:
hallos GET /hallos(.:format) {:action=>"index", :controller=>"hallos"} POST /hallos(.:format) {:action=>"create", :controller=>"hallos"} new_hallo GET /hallos/new(.:format) {:action=>"new", :controller=>"hallos"} edit_hallo GET /hallos/:id/edit(.:format) {:action=>"edit", :controller=>"hallos"} hallo GET /hallos/:id(.:format) {:action=>"show", :controller=>"hallos"} PUT /hallos/:id(.:format) {:action=>"update", :controller=>"hallos"} DELETE /hallos/:id(.:format) {:action=>"destroy", :controller=>"hallos"}
Ganz links ist für manche Routes ein Name angegeben, den man innerhalb von Views und Controllern verwenden kann. In der zweiten Spalte finden Sie die HTTP Methode, anschießend die URL und zuletzt die Kombination von Controller und Action die hier aufgerufen werden.
Die erste Route hallos ruft die action index auf und alle Hallo-Meldungen werden angezeigt.
Die route mit dem namen hallo (3.von unten) bedeutet also: Wenn mit GET ein Aufruf von http://localhost:3000/hallos/14 erfolgt, dann wird der Controller hallos mit Action show aufgerufen. Wo :id steht kann also eine Zahl eingesetzt werden. Wo :format steht kann json eingefügt werden: Wenn mit GET ein Auftruf von http://localhost:3000/hallos/14.json erfolgt, dann wird der Controller hallos mit Action show aufgerufen, er soll dann JSON als Output produzieren.
Nun wäre ein guter Zeitpunkt die Applikation auszuprobieren und ein paar Meldungen einzufügen, wieder zu verändern oder zu löschen
Die Views in app/views/hallos/*.html.erb
[Bearbeiten]Vier Views wurden automatisch angelegt:
- index.html.erb - listet alle Meldungen auf
- show.html.erb - zeigt eine bestimmte meldung an (ID notwendig!)
- new.html.erb - zeigt ein Eingabeformular für eine neue Meldung an (weiter bei create)
- edit.html.erb - zeigt eine Meldung in einem Bearbeitungsformular an (ID notwendig!, weiter bei update)
Das Format der Views ist Embedded Ruby (Endung .erb). Eine View besteht hauptsächlich aus HTML, mit <% ... %> kann Ruby eingebettet werden das ausgeführt wird. Mit <%= ... %> kann Ruby eingebettet werden das Output liefert. Dabei werden hauptächlich die Instanz-Variablen des Controllers verwendet.
Die View show.html.erb zeigt eine Meldung an.
<p id="notice"><%= notice %></p> <p> <b>Von:</b> <%=h @hallo.von %> </p> <p> <b>Meldung:</b> <%=h @hallo.meldung %> </p> <p> <b>Farbe:</b> <%=h @hallo.farbe %> </p> <%= link_to 'Edit', edit_hallo_path(@hallo) %> | <%= link_to 'Back', hallos_path %>
Die beiden letzten Zeilen erzeugen zwei Links. Das erste Argument von link_to ist der Link-Text. Das zweite Argument ist die URL. Hier werden Methoden aufgerufen, die automatisch für alle benannten routes vorhanden sind (siehe rake routes).
Die route hallo braucht keine Parameter, mit hallo_path erhält man die URL ohne Hostnamen, mit hallo_url die vollständige URL.
Bei edit_hallo ist es etwas komplizierter: diese Route muss wissen, welches Objekt gemeint ist. Hier kann man als Argument entweder das ganze Objekt oder nur die id des Objekts übergeben. Auch hier gibt es wieder die Formen edit_hallo_path und edit_hallo_url.
Die Darstellung der Meldung können wir ganz verändern: Die Farbe wird wirklich als Farbe verwendet und nicht als Text angezeigt
<div class="meldung"> <p><%=h @hallo.von %> sagt:</p> <blockquote style="color: <%= @hallo.farbe %>"><%=h @hallo.meldung %></blockquote> </div> <%= link_to 'Edit', edit_hallo_path(@hallo) %> | <%= link_to 'Back', hallos_path %>
Verändern Sie auch index.html.erb so, dass die Farbe wirklich als Farbe verwendet wird! Achtung: in der index-View gibt es zwei Variablen: @hallos ist ein Array aller Meldungen, hallo ist die jeweils aktuelle Meldung.
Eine neue Homepage einsetzen
[Bearbeiten]Der hallo-Controller wäre ein sehr gut Homepage für unsere 'Hallo Welt' Applikation. Um das zu erreichen, muss man die Datei public/index.html löschen und in config/routes.rb folgende Zeile einfügen:
root :to => "hallos#index"
Damit ist die erste Webapplikation in Ruby on Rails fertig.
- ↑ Tilkov(2007): A Brief Introduction to REST. In: InfoQ.
- ↑ Rails Guide Validations
Konfiguation und Datenbank
Wenn man ein Projekt mit
rails new projektname
startet, speichert Rails die Daten in einer sqlite-Datenbank. Das ist besonders einfach: alle Daten sind in der Datei db/development.sqlite3 zu finden.
Wenn man das Projekt gleich mit
rails new -d mysql projektname
startet wird statt dessen mysql verwendet. Nun wollen wir uns näher mit der Konfiguration befassen und dabei lernen wie man ein laufendes Projekt auf ein mysql-Datenbank umstellt.
Konfigurations-Dateien
[Bearbeiten]Im Ordner config sind u.a. folgende Dateien zu finden:
- database.yml
- Konfiguration der Datenbanken für die verschiedenen Server
- routes.rb
- Konfiguation der URLs und ihrer Umrechnung auf Controller und Actions
- environment.rb
- weitere Konfiguration
- locales/en.yml
- Texte für die englische Version der Website (hier könnte man de.yml, it.yml, u.s.w. einfügen)
Datenbank
[Bearbeiten]Die Datei database.yml enthält normalerweise folgenden Code:
development: adapter: sqlite3 database: db/development.sqlite3 pool: 5 timeout: 5000 # test: adapter: sqlite3 database: db/test.sqlite3 pool: 5 timeout: 5000 # production: adapter: sqlite3 database: db/production.sqlite3 pool: 5 timeout: 5000
Hier sehen wir dass in Rails von Anfang an drei verschiedene Server vorgesehen sind:
- development
- ist die Datenbank für die Entwicklung, wenn ich das Programm schreibe und ausprobiere
- test
- ist die Datenbank für die automatischen Tests. Diese Datenbank wird auch automatisch erzeugt und wieder gelöscht
- production
- ist die Datenbank für den Echt-Betrieb
Wenn ich mysql statt sqlite verwenden will muss ich einerseits entsprechende Datenbanken
in meinem MySQL Server anlegen und andererseits die Konfigurationsdatei ändern. Hier
ein Beispiel für das Projekt 'chor': (Achtung beim Kopieren dieses Codes: diese Konfigurationsdatei
verträgt keine Tabulatoren und keine Leerzeichen vor development, test, production!)
development: adapter: mysql encoding: utf8 reconnect: false database: chor_development pool: 5 username: root password: host: localhost # test: adapter: mysql encoding: utf8 reconnect: false database: chor_test pool: 5 username: root password: host: localhost # production: adapter: mysql encoding: utf8 reconnect: false database: chor_production pool: 5 username: root password: host: localhost
Die Konvention, an den Applikationsnamen mit Unterstrich etwas anzuhängen um den Datenbanknamen zu erzeugen
hat eine angenehme Nebenwirkung: in phpmyadmin werden die Datenbanken gruppiert.
Datenbank und Model
Wie arbeiten Datenbank und Model zusammen?
Datenbank verändern mit Migrations
[Bearbeiten]Die Problemstellung
[Bearbeiten]Bei Web-Applikationen tritt folgendes Problem auf: im Laufe der Entwicklung ändert sich das Datenbank-Schema. Gleichzeitig ist die Applikation aber schon im Echt-Betrieb, man kann die Produktions-Datenbank nicht einfach löschen und von Grund auf neu bauen.
Es ist also notwendig, die Veränderungs-Schritte der Datenbank zu speichern, und auf allen Servern (verschiedene Entwicklungs-Rechner, Test, Produktion) in der gleichen Reihenfolge nachzuvollziehen.
Ruby on Rails bietet dafür eine Lösung: Migrations.
Eine Migration ist dabei ein Schritt der Veränderung - wobei dieser Schritt immer in beide Richtungen definiert ist.
Im Ordner /db/migrations/ sind die einzelnen Migrations gespeichert.
Nach der Erzeugung des scaffolds für hallo sieht das z.B. so aus:
20090829142637_create_hallos.rb
Diese Datei enthält das Anlegen der Tabelle für das Model Hallo:
class CreateHallos < ActiveRecord::Migration def self.up create_table :hallos do |t| t.string :von t.text :meldung t.string :farbe t.timestamps end end def self.down drop_table :hallos end end
Eine Migration erstellen
[Bearbeiten]Mit dem Kommandozeilen-Befehl
↯rake db:migration↯
rake db:migrate (siehe auch rake -T [korrigiert uwe.schimon@mailpost.de])
werden jeweils alle neuen Migrations ausgeführt. Mit
rake db:rollback
kann der letzte Schritt rückgängig gemacht werden. Welcher Migrations-Schritt bereits angewandt wurde wird in der Datenbank salbst gespeichert, in der Tabelle schema_migrations
Wenn man die Datenbank verändern will - zum Beispiel um eine neue Spalte in die Tabelle einzufügen - so kann man selbst eine migration erzeugen:
rails generate migration AddWiederholungToHallo wiederholung:integer
Aus der Namens-Konvention erzeugt der Generator gleich einen guten Entwurf für die Migration:
class AddWiederholungToHallo < ActiveRecord::Migration def self.up add_column :hallos, :wiederholung, :integer end
def self.down remove_column :hallos, :wiederholung end end
Was noch zu tun ist
[Bearbeiten]Dazu gibt es einen Rails Guide [1] Migrations |
Nach dem Hinzufügen oder Löschen von Spalten aus der Tabelle muss das Model nicht verändert werden!
Die Spalten werden automatisch als Attribute des Objekts übernommen. Der Controller muss meist auch nicht verändert werden.
Nur die Views (Eingabemasken, Anzeige des Objekts) müssen verändert werden!
Daten lesen und speichern mit dem Model
[Bearbeiten]Wir arbeitet das Model mit der Datenbank zusammen? Das werden wir nun interaktiv erforschen
Daten Suchen und Finden
[Bearbeiten]Es gibt eine interaktive Shell für Rails. Gestartet wird sie mit
rails console
Nun kann man ruby-befehle eintippen und sieht sofort das Ergebnis.
>> Hallo.find(1) => #<Hallo id: 1, von: "Brigitte", meldung: "Hallo Welt, es freut mich sehr Dich zu sehen!", farbe: "#FF0000", created_at: "2009-08-29 14:28:54", updated_at: "2009-08-29 14:28:54">
Die Klasse Hallo hat (wie jedes Klasse des Models) eine Methode find. Mit find(1) suche ich nach dem Objekt das in der Datenbank die id=1 hat.
Mit find(:all) suche ich nach allen Objekten. Das gibt viel output (hier gekürzt!)
>> Hallo.find(:all) => [#<Hallo id: 1, von: "Brigitte">, #<Hallo id: 2, von: "Karin">, #<Hallo id: 3, von: "Hilmar">]
Daten anlegen
[Bearbeiten]Hier kann man auch neue Objekte anlegen:
>> h = Hallo.new => #<Hallo id: nil, von: nil, meldung: nil, farbe: nil, created_at: nil, updated_at: nil> >> h.von = "Tippse" => "Tippse" >> h.meldung = "Auch ohne das Web kann ich einfügen" => "Auch ohne das Web kann ich einf\201gen" >> h => #<Hallo id: nil, von: "Tippse", meldung: "Auch ohne das Web kann ich einf\201gen", farbe: nil, created_at: nil, updated_at: nil>
das Objekt existiert derzeit nur im Hauptspeicher, im laufenden Programm - es ist noch nicht in der Datenbank gespeichert. (Deswegen hat es auch noch keine id). mit save kann ich versuchen das Objekt zu speichern:
>> h.save => false
Der Rückgabewert false bedeutet: das Speichern hat nicht funktioniert. Warum? Weil die Farbe nicht gespeichert werden kann, ich habe ja eine Validierung auf dem Farb-Feld Programmiert:
# im Model validates_format_of :farbe, :with => /^#......$/, :message => "nur Farb-Code mit # und 6 Stellen (hexadezimal) erlaubt"
Also muss ich die Farbe korrekt setzen und kann dann speichern:
>> h.farbe="#00FF00" => "#00FF00" >> h.save => true
Danach hat h auch eine id, und werte in created_at und updated_at:
>> h => #<Hallo id: 7, von: "Tippse", meldung: "Auch ohne das Web kann ich einf\201gen", farbe: "#00FF00", created_at: "2009-08-31 11:46:06", updated_at: "2009-08-31 11:46:06">
Controller und View
- ↑ Rails Guide Migrations
- ↑ Rails API ActiveRecord Migration
Views, Partials und Layouts
Dazu gibt es einen Rails Guide [1] Layouts and Rendering |
Beim Schreiben der View ist Dir vielleicht aufgefallen, dass nirgendwo ein head oder body zu lesen ist. Diese Teile der Webseite, die für die einzelnen Seiten der Applikation gleich sind, werden nicht in der View selbst gespeichert:
Layout
[Bearbeiten]Für eine einfache Webapplikation brauchen wir nur ein Layout: es heisst application.html.erb.
Die vom Scaffold erstellte Datei enthält HTML5, und an vier Stellen Ruby:
- ein Link zum (gesammelten) Stylesheet
- ein Link zum (gesammelten) Javascript
- ein Tag der Code einfügt, der Cross Site Request Forgeries verhindert
- der Befehl yield, hier wird die View eingefügt
Eine Veränderung im Layout kann beim Verständnis der Applikation helfen: Im Titel geben wir automatisch den Controller und die Action aus:
<title><%= controller.controller_name %> : <%= controller.action_name %></title>
Partials
[Bearbeiten]Einfaches Partial
[Bearbeiten]In den Views edit.html.erb und new.html.erb wird genau das selbe Formular verwendet. Dieses Formular ist deswegen nur einmal gespeichert, in einem Partial. In beiden Views steht nur
<%= render "form" %>
Achtung: damit wird die Datei _form.html.erb geladen! (Beachte den Unterstrich!)
Das Partial selbst enthält einfach den Code mit dem Formular.
Partial mit Schleife
[Bearbeiten]Die Darstellung der hallo-Meldungen in show und index können wir auch vereinheitlichen. Wir erzeugen ein Partial _meldung.html.erb', das eine einzelne Meldung darstellt. Diese Meldung ist in der Varialbe meldung zu finden - das ergibt sich aus dem Dateinamen des Partials:
<div class="meldung"> <p><%=h meldung.von %> sagt:</p> <blockquote><%=h meldung.meldung %></blockquote> </div>
Dieses Partial kann nun in der View von show einfach aufgerufen werden:
<%= render :partial => "meldung", :object => @hallo %>
In der View von index war bisher eine Schleife über das Array @hallos nötig. Dies entfällt mit dem Aufruf des Partials mit dem Argument :collection:
<%= render :partial => "meldung", :collection => @hallos %>
Absolute Links und URLs vermeiden
[Bearbeiten]Wir haben schon verschiedene Methoden kennen gelernt, wie wir vermeiden können fixe URLs zur programmieren:
<%= link_to 'Edit', edit_hallo_path(@hallo) %> | <%= link_to 'Back', hallos_path %> <%= stylesheet_link_tag "application", :media => "all" %>
Analoges gibt es auch noch für Bilder:
<%= image_tag("rails.png") %>
Für Javascript gibt es auch eine Standard-Einstellung, damit wird normalerweise jquery geladen:
<%= javascript_include_tag "application" %>
Hinter all diesen Tags steckt fortgeschrittene Magie: die Asset Pipeline.
Dazu gibt es einen Rails Guide [3] Asset Pipeline |
Weitere Quellen
- Auf Englisch: Rails Guides
- Auf Englisch: Buch Rails for PHP Developers
jQuery
Rails unterstützt die Verwendung der Javascript-Library jQuery.
Beispiel
[Bearbeiten]Bei der Anzeige der Meldung sollen die Bearbeitungs-Links (show, edit, destroy) unsichtbar sein bis man mit der Maus über die Meldung fährt. Dazu wird der HTML-Code nicht verändert, nur zwei Klassen werden eingeführt: sensor bezeichnet den Bereich der auf die Maus reagieren soll, hide_until_hover muss innerhalb von sensor sein und bezeichnet den Bereich der versteckt wird.
Dazu noch der etwas veränderte Code der hallos/index view:
<div class="meldung sensor"> <p><%=h hallo.von %> sagt: <span class="hide_until_hover">( <%= link_to 'Show', hallo_comments_path(hallo) %> | <%= link_to 'Edit', edit_hallo_path(hallo) %> | <%= link_to 'Destroy', hallo, method: :delete, data: { confirm: 'Are you sure?' } %> )</span></p> <blockquote><%=h hallo.meldung %></blockquote> </div>
Da dieses Javascript nur in den Views von "Hello" gebraucht wird, fügen wir in die
Datei app/assets/javascripts/hallos.js ein:
<script> $(document).ready(function() { $('.sensor .hide_until_hover') .hide(); $('.sensor') .mouseover( function(){ $('.hide_until_hover', this).show(); }) .mouseout( function(){ $('.hide_until_hover', this).hide(); }) ; }); </script>
Hinweis: es existiert eine leere Datei app/assets/javascripts/hallos.js.coffee. Hier
könnte man CoffeeScript einfügen, dass dann in einem separaten Schritt zu Javascript kompiliert wird.
Man kann diese Datei einfach löschen, wenn man lieber direkt in Javascript arbeiten will.
Testen
Dazu gibt es einen Rails Guide [4] Testing |
Bei Web-Applikationen, die nicht nur einmal veröffentlicht, sondern auch dauernd weiter entwickelt werden, ist es besonders wichtig automatische Tests zu haben: Wenn ich ein neues Feature einbaue kann ich anschließend die Tests laufen lassen, und somit sicher sein, dass die Applikation noch funktioniert.
Die Test-Umgebung
[Bearbeiten]Im Ordner test findet man den Code den man für Tests schreibt. Ein weiterer Teil der Test-Umgebung ist die Datenbank, die in config/database.yml definiert wurde.
Bevor man Tests startet muss man die Test-Datenbank auf den neuesten Stand bringen:
rake db:migrate rake db:test:load
Unit Tests
[Bearbeiten]Die Unit-Tests testen die einzelnen Klassen des Models. In unserem sehr einfachen Beispiel-Programm gibt es noch nicht viel zu testen:
In der Datei \test\unit\hallo_test.rb:
def test_hallo_ohne_farbe_kann_man_nicht_speichern h = Hallo.new assert !h.save end def test_hallo_mit_farbe_kann_man_speichern h = Hallo.new h.farbe = '#ff0000' assert h.save end
Um diesen Test durchzuführen muss man auf der Kommandozeile in den ordner test wechseln:
cd test ruby unit\hallo_test.rb
Functional Tests
[Bearbeiten]Diese Tests prüfen die Funktion der Controller. Durch das scaffolding wurde schon für jede Action ein Test erstellt. Den Test kann man wieder direkt starten:
ruby functional\hallos_controller_test.rb
In dieser Datei ist die Schreibweise etwas anders: jeder Test beginnt mit dem Wort 'test' und einem Namen als String.
test "should show hallo" do get :show, :id => hallos(:one).to_param assert_response :success end
hallos(:one) ist ein verweis auf eine "Fixture". Das sind Test-Daten, die vor dem Testlauf aus einer Datei in die Datenbank geladen werden.
So könnte die Datei \test\fixtures\hallo.yml aussehen:
one: von: Brigitte meldung: Hallo Welt farbe: "#ff0000"
Achtung: dies ist wieder eine Datei im yaml-Format (so wie database.yml), die sehr empfindlich auf einrückungen reagiert!
Damit wird ein Datensatz definiert, der von den Test-Dateien aus als hallos(:one) erreichbar ist.
Integration Tests
[Bearbeiten]Alle bisherigen Tests betrachten Details: nur ein Modell, oder nur einen Schritt beim Aufruf über das Web. Für das "große Ganze" gibt es noch Integration Tests. Diese verwendet man zum Beispiel, um zu testen ob das Login funktioniert, und ein User nach dem Login bestimmte Recht hat - also Szenarien mit mehreren Schritten.
Integration Tests werden nicht automatisch erzeugt.
Ausblick: Cucumber
[Bearbeiten]Mit Cucumber[5] kann man die intendierte Funktion der Webseite auf "Pseudo-englisch" oder "Pseudo-Deutsch" beschreiben. Damit eignet es sich gut um mit Nicht-Programmierern gemeinsam die Tests zu schreiben.
Wenn alle Tests erfolgreich durchlaufen wird Cucumber grün - deswegen ist es nach der Gurke benannt:
Assoziationen
Dazu gibt es einen Rails Guide [6] Active Record Associations |
Zwei Modelle sollen eine Beziehung haben, zum Beispiel: zu jeder unserer Hello-Meldungen soll es mehrere Comments geben können, jedes Comment gehört zu genau einem Hallo. Dies nennt man eine 1:n Assoziationen.
Modell und Datenbank
[Bearbeiten]Diese Beziehung passiert auf zwei Ebenene: im Modell und in der Datenbank.
In den beiden Modellen wird die Beziehung so aussehen:
class Comment < ActiveRecord::Base belongs_to :hallo attr_accessible :text end class Hallo < ActiveRecord::Base has_many :comments, :dependent => :destroy attr_accessible :farbe, :meldung, :von end
In der Datenbank gibt es einen Fremdschlüssel hallo_id in der comment-Tabelle.
Wenn man das gleich von Anfang an bedenkt kann man diesen Schlüssel schon beim scaffold mit erzeugen lassen:
rails generate scaffold Comment text:text hallo_id:integer rake db:migrate
Unter der URL http://localhost.3000/comments/ kann man jetzt Comments anlegen
Die Objekte sind auf foglende Weise miteinander verbunden: man kann von einem comment immer zu seinem hallo gelangen und umgekehrt:
@hallo.comments # ---> Array aller Comments @comment.hallo # ---> ein Hallo
Routes
[Bearbeiten]Bei einer belongs_to/has_many Beziehung macht es Sinn, die URLs zu verschachteln: Ein Comment ist immer einem Hallo zugeordnet.
hallo_comments GET /hallos/:hallo_id/comments(.:format) comments#index POST /hallos/:hallo_id/comments(.:format) comments#create new_hallo_comment GET /hallos/:hallo_id/comments/new(.:format) comments#new edit_hallo_comment GET /hallos/:hallo_id/comments/:id/edit(.:format) comments#edit hallo_comment GET /hallos/:hallo_id/comments/:id(.:format) comments#show PUT /hallos/:hallo_id/comments/:id(.:format) comments#update DELETE /hallos/:hallo_id/comments/:id(.:format) comments#destroy hallos GET /hallos(.:format) hallos#index POST /hallos(.:format) hallos#create new_hallo GET /hallos/new(.:format) hallos#new edit_hallo GET /hallos/:id/edit(.:format) hallos#edit hallo GET /hallos/:id(.:format) hallos#show PUT /hallos/:id(.:format) hallos#update DELETE /hallos/:id(.:format) hallos#destroy root / hallos#index
Dies erreicht man mit folgender Änderung in config/routes.rb
resources :hallos do resources :comments end
Nun können wir die Controller und Views ausprobieren: die URL zum Einstieg lautet http://localhost:3000/hallo/1/comments
Controller
[Bearbeiten]Im comments_controller erhalten wir immer den Parameter hallo_id. Wir können also bei jeder einzelnen Action folgende erste Zeile eintragen:
@hallo = Hallo.find(params[:hallo_id])
Statt das 7mal einzutragen gibt es eine einfachere Möglichkeit: einen before_filter. Die im before_filter angegebene Funktion wird vor jeder Action ausgeführt. Da die Funktion in eine Instanz-Variable schreibt (erkennbar am @ vor dem Variablennamen) ist die Information dann auch in der Action verfügbar.
class CommentsController < ApplicationController before_filter :get_hallo ... # am Ende der Klasse kommen die privaten Funktionen: private def get_hallo @hallo = Hallo.find(params[:hallo_id]) if @hallo.nil? then raise "Welches Hallo?" end end end
Die Methoden create muss auch angepasst werden: das Kommentar wird nicht einfach als Objekt der Klasse Comment erzeugt, sondern wir verwenden die build Methode des Objekts @hello:
@comment = @hallo.comments.build(params[:comment])
Im index wollen wir nicht alle comments der Datenbank (Comments.all) anzeigen, sondern
nur diejenigen, die zum akutellen Hallo gehören:
def index @comments = @hallo.comments ...
Neue URLs in Views und Controller
[Bearbeiten]In den Views des comment-Controllers kann man jetzt auch @hallo lesen.
In den Views muss man viele Pfade ändern. Ein Beispiel:
aus comment_path() wird hallo_comment_path(@hallo)
Am einfachsten ist es sich die Routes anzeigen zu lassen, um heraus zu finden wie die neue Version der einzelnen Methoden lautet.
Vorher:
<td><%= link_to 'Show', comment %></td> <td><%= link_to 'Edit', edit_comment_path(comment) %></td> <td><%= link_to 'Destroy', comment, method: :delete, data: { confirm: 'Are you sure?' } %></td>
Nachher:
<td><%= link_to 'Show', hallo_comment_path(@hallo, comment)' %></td> <td><%= link_to 'Edit', edit_hallo_comment_path(@hallo,comment) %></td> <td><%= link_to 'Destroy', hallo_comment_path(@hallo,comment), method: :delete, data: { confirm: 'Are you sure?' } %></td>
Analog für form_for: auch das Formular muss einen längern Pfad benutzen:
<%= form_for([@hallo,@comment]) do |f| %>
Das Eingabefeld für hallo_id kann man dafür ersatzlos streichen: der Parameter hallo_id wird aus der URL übernommen.
Wenn man nun ein neues Comment speichern will gibt es ein Problem mit dem Controller:
# POST /comments # POST /comments.json def create @comment = Comment.new(params[:comment])
respond_to do |format| if @comment.save format.html { redirect_to @comment, notice: 'Comment was successfully created.' } format.json { render json: @comment, status: :created, location: @comment } else format.html { render action: "new" } format.json { render json: @comment.errors, status: :unprocessable_entity } end end end
Das Erzeugen und Abspeichern des Comments funktioniert schon richtig, das Problem tritt danach auf:
format.html { redirect_to @comment, notice: 'Comment was successfully created.' }
Hier soll zur show-action des neu angelegten Comments umgeleitet werden. Da Comments nun eine verschachtelte Ressource ist, müssen wir die entsprechende URL verwenden:
format.html { redirect_to hallo_comment_path(@hallo,@comment), notice: 'Comment was successfully created.' }
Vereinfachen
Dazu gibt es einen Rails Guide [7] Layouts and Rendering in Rails |
Die Views, die das Scaffolding für unsere automatisch erzeugt hat, sind nicht miteinander verbunden: Hallos linkt zur zu Hallos-Views, Comments zur zu Comments-Views. Das ist nicht gut zu bedienen, und eigentlich unpraktisch
Nun wollen wir diese Views vereinfachen, und leichter bedinbar machen.
Welche View bleibt?
[Bearbeiten]Durch die Vereinfachung werden einige Views wegfallen - aber welche? Betrachent wir folgende routes:
0 root / umgeleitet auf hallos#index 1 hallos GET /hallos hallos#index Liste aller Hallos 2 hallo GET /hallos/:id hallos#show Anzeigen eines Hallos, ohne Kommentare 3 hallo_comments GET /hallos/:hallo_id/comments comments#index Liste der Kommentare zu einem Hallo 4 hallo_comment GET /hallos/:hallo_id/comments/:id comments#show Ein Kommentar
Nummer 2 und Nummer 3 würden sich beide eigenen, um ein Hallo mit allen Kommentaren
dazu anzuzeigen. Betrachten wir an die dahinter liegenden Controller: im comments_controller
wird @hallo schon gefunden und geladen. D.h. es ist einfach, es auch in der View anzuzeigen.
Im hallos_controller werden keine Kommentare geladen. Deswegen ist es einfacher, Nummer 2 nicht weiter
zu verwenden, und überall durch Nummer 3 zu ersetzen.
View zeigt Hallo und Kommentare
[Bearbeiten]Wir können in die view comments/index.html.erb einfach Code zur Darstellung des Hallos einfügen:
<h1>Listing comments</h1> <p><%= @hallo.von %> sagt:</p> <blockquote><%= @hallo.meldung %></blockquote>
Neu verlinken
[Bearbeiten]In der View hallos/index.html.erb können wir nun die Show-Links verändern: statt
hallo_path( hallo )
auf
hallo_comments_path( hallo )
User und Login
In diesem Kapitel werden wir eine App um eine n:m Beziehung erweitern: Ausgangspunkt ist tvtogether, eine App die Serien und Folgen von tvrage.com laden kann.
In diesem Kapitel werden wir als ersten Schritt in die App die Möglichkeit einbauen, dass User sich anmelden, einloggen und ausloggen. Dabei unterstützt uns Rails 3 sehr, wir brauchen nur wenig selbst zu Programmieren.
Diese App werden wir um ein Model 'user' erweitern. Zwischen users und episodes besteht eine n:m Beziehung: eine Person kann mehrere Episoden gesehen haben, eine Episode wurde von mehreren Personen gesehen.
User und Authentisierung
[Bearbeiten]Wir werden ein Model "User" brauchen, das Namen, E-Mail und das gehashte Passwort gespeichert.
Ausserdem werden wir den Hash session verwenden. Dieser wird von Rails zur Verfügung gestellt. Daten die wir hier speichern sind noch vorhanden, wenn vom gleichen Browser auf die nächste Seite aufgerufen wird. (Das wird mittels Cookies erreicht, diese werden automatisch von Rails gesetzt)
User
[Bearbeiten]Dazu gibt es einen Railscast [8] Authentication in Rails 3.1 |
Diese Funktionalität braucht ein zusätzliches gem: Wir fügen in das Gemfile ein:
gem 'bcrypt-ruby'
und führen danach einmal
bundle install
aus. Damit wird das gem installiert. Damit es in der App verwendet wird, müssen wir den Webserver stoppen (mit STRG-C) und neu starten
rails server
Nun legen wir das User-Model an.
rails g model user name:string email:string password_digest:string rake db:migrate
Dem Model fügen wir eine Zeile hinzu:
class User < ActiveRecord::Base has_secure_password validates_presence_of :password, :on => :create attr_accessible :name, :email, :password, :password_confirmation end
Diese eine Zeile fügt sehr viel Automatik ein: Methoden zum Setzen und Verifizieren des eingegeben Passworts, validations. In der Datenbank wird dabei nie das Original-Passwort gespeichert, sondern nur eine gehashte Version. Damit sich neue User anmelden können brauchen wir einen Controller:
rails g controller users
Der Code des Controllers ist kurz:
class UsersController < ApplicationController def new @user = User.new end def create @user = User.new(params[:user]) if @user.save redirect_to root_url, :notice => "Erfolgreich angemeldet!" else render "new" end end end
Und dann brauchen wir noch eine View zur Anmeldung:
<h1>Anmeldung</h1> <%= form_for @user do |f| %> <% if @user.errors.any? %> <div class="error_messages"> <h2>Fehler bei der Anmeldung</h2> <ul> <% for message in @user.errors.full_messages %> <li><%= message %></li> <% end %> </ul> </div> <% end %> <div class="field"> <%= f.label :name %><br> <%= f.text_field :name %> </div> <div class="field"> <%= f.label :email %><br> <%= f.text_field :email %> </div> <div class="field"> <%= f.label :password %><br> <%= f.password_field :password %> </div> <div class="field"> <%= f.label :password_confirmation %><br> <%= f.password_field :password_confirmation %> </div> <div class="actions"><%= f.submit %></div> <% end %>
Session, Login, Logout
[Bearbeiten]Wie oben erwähnt ist die session ein Hash in Rails der in jedem Controller zugänglich ist, ähnlich wie params. Wir werden nun aber so tun, als ob sesssion ein model wäre, und eine View und einen Controller dafür anlegen.
Das Login-Formular wird as view sessions/new gespeichert. Dies ist das erste Formular, das nicht mit form_for erstellt wird, sonder mit from_tag. Der Unterschied: form_for ist immer an ein Model gebunden, mit form_tag kann man beliebige Formulare ganz unabhängig von Datenbank und Model, erzeugen.
<h1>Login</h1> <%= form_tag sessions_path do %> <div class="field"> <%= label_tag :email %> <%= text_field_tag :email, params[:email] %> </div> <div class="field"> <%= label_tag :password %> <%= password_field_tag :password %> </div> <div class="actions"><%= submit_tag "Log in" %></div> <% end %>
Zu dieser View gehört ein Controller:
rails g controller sessions
Der Controller hat 3 Actions, eine ist hier noch nicht vollständig implementiert:
class SessionsController < ApplicationController def new end def create # ...... if # ..... session[:user_id] = user.id redirect_to root_url, :notice => "Logged in!" else flash.now.alert = "Falsche E-Mail or falsches Passwort" render "new" end end def destroy session[:user_id] = nil redirect_to root_url, :notice => "Logged out!" end end
Wenn das Login erfolgreich ist speichern wir die user_id in der Session - und nicht mehr.
Wie können wir nun prüfen ob der Login erfolgreich ist? Zuerst holen wir den user mit der Passenden E-Mail aus der Datenbank. Mit der authenticate-Methode des user-Objekts können wir prüfen ob das Passwort passt:
user = User.find_by_email(params[:email]) if user && user.authenticate(params[:password])
Damit wir in jedem Controller und jeder View die Information zur Verfügung haben ob jemand eingeloggt ist legen wir im applications_controller eine helper-Funktion an:
class ApplicationController < ActionController::Base protect_from_forgery private def current_user @current_user ||= User.find(session[:user_id]) if session[:user_id] end helper_method :current_user end
Navigation mit Login und Logout
[Bearbeiten]Ähnlich wie hier in Wikibooks soll auch in der App tvtogether oben auf jeder Seite angezeigt werden ob mein eingloggt ist, bzw. Links für Login und Logout angeboten werden.
Das kann man in das Layout einfügen: app/views/layouts/application.html.erb
<nav> <% if current_user.nil? %> <%= link_to "Login", new_session_path %> <% else %> Eingeloggt als <%= current_user.name %>. <%= link_to "Logout", session_path( session ), :method => :delete %> <% end %> </nav>
n:m Beziehung
In diesem Kapitel werden wir weiter an der App tvtogether arbeiten. Zwischen users und episodes besteht eine n:m-Beziehung: eine Person kann mehrere Episoden gesehen haben, eine Episode wurde von mehreren Personen gesehen.
Dazu gibt es einen Railscast [9] HABTM Checkboxes (revised) |
In einer relationalen Datenbank braucht man für die Darstellung einer n:m-Beziehung eine Zwischentabelle. Es es sinnvoll, aus dieser Zwischentabelle ein sogenanntes "Join Model" zu machen.
Join Model
[Bearbeiten]Die drei beteiligten Modelle haben dabei folgende Beziehungen:
class A < ActiveRecord::Base has_many :ABs has_many :Bs, through: :ABs end class AB < ActiveRecord::Base belongs_to :A belongs_to :B end class B < ActiveRecord::Base has_many :ABs has_many :As, through: :ABs end
Wie soll das Join-Modell in unserem Projekt heißen? hat_gesehen? seen? oder
denken wir schon weiter zu einem Rating: meinung? rating?
Ich habe mich dann für rating entschieden:
rails g model rating user_id:integer episode_id:integer rating:integer rake db:migrate
So sehen die Models aus:
class User < ActiveRecord::Base has_many :ratings has_many :episodes, :through => :ratings ... end class Ratings < ActiveRecord::Base belongs_to :user belongs_to :episode validates :rating, :inclusion => { :in => [0,1,2,3,4,5] }, :allow_nil => true end class Episode < ActiveRecord::Base has_many :ratings has_many :users, :through => :ratings ... end
Join Model auf der Console
[Bearbeiten]Bevor wir mit dem Join-Model etwas programmieren, erforschen wir es interaktiv auf der Rails-Console:
$ rails console Loading development environment (Rails 3.2.6) irb(main):001:0> u = User.first User Load (0.1ms) SELECT "users".* FROM "users" LIMIT 1 => #<User id: 1, name: "B", email: "b@gmail.com", password_digest: "$2a$10..."> irb(main):002:0> e = Episode.last Episode Load (0.2ms) SELECT "episodes".* FROM "episodes" ORDER BY "episodes"."id" DESC LIMIT 1 => #<Episode id: 1658, tvshow_id: 9, epnum: 195, seasonnum: 10, title: "To The Boy in the Blue Knit Cap"> irb(main):003:0> u.ratings Rating Load (0.1ms) SELECT "ratings".* FROM "ratings" WHERE "ratings"."user_id" = 1 => [#<Rating id: 1, user_id: 1, episode_id: 1658, rating: nil>] irb(main):004:0> u.episodes Episode Load (0.2ms) SELECT "episodes".* FROM "episodes" INNER JOIN "ratings" ON "episodes"."id" = "ratings"."episode_id" WHERE "ratings"."user_id" = 1 => [#<Episode id: 1658, tvshow_id: 9, epnum: 195, seasonnum: 10, title: "To The Boy in the Blue Knit Cap">] irb(main):005:0> u.episode_ids => [1658]
Die letzte Schreibweise, mit den ids, kann man auch für Zuweisungen verwenden:
irb(main):007:0> u.episode_ids = [1650, 1651, 1652] Episode Load (0.4ms) SELECT "episodes".* FROM "episodes" WHERE "episodes"."id" IN (1650, 1651, 1652) (0.1ms) begin transaction SQL (0.3ms) DELETE FROM "ratings" WHERE "ratings"."user_id" = 1 AND "ratings"."episode_id" = 1658 SQL (15.1ms) INSERT INTO "ratings" ("created_at", "episode_id", "rating", "updated_at", "user_id") VALUES (?, ?, ?, ?, ?) SQL (0.1ms) INSERT INTO "ratings" ("created_at", "episode_id", "rating", "updated_at", "user_id") VALUES (?, ?, ?, ?, ?) SQL (0.1ms) INSERT INTO "ratings" ("created_at", "episode_id", "rating", "updated_at", "user_id") VALUES (?, ?, ?, ?, ?) (0.8ms) commit transaction => [1650, 1651, 1652]
Hier werden also eventuell vorhandene Ratings gelöscht, und dann für die drei neuen Episoden Ratings eingefügt.
Controller und View
[Bearbeiten]Wir bauen nun eine edit_user View, die immer eine Liste aller Episoden anzeigt. Bei jeder Episode gibt es eine Checkbox die für die Rating-Beziehung steht.
Hier ein erster Entwurf der View:
<h1>Fernsehen mit <%= @user.name %></h1> <%= form_for(@user) do |f| %> <div class="field"> <%= f.label :name %><br /> <%= f.text_field :name %> </div> <div class="field"> <% Episode.all.each do |ep| %> <label> <%= check_box_tag "user[episode_ids][]", ep.id %> <%= ep.tvshow.name %> <%= ep.epnum %> <%= ep.title %> </label><br/> <% end %> </div> <div class="actions"> <%= f.submit %> </div> <% end %>
Die params die dieses Formular liefert sehen z.B. so aus:
"user"=>{ "name"=>"Brigitte", "episode_ids"=>["1666", "1668", "1706", "1707", "1781"] }
So werden die Checkboxes im user_controller verarbietet: die episode_ids müssen gar nicht extra behandelt werden:
def update @user = User.find(params[:id]) if @user.update_attributes(params[:user]) redirect_to @user, notice: "Erfolgreich gespeichert" else render :edit end end
Nur im user-Model müssen sie als attr_accessible eingetragen werden:
class User < ActiveRecord::Base has_many :ratings has_many :episodes, :through => :ratings, :order => "episodes.tvshow_id" has_secure_password validates_presence_of :password, :on => :create attr_accessible :name, :email, :password, :password_confirmation, :episode_ids end
Was das Formular noch nicht kann: die Checkboxes sind nie bereits angeklickt, auch nicht wenn in der Datenbank bereits ein Rating exisitert.
Bei Formularfelder die über den from_for Tag erzeugt ist das immer automatisch gegeben: Sie spiegeln den Zustand des Models wieder.
Diesen Checkboxes müssen wir "von Hand" setzen:
<%= check_box_tag "user[episode_ids][]", ep.id, @user.episode_ids.include?(ep.id) %>
Anhang: Installation unter Windows
Anhang: Referenz
Weitere Quellen
- Auf Englisch: Rails Guides
- Auf Englisch: Buch Rails for PHP Developers
Verwendete Quellen
[Bearbeiten]- ↑ Rails Guide Layouts and Rendering
- ↑ Rails API AssetTagHelper
- ↑ Rails Guide Asset Pipeline
- ↑ Rails Guide Testing
- ↑ Hellesøy: Cucumber
- ↑ Rails Guide Active Record Associations
- ↑ Rails Guide Layouts and Rendering in Rails
- ↑ Railscast Authentication in Rails 3.1
- ↑ Railscast HABTM Checkboxes (revised)