ppt - CS Course Webpages

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
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?
☐ We can say movie.reviews, but review.movie
won’t work
☐
☐ We will have no way of determining which
movie a given review is associated with
☐ All of the above
5
6
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
reviews
movies
id
moviegoer_id
id
movie_id
...
number
 moviegoer: has_many :reviews
 movie: has_many :reviews
 review: belongs_to :moviegoer
belongs_to :movie
 How to get all movies reviewed by some
moviegoer?
has_many :through
moviegoers
reviews
movies
id
moviegoer_id
id
movie_id
...
...
 moviegoer: has_many
:reviews
has_many :movies, :through => :reviews
 movie: has_many
:reviews
has_many :moviegoers, :through => :reviews
 reviews: belongs_to
:moviegoer
belongs_to :movie
Through
• Now you can do:
@user.movies # movies rated by user
@movie.users # users who rated this movie
• My potato scores for R-rated movies
@user.reviews.select {
|r| r.movie.rating == 'R' }
has_many :through
moviegoers
reviews
movies
id
moviegoer_id
id
movie_id
...
...
@user.movies
SELECT * FROM movies
JOIN moviegoers ON
reviews.moviegoer_id = moviegoers.id
JOIN movies ON
reviews.movie_id = movies.id
13
Which of these, if any, is NOT a correct way
of saving a new association, given m is an
existing movie:
☐ Review.create!(:movie_id=>m.id, :potatoes=>5)
☐
☐ m.reviews << Review.new(:potatoes=>5)
m.save!
☐ All will work
14
15
Shortcut: Has and Belongs to Many
(habtm)
• join tables express a relationship between
existing model tables using FKs
• Join table has no primary key http://pastebin.com/tTVGtNLx
• because there’s no object being represented!
movie has_and_belongs_to_many :genres
genre has_and_belongs_to_many :movies
@movie.genres << Genre.find_by_name('scifi')
genres
genres_movies
id
genre_id
description
movie_id
movies
id
name
...etc.
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
18
18
HABTM Naming Conventions
M-M relationship naming convention:
if a Bar
has_and_belongs_to_many :foos
then a Foo
has_and_belongs_to_many :bars
and the database table is the
plural AR names in alphabetical order
bars_foos
19
20
We want to model students having
appointments with faculty members. Our
model would include which relationships:
☐ Faculty has-many appointments,
Student has-many appointments
☐
☐ Faculty belongs-to appointment,
Student belongs-to appointment
☐ Faculty has-many appointments,
through Students
21
22
RESTful Routes for
Associations
(ESaaS §5.5)
© 2013 Armando Fox & David Patterson, all rights reserved
Creating/Updating ThroughAssociations
• 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 :movies
becomes
resources :movies do
resources :reviews
end
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.xml
def 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'
end
end
ReviewsController#new
# GET /movies/1/reviews/new
def 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.new
end
• Another possibility: do it in a before-filter
before_filter :lookup_movie
def 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
If we also have
moviegoer has_many reviews,
can we use moviegoer_review_path() as a
helper?
☐ Yes, it should work as-is because of convention
over configuration
☐
moviegoers
reviews
routes.rb
☐ No, because there can be only one RESTful
route to any particular resource
☐ No, because having more than one
through-association involving Reviews
would lead to ambiguity
31
32
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
1
2
3
4
5
6
7
# in controller:
def good_movies_for_kids
@m = Movie.for_kids.with_good_reviews(3)
end
# in view:
- @m.each do |movie|
%p= pretty_print(movie)
Where do database queries happen?
☐ Line 3 only
☐
☐ Line 3 AND lines 6-7
☐ Depends on return value of for_kids
37
38
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 to
simplify complex
queries & 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
lambda { Review.find(review_id) }.should
raise_error(ActiveRecord::RecordNotFound)
end
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
If using the DataMapper pattern and you
want to do one-to-many associations, you
can expect:
to have to write the association methods
☐
yourself
☐
☐ Worse scalability
☐ All of the above are possible
46
47