Download - Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved
Associations: Mechanics(ESaaS §5.3)
© 2013 Armando Fox & David Patterson, all rights reserved
How Does It Work?
• Models must have attribute for foreign key of owning object– e.g., movie_id in reviews table
• ActiveRecord manages this field in both database & in-memory AR object
• Don’t manage it yourself!– Harder to read– May break if database schema doesn’t follow
Rails conventions
Rails Cookery #4
To add a one-to-many association:
1.Add has_many to owning model and belongs_to to owned model
2.Create migration to add foreign key to owned side that references owning side
3.Apply migration
4.rake db:test:prepare to regenerate test database schema
4
END
We will get a database error when trying to save a Review
We will have no way of determining which movie a given review is associated withAll of the above
We can say movie.reviews, but review.movie won’t work
☐
☐
☐
☐
5
Suppose we have setup the foreign key movie_id in reviews table. If we then add has_many :reviews to Movie, but forget to put belongs_to :movie in Review, what happens?
6
END
Through-Associations(ESaaS §5.4)
© 2013 Armando Fox & David Patterson, all rights reserved
Many-to-Many Associations
• Scenario: Moviegoers rate Movies– a moviegoer can have many
reviews– but a movie can also have many reviews
• Why can’t we use has_many & belongs_to?• Solution: create a new AR model to model
the multiple association
Many-to-Many
moviegoer: has_many :reviews movie: has_many :reviews review: belongs_to :moviegoer belongs_to :movie
How to get all movies reviewed by some moviegoer?
reviews
moviegoer_id
movie_id
number
movies
id
...
moviegoer
id
has_many :through
moviegoer: has_many :reviewshas_many :movies, :through => :reviews
movie: has_many :reviews has_many :moviegoers, :through => :reviews
reviews: belongs_to :moviegoer belongs_to :movie
reviews
moviegoer_id
movie_id
...
movies
id
...
moviegoers
id
Through
• Now you can do:@user.movies # movies rated by [email protected] # users who rated this movie
• My potato scores for R-rated [email protected] { |r| r.movie.rating == 'R' }
has_many :through
@user.moviesSELECT * FROM movies JOIN moviegoers ON reviews.moviegoer_id = moviegoers.id JOIN movies ON reviews.movie_id = movies.id
reviews
moviegoer_id
movie_id
...
movies
id
...
moviegoers
id
13
END
r = m.reviews.build(:potatoes => 5)r.save!
m.reviews << Review.new(:potatoes=>5)m.save!
All will work
Review.create!(:movie_id=>m.id, :potatoes=>5)☐
☐
☐
☐
14
Which of these, if any, is NOT a correct way of saving a new association, given m is an existing movie:
15
END
Shortcut: Has and Belongs to Many (habtm)
• join tables express a relationship between existing model tables using FKs
• Join table has no primary key• because there’s no object being represented! movie has_and_belongs_to_many :genres genre has_and_belongs_to_many :[email protected] << Genre.find_by_name('scifi')
genres
id
description
movies
id
name
...etc.
genres_movies
genre_id
movie_id
http://pastebin.com/tTVGtNLx
Rules of Thumb
• If you can conceive of things as different real-world objects, they should probably be distinct models linked through an association
• If you don’t need to represent any other aspect of a M-M relationship, use habtm
• Otherwise, use has_many :through
1818
19
HABTM Naming Conventions
M-M relationship naming convention: if a Bar
has_and_belongs_to_many :foos
then a Foohas_and_belongs_to_many :bars
and the database table is the plural AR names in alphabetical order
bars_foos
20
END
Faculty HABTM Students,Students HABTM Faculty
Faculty belongs-to appointment,Student belongs-to appointmentFaculty has-many appointments, through Students
Faculty has-many appointments, Student has-many appointments
☐
☐
☐
☐
21
We want to model students having appointments with faculty members. Our model would include which relationships:
22
END
RESTful Routes for Associations(ESaaS §5.5)
© 2013 Armando Fox & David Patterson, all rights reserved
Creating/Updating Through-Associations
• When creating a new review, how to keep track of the movie and moviegoer with whom it will be associated?– Need this info at creation time– But route helpers like new_movie_path (provided
by resources :movies in routes file) only “carry around” the ID of the model itself
Nested RESTful Routes
in config/routes.rb:resources :moviesbecomesresources :movies do resources :reviewsend
Nested Route: access reviews by going ”through” a movie
Nested RESTful Routes
available as params[:movie_id]available as params[:id]
ReviewsController#create
# POST /movies/1/reviews# POST /movies/1/reviews.xmldef create # movie_id because of nested route @movie = Movie.find(params[:movie_id]) # build sets the movie_id foreign key automatically @review = @movie.reviews.build(params[:review])
if @review.save flash[:notice] = 'Review successfully created.' redirect_to(movie_reviews_path(@movie)) else render :action => 'new' endend
ReviewsController#new
# GET /movies/1/reviews/newdef new # movie_id because of nested route @movie = Movie.find(params[:movie_id]) # new sets movie_id foreign key automatically @review ||= @movie.reviews.new @review = @review || @movie.reviews.newend
• Another possibility: do it in a before-filter before_filter :lookup_moviedef lookup_movie @movie = Movie.find_by_id(params[:movie_id]) || redirect_to movies_path, :flash => {:alert => "movie_id not in params"}end
Views
%h1 Edit
= form_tag movie_review_path(@movie,@review), :method => :put do |f|
...Will f create form fields for a Movie or a Review?
= f.submit "Update Info"
= link_to 'All reviews for this movie', movie_reviews_path(@movie)
• Remember, these are for convenience. Invariant is: review when created or edited must be associated with a movie.
30
END
Yes, but we must declare reviews as a nested resource of moviegoers in routes.rb
No, because there can be only one RESTful route to any particular resourceNo, because having more than one through-association involving Reviews would lead to ambiguity
Yes, it should work as-is because of convention over configuration
☐
☐
☐
☐
31
If we also have moviegoer has_many reviews,can we use moviegoer_review_path() as a helper?
32
END
DRYing Out Queries with Reusable Scopes
(ESaaS §5.6)
© 2013 Armando Fox & David Patterson, all rights reserved
“Customizing” Associations with Declarative Scopes
• Movies appropriate for kids?• Movies with at least N reviews?• Movies with at least average review of N?• Movies recently reviewed?• Combinations of these?
Scopes Can Be “Stacked”
Movie.for_kids.with_good_reviews(3)
Movie.with_many_fans.recently_reviewed
• Scopes are evaluated lazily!
http://pastebin.com/BW40LAHX
36
END
Lines 6-7 only
Line 3 AND lines 6-7
Depends on return value of for_kids
Line 3 only☐
☐
☐
☐
37
1 # in controller:2 def good_movies_for_kids3 @m = Movie.for_kids.with_good_reviews(3)4 end5 # in view:6 - @m.each do |movie|7 %p= pretty_print(movie)
Where do database queries happen?
38
END
Associations Wrap-Up(ESaaS §5.7-5.9)
© 2013 Armando Fox & David Patterson, all rights reserved
Associations Wrap-Up
• Associations are part of application architecture– provides high-level, reusable association constructs
that manipulate RDBMS foreign keys– Mix-ins allow Associations mechanisms to work
with any ActiveRecord subclass
• Proxy methods provide Enumerable-like behaviors– A many-fold association quacks like an Enumerable– Proxy methods are an example of a design pattern
• Nested routes help you maintain associations RESTfully - but they’re optional, and not magic
Elaboration: DataMapper
• Data Mapper associates separate mapper with each model– Idea: keep mapping independent of particular data store
used => works with more types of databases– Used by Google AppEngine– Con: can’t exploit
RDBMS features tosimplify complexqueries & relationships
41
Referential Integrity
• What if we delete a movie with reviews?– movie_id field of those reviews then refers to
nonexistent primary key– another reason primary keys are never recycled
• Various possibilities depending on app...– delete those reviews?has_many :reviews, :dependent => :destroy
– make reviews “orphaned”? (no owner) has_many :reviews, :dependent => :nullify
• Can also use lifecycle callbacks to do other things (e.g., merging)
Testing Referential Integrity
it "should nuke reviews when movie deleted" do @movie = @movie.create!(...)
@review = @movie.reviews.create!(...) review_id = @review.id @movie.destroy
end
lambda { Review.find(review_id) }.should raise_error(ActiveRecord::RecordNotFound)
Advanced Topics
• Single-Table Inheritance (STI) & Polymorphic Associations
• Self-referential has_many :through• Many declarative options on manipulating
associations (like validations) • To learn (much) more:
– http://guides.rubyonrails.org/association_basics.html
– The Rails Way, Chapter 9
45
END
Better scalability
Worse scalability
All of the above are possible
to have to write the association methods yourself
☐
☐
☐
☐
46
If using the DataMapper pattern and you want to do one-to-many associations, you can expect:
47
END