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

Post on 21-Dec-2015

218 Views

Category:

Documents

0 Downloads

Preview:

Click to see full reader

TRANSCRIPT

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

@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 :movies@movie.genres << 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

top related