associations: mechanics (esaas §5.3) © 2013 armando fox & david patterson, all rights reserved

46
Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

Upload: tyler-baker

Post on 21-Dec-2015

218 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: 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

Page 2: 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

Page 3: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

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

Page 4: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

4

END

Page 5: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

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?

Page 6: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

6

END

Page 7: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

Through-Associations(ESaaS §5.4)

© 2013 Armando Fox & David Patterson, all rights reserved

Page 8: Associations: Mechanics (ESaaS §5.3) © 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

Page 9: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

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

Page 10: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

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

Page 11: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

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' }

Page 12: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

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

Page 13: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

13

END

Page 14: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

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:

Page 15: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

15

END

Page 16: Associations: Mechanics (ESaaS §5.3) © 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 :[email protected] << Genre.find_by_name('scifi')

genres

id

description

movies

id

name

...etc.

genres_movies

genre_id

movie_id

http://pastebin.com/tTVGtNLx

Page 17: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

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

Page 18: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

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

Page 19: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

20

END

Page 20: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

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:

Page 21: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

22

END

Page 22: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

RESTful Routes for Associations(ESaaS §5.5)

© 2013 Armando Fox & David Patterson, all rights reserved

Page 23: Associations: Mechanics (ESaaS §5.3) © 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

Page 24: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

Nested RESTful Routes

in config/routes.rb:resources :moviesbecomesresources :movies do resources :reviewsend

Nested Route: access reviews by going ”through” a movie

Page 25: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

Nested RESTful Routes

available as params[:movie_id]available as params[:id]

Page 26: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

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

Page 27: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

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

Page 28: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

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.

Page 29: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

30

END

Page 30: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

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?

Page 31: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

32

END

Page 32: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

DRYing Out Queries with Reusable Scopes

(ESaaS §5.6)

© 2013 Armando Fox & David Patterson, all rights reserved

Page 33: Associations: Mechanics (ESaaS §5.3) © 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?

Page 34: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

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

Page 35: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

36

END

Page 36: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

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?

Page 37: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

38

END

Page 38: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

Associations Wrap-Up(ESaaS §5.7-5.9)

© 2013 Armando Fox & David Patterson, all rights reserved

Page 39: Associations: Mechanics (ESaaS §5.3) © 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

Page 40: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

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

Page 41: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

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)

Page 42: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

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)

Page 43: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

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

Page 44: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

45

END

Page 45: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

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:

Page 46: Associations: Mechanics (ESaaS §5.3) © 2013 Armando Fox & David Patterson, all rights reserved

47

END