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