Websiteentwicklung: Ruby on Rails: En zu Em 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 [1] 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) %>
Quellen
- ↑ Railscast HABTM Checkboxes (revised)