Skip to content

Instantly share code, notes, and snippets.

@grant-roy
Last active September 3, 2015 18:42
Show Gist options
  • Save grant-roy/ef0c0ebd5ded314ce818 to your computer and use it in GitHub Desktop.
Save grant-roy/ef0c0ebd5ded314ce818 to your computer and use it in GitHub Desktop.
AR walkthough exercises

###AR Exercise walk-through

#create a new rails project named 'ar'
$ rails new ar -T -d postgresql

#run bundle
$ bundle

#create Band model
$ rails g model Band gig:string pay:string date:timestamp

#create Artist model
$ rails g model Artist name:string

#initialize the database
$ rake db:create

#run the 1st migration to add the models to our schema
#(and create the actual tables in the database)
$ rake db:migrate

At this stage we could either drop into a text editor and build controllers or views for our models, or use rails console to play around with the models a bit. We'll mess around in the console for a bit at this point

#open up the console
$ rails c

#create a new Band entry in the db
> Band.create(gig:'Malarkys',pay:'$100.00',date:Date.new)
> Band.create(gig:'Sams',pay:'$50.00',date:Date.new)

#get all the bands back,notice the class type: ActiveRecord::Relation
#note that ActiveRecord::Relation is an iterable class, meaning you can
#loop over it
> Band.all

#find a band by id
> Band.find(1)

#find two bands with two id's 
> Band.find([1,2])

#return the band where gig='Malarkys'
> Band.where(gig:'Malarkys')

#see if a Band record exists where gig='Staples'  a boolean is returned
> Band.exists?(gig:'Staples')   

#now try it again
> Band.exists?(gig:'Sams')

#try now simply returning a field, not a whole record
> Band.select("date").where(pay:'$50.00' )

#now try it knowing it will fail so you can see the different result
> Band.select("date").where(pay:'$' )

#lets also take first for a spin
> Band.first

#and why not last as well
> Band.last

#importantly, let's also try ordering by date
> Band.order('date asc')

#lets try it now with a limit of 1
> Band.order('date asc').limit(1)

#is the following the same as above?
> Band.order('date asc').first  

That was a lot of fun. Now let's mess around with Relationships.

Fire up a text editor and open up your two model classes. They'll be in the app/models folder.

Update your models to reflect the relationships we specified

class Band < ActiveRecord::Base
  has_one :artist
end

class Artist < ActiveRecord::Base
  belongs_to :band
end

Now let's go back to the console and see if we can play around with these models

$ rails c

> band = Band.first

> artist = Artist.create(name:'Sting')

>band.artist  = artist

#what happened?

We updated our model to add relationships to them, however we didn't do a database migration, so our schema and database aren't aware of any changes

Let's fix that. We'll need to create two migrations

#this will create a migration that will add a band_id column to the Artists table,
#thereby creating the necessary relationship
$ rails g migration AddBandsRefToArtist band:references

#now try
$ rails c

> band = Band.first
>artist = Artist.first
> band.artist = artist

#you should see now see an Artist record that has a 'band_id' column=1
#we can continue to access and work with this relationship via the '.' operator

###has_many :through

This relationship is super critical to really understanding many to many relationships and how they should work in rails with Active Record.

What we'll do is define two models: Student and Course, and they will relate to one another in a many to many relationship.

In a normal HABTM relationship, the only thing we have is a join table containing the foreign keys of each model. No other information is stored about the relationship. To understand the problems with this, consider the following:

What do we do in the case where we want to associate a Grade with a student for that course?

The normal HABTM cannot account for the need to model an attribute as part of a relationship. In other words a Grade is actually associated with a particular student, at a particular time: when they were taking the course. It's a product of the relationship between a student and a course. We have adequately captured the relationship between a student and a course, but we have no ability to add a Grade to this association.

This is why we need the has many through relationship. Using has many through we will be able to add aditional fields that are specific to the relationship itself, that don't exist independently of the relationship. We'll see how this works by adding a grade to the students/courses association.

Here we can set up our two initial models: Student and Course

#create a Student model
$ rails g model Student name age:integer bday:timestamp

#create a Course model
$ rails g model Course name start:timestamp end:timestamp

Following the creation of our two models, we'll also need an associative table, but unlike the standard HABTM relationship, we'll work with this association through an actual model, one that we can also define attributes on.

#create the many-to-many association table, and also add a column for grade
$ rails g model Grade course:references student:references grade

#make sure we keep the db updated
$ rake db:migrate

In order to complete the relationship, we'll need to open up our other two model files and modify them so that they contain the declarations necessary for the relationship.

Course.rb

class Course < ActiveRecord::Base

    has_many :grades
    has_many :students, through: :grades
end

Student.rb

class Student < ActiveRecord::Base

    has_many :grades
    has_many :courses, through: :grades
end

Grade.rb

class Grade < ActiveRecord::Base
  belongs_to :student
  belongs_to :course
end

We can now work in the console, create a student, and add a grade onto the student

#open up the rails console
$ rails c

# create the student and a couple of courses
> amy = Student.create(name:'Amy', age:23,bday:Date.new)
> history  = Course.create(name:'History',start:Date.new,end:Date.new)
> english = Course.create(name:'English',start:Date.new,end:Date.new)

#now create grades for the student for the courses they are enrolled in 
#take note that since we are working with a student object, AR knows to use the 
#current object for the 'student' field in the associative table, so we only need to
# specify the 'course' 
> amy.grades.create(course:history,grade:'A')
> amy.grades.create(course:english,grade:'B')

#let's take a look at some grades
> Grade.all
> Grade.where('grade' => 'A')
> amy.grades.all

##Other relationships

###Polymorphic

An important case is when you have a model that belongs to multiple models. We can designate this model to be polymorphic, and AR provides some very nice ways of dealing with this case.

Let's say that we have an Assistant model that helps out in both a class and study hall session. We would like the Assistant to belong to both so that we may assign a TA to classes and study halls of our choosing, and a TA may help out in either or both.

To start out we'll first create our new Course2, StudyHall, and Assistant model

#create a new Course2 model for use with the polymorphic model
#( this is done for convenience purely,as we have used Course above  )
#we could augment Course but we want to keep things simple and look at each relationship
#in isolation for now
$ rails g model Course2 name

#create our StudyHall Model
$ rails g model StudyHall name 

#we use a special generator command here to declare the assignment
#attribute polymorphic
$ rails g model Assistant name assistant:references{polymorphic} 

Essentially, what happens with our Assistant model is we keep track of two things:

  • The owner's id (this is a standard belongs_to operation)
  • The owner's type, in our case either StudyHall or Course2

This makes sense when you think about it, embedding just an id in Assistant won't do the trick because you have to ask the question which model's id is it?. It you have just the value 1 for the id, is it the first record of StudyHall or Course2? By also embedding the type along with the id, AR knows which table to look into to grab the row with the matching id.

Now that we have our models we'll need to adjust Course2 and StudyHall to reflect the relationship we want them to have to Assistant

course2.rb

class Course2 < ActiveRecord::Base
  has_many :assistants, as: :assistant
end

study_hall.rb

class StudyHall < ActiveRecord::Base
  has_many :assistants, as: :assistant
end

Let's pull up irb and even psql in two separate windows so we can examine how this relationship looks and works in practice.

#start irb
$ rails c 

irb> course = Course2.create(name: 'history')
#output 
#=> #<Course id: 1, name: "history", created_at: "2015-06-01 17:53:00", updated_at: "2015-06-01 17:53:00">

irb> study_hall = StudyHall.create(name: 'history')
#output
#=>  #<StudyHall id: 1, name: "history", created_at: "2015-06-01 17:53:22", updated_at: "2015-06-01 17:53:22">


irb> assistant = Assistant.create(name: 'Sally')
#output
#=>  #<Assistant id: 1, assistant_id: nil, assistant_type: nil, created_at: "2015-06-01 17:53:49", updated_at:
# "2015-06-01 17:53:49", name: "Sally">


##NOTE: at this stage we have created the relevant objects, but we haven't defined any relationships

#we assign our history course an assistant
irb>  course.assistants << assistant
#output
#=> #<ActiveRecord::Associations::CollectionProxy [#<Assistant id: 1, assistant_id: 1, assistant_type: "Course", created_at: 
#"2015-06-01 17:53:49", updated_at: "2015-06-01 17:54:02", name: "Sally">]>

#we assign 
irb> study_hall.assistants << assistant
#output
#=>  #<ActiveRecord::Associations::CollectionProxy [#<Assistant id: 1, assistant_id: 1, assistant_type: "StudyHall", created_at:
# "2015-06-01 17:53:49", updated_at: "2015-06-01 17:54:25", name: "Sally">]>

Above we have created a course and it's matching study hall, and we were able to assign the same assistant to both the course and the study hall. Look above at the output of course.assistants << assistant, notice assistant_type is the name of the model to which the assistant is being assigned. In both course and study_hall, assistant_id is the same as we are assigning the same assistant to both.

We can now retrieve these relationships as well.

#a general query that will return the first assistant for the first StudyHall record in our db
#we then access the name attribute of the Assistant
irb> StudyHall.first.assistants.first.name
#output
#=> "Sally"


irb> course.assistants
#output
#=> #<ActiveRecord::Associations::CollectionProxy [#<Assistant id: 1, assistant_id: 1, assistant_type: "StudyHall", created_at: 
#"2015-06-01 17:53:49", updated_at: "2015-06-01 17:54:25", name: "Sally">]>

##Self Referential Associations

Let's say you were creating a social networking app, you've identified a niche isn't served by current sn's, and have decided to build out your user model. We'll look at one of the simplest ways you could set up a friendship relationship using a single table.

What we'll do is create a User model, and we'll then use relationships to create friend links withing the same table

#create the model and migration for our User class
$ rails g model User name friend_id:integer

We need to add a foreign key to our Users table, the only thing special about this particular foreign key is that it is an id of another User record from the same table. This is the bit that creates the self referential association. The next thing we'll need to do is setup our User model to reflect this.

class User < ActiveRecord::Base
  has_many :friends, class_name: 'User', foreign_key: "friend_id"
  belongs_to :user

end

As can be seen, we need to specify a class_name to go along with :friends. If we don't do tell rails that :friends is of the same type as User, then it will look for a model class named Friend, and you'll get an uninitialized constant error. We also will declare that :friends are associated to User by way a foreign_key named "friend_id".

Let's see how this works in a simple case, using irb.

 
irb> user = User.create(name: 'Amy')

irb> user.friends << User.create(name: 'Sue')

irb> user.friends << User.create(name: 'Rick')

irb> user.friends
#output
#=> #<ActiveRecord::Associations::CollectionProxy [#<User id: 9, name: "Sue", friend_id: 8, created_at: 
#"2015-06-01 21:30:00", updated_at: "2015-06-01 21:30:00">, #<User id: 10, name: "Rick", friend_id: 8, created_at:
# "2015-06-01 21:30:16", updated_at: "2015-06-01 21:30:16">]>
 

We can see from above that we were able to tack on friends to our Users, but what we have done by doing it the way we have is create a lonely world, a User can only have one friend in essence, so it better be a good one. This is a consequence of using the belongs_to association. In each case we have embedded the id of the user's friend in the same row as the User object. Since there is only one column for friend_id, we can only add one friend for each user.

To actually do this for real, what we actually need to do is create a many-to-many self referential join table on the user's table. It sounds like a mouthful but it's not really that bad. Once we get used to the class_name business of doing the self referential stuff, this is really just a stock has many through we'll be creating.

So we don't get burned by any models we have setup, let's do a rails new again to just play with this one relationship

$ rails new friend-practice -T -d postgresql --quiet

$ cd friend-practice && bundle

$ rake db:create

Ok, we did that so we could create a fresh user class model that will server as our base model. Let's create the two models now that we'll need to pull this off.

$ rails g model user name

#This is the Join model that represents our Join table, which will be just 
#user id's matched with other user id's.  
$ rails g model Friendship user:references friend_id:integer && rake db:migrate

Above we created a pretty standard model Frienship that we would use for a has_many through, which is how we will use it. Notice again the difference here is we are specifying :friend, to be of type 'User'.

friendship.rb

class Friendship < ActiveRecord::Base
  belongs_to :user
  belongs_to :friend, class_name: "User"
end

Now in our user model we must set up the other side of the things. This looks just like a standard has many through.

user.rb

class User < ActiveRecord::Base
  has_many :friendships
  has_many :friends,through: :friendships
end

Now let's see this in action.

in irb

irb> user  = User.create(name: 'Sam')
#output
#=> #<User id: 1, name: "Sam", created_at: "2015-06-01 21:53:22", updated_at: "2015-06-01 21:53:22">

irb> user.friends << User.create(name: 'Kelly')
#output
#=> #<ActiveRecord::Associations::CollectionProxy [#<User id: 2, name: "Kelly", created_at: "2015-06-01 21:53:56", 
#updated_at: "2015-06-01 21:53:56">]>


irb> ron = User.create(name: 'Ron') 
#output
# => #<User id: 3, name: "Ron", created_at: "2015-06-01 21:54:36", updated_at: "2015-06-01 21:54:36">


irb> user.friends << ron
#output
#=> #<ActiveRecord::Associations::CollectionProxy [#<User id: 2, name: "Kelly", created_at: "2015-06-01 21:53:56", 
#updated_at: "2015-06-01 21:53:56">, #<User id: 3, name: "Ron",created_at: "2015-06-01 21:54:36", 
#updated_at: "2015-06-01 21:54:36">]>

irb>  kelly = User.find_by(name: 'Kelly')
#output
#=> #<User id: 2, name: "Kelly", created_at: "2015-06-01 21:53:56", updated_at: "2015-06-01 21:53:56">


irb> kelly.friends << ron
#output
#=> #<ActiveRecord::Associations::CollectionProxy [#<User id: 3, name: "Ron", created_at: "2015-06-01 21:54:36",
# updated_at: "2015-06-01 21:54:36">]>

If we were now to look in our database at our friendships table, we would see that the user_id, and friend_id columns correspond exactly to what we just did above.

  • 'Sam' has two friends: 'Kelly' and 'Ron'
  • 'Kelly' has one friend: 'Ron'
#here we can see the results of all of this in our join table

practice_development=# select * from friendships;
 id | user_id | friend_id |         created_at         |         updated_at         
----+---------+-----------+----------------------------+----------------------------
  1 |       1 |         2 | 2015-06-01 21:53:56.068828 | 2015-06-01 21:53:56.068828
  2 |       1 |         3 | 2015-06-01 21:54:45.093909 | 2015-06-01 21:54:45.093909
  3 |       2 |         3 | 2015-06-01 21:55:28.974605 | 2015-06-01 21:55:28.974605
(3 rows)

This is a very flexible and powerful way to model relationships between users in an application. It is not limited to just the case of users, but it show up in a lot of places, like with customers and businesses.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment