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 have no way of determining which movie a given review is associated with All 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 :reviews has_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 # movies rated by # users who rated this movie My potato scores for R-rated { |r| r.movie.rating == 'R' }
has_many SELECT * 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
m.reviews 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
Has and Belongs to Many (ESaaS §5.4) © 2013 Armando Fox & David Patterson, all rights reserved
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 << Genre.find_by_name('scifi') genres id description movies id name...etc. genres_movies genre_id movie_id
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
19 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
20 END
Faculty belongs-to appointment, Student belongs-to appointment Faculty 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 :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 = Movie.find(params[:movie_id]) # build sets the movie_id foreign key flash[:notice] = 'Review successfully created.' else render :action => 'new' end
ReviewsController#new # GET /movies/1/reviews/new def new # movie_id because of nested = Movie.find(params[:movie_id]) # new sets movie_id foreign key ||= end Another possibility: do it in a before-filter before_filter :lookup_movie def = Movie.find_by_id(params[:movie_id]) || redirect_to movies_path, :flash => {:alert => "movie_id not in params"} end
Views %h1 Edit = form_tag :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', Remember, these are for convenience. Invariant is: review when created or edited must be associated with a movie.
30 END
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 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!
36 END
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_kids = Movie.for_kids.with_good_reviews(3) 4 end 5 # in view: 6 do |movie| 7 %p= pretty_print(movie) Where do database queries happen?
38 END
Associations Wrap-Up (ESaaS § ) © 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" = 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: – htmlhttp://guides.rubyonrails.org/association_basics. html –The Rails Way, Chapter 9
45 END
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