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
BearbeitenDie 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
BearbeitenBevor 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
BearbeitenWir 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)