The Relationship model - A problem with the data model (and a solution)

$ rails generate model Relationship follower_id:integer followed_id:integer

The 'problem' comes from an attempt to create two tables 'followed_users' & 'followers', and then model the association as
User has_many :followed_users & User has_many :followers
That results in redundant: each row of 'followed_user' contains not only followed user's id, but all their information as well.

The problem here is that we are missing an underlying abstraction. One way to find the proper abstraction is to consider how we might implement the act of following in a web application. Recall from Section 7.1.2 that the REST architecture involves resources that are created and destroyed. This leads us to ask two questions: When a user follows another user, what is being created? When a user unfollows another user, what is being destroyed?

The answer is 'relationship', rather than an 'user'

Rails allows us to say a user if following many users through the relationship table, using code like:
has_many :followed_users, through: :relationships, source: "followed_id"
The Relationship model: Adding indices for the relationships table

Run commands
$ bundle exec rake db:migrate
$ bundle exec rake test:prepare
User/relationship associations: A failing test that verifies we could create new relationships using the user association: 'user.relationships.build(followed_id: ...)'
User/relationship associations: Another failing test that User model should respond to 'user.relationships' attribute
User/relationship associations: Implementing the user/relationships has_many association

Technicals:
Remember we write:
  class User < ActiveRecord::Base
    has_many :microposts
    ...
  end

You might expect code for user/relationships association is similar.
But there's one critical difference.
Rails infers the association automatically:
By default, Rails expects a foreign key of the form <class>_id, where <class> is the lower-case version of the class name. And when we model Micropost, we did add 'user_id' attribute. So we say User 'has_many :microposts', it's ok because Rails infers that microposts table should have a foreign key 'user_id'.
In the present case, although we are still dealing with users, they are now identified with the foreign key 'follower_id'. So we have to tell that to Rails:
User 'has_many :relationships, foreign_key: "follower_id", dependent: :destroy'

>>> Note <<<
You may wonder why there's only one direction relationship in the code:

has_many :relationships, foreign_key: "follower_id", dependent: :destroy

The relationship should be bi-directional, isn't it?
But in the one line of code we added, it only says: A user could find his relationships as a 'follower'. What about his relationships as a user being 'followed' by others?
You're right if you're asking such question.
We only implement one direction here. And there will another direction later. It's called 'reverse_relationship'.
What you need to know is we cannot model bi-directional relationship in one line of code:

*WRONG*
has_many :relationships, foreign_key: "follower_id", another_foreign_key: "followed_id", dependent: :destroy

Because then Rails don't know how to do database SELECT when encounter code 'user.relationships':
Shall I 'SELECT ... FROM relationship AS r, user AS u WHERE r.follower_id = u.user_id'
Or shall I 'SELECT ... WHERE r.followed_id = u.user_id'?
>>> Note End <<<
User/relationship associations: Add more faling tests that Relationship model has attributes 'followed' & 'follower'

Note we also verify that relationship.followed & relationship.follower are both object of User class, and
they equal to those user objects we used to create such relationship object
User/relationship associations: Get the tests green by adding 'belongs_to' to the Relationship model

Techniques:
When we write 'belongs_to', Rails infers there's foreign key.
Rails infers the names of the foreign keys from the corresponding symbols (i.e., follower_id from :follower, and followed_id from :followed)
But since there is neither a Followed nor a Follower model we need to supply the class name User.

This results in:

class Relationship < ActiveRecord::Base
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"
end

Compared with:

class Micropost < ActiveRecord::Base
  belongs_to :user
end

>>> Note <<<
When you write 'belongs_to', it has two meanings:
1. A Relationship object will have attributes 'follower' & 'followed', they're objects of User class
2. If you later write 'relationship.follower', Rails will lookup the user object in the database, via
   the foreign key you specified in 'belongs_to'. That is:
   SELECT ... FROM users, relationships WHERE users.id = relationships.follower_id
>>> Note end <<<
Validations: Add failing test for validity of Relationship when follower_id/followed_id is nil
Validations: Get the tests green by adding the Relationship model validations
Followed users: A test for the user.followed_users attribute
Followed users: Get the test pass by adding the User model 'followed_users' association

Techniques:
We use 'uses has_many through' for the first time.
A user has many following users (i.e. users followed by him) through relationships.
By default, in a 'has_many through' association Rails looks for a foreign key corresponding to the singular version of the association.
In other words, code like:

has_many :followeds, through: :relationships

would assemble an array using the 'followed_id' in the relationships table.
But, 'user.followeds' is rather awkward; far more natural is to use “followed users” as a plural of “followed”, and write instead 'user.followed_users' for the array of followed users.
Naturally, Rails allows us to override the default, in this case using the ':source' parameter (see below), which explicitly tells Rails that the source of the followed_users array is the set of followed ids.

has_many :followed_users, through: :relationships, source: :followed
Followed users: Add failing tests for some utility methods: User#follow! and User#following?
Followed users: Get the test green by adding 'follow!' and 'following?' methods

Note we omit 'self' in the method

def following?(other_user)
  relationships.find_by(followed_id: other_user.id)
end

It's equivalent to:
  self.relationships.find_by(followed_id: other_user.id)
Followed users: Add failing test for User#unfollow! other users
Followed users: Unfollowing a user by destroying a user relationship
Followers: Add failing tests User model should have attributes 'reverse_relationships' & 'followers'
Followers: Get the test green by implementing 'user.followers' using 'reverse_relationships'

Techniques:
1. We will not be making a real database table 'reverse_relationships'.
Instead, we simulate a 'reverse_relationships' table by passing 'followed_id' as the foreign key.
In other words, where the relationships association uses the follower_id foreign key:

  has_many :relationships, foreign_key: "follower_id"

the reverse_relationships association uses followed_id:

  has_many :reverse_relationships, foreign_key: "followed_id"

2. We write 'class_name: "Relationship"'
  has_many :reverse_relationships, foreign_key: "followed_id"
           class_name: "Relationship"
because otherwise Rails would look for a ReverseRelationship class, which doesn’t exist.

3. We also added 'dependent: :destroy', which means if a user is deleted, the relationships in which he is a followed user should also be deleted
Sample following data: Adding following/follower relationships to the sample data

Run commands:
rake db:reset
rake db:populate <-- a task defined by us
rake test:prepare
A web interface for following users: Create routes for two separate pages, which list user's followed users & followers respectively. We will create two links like "50 following" & "77 followers" on user's profile page & home page, to these two pages.

Techniques:
We write in route.rb:
  resources :users do
    member do
      get :following, :followers
    end
  end

Which generates routes like:
        Prefix Verb   URI Pattern                    Controller#Action
following_user GET    /users/:id/following(.:format) users#following
followers_user GET    /users/:id/followers(.:format) users#followers

So we can GET url /users/1/following or /users/1/follower.
Note the named routes are 'following_user_path(user)' or 'following_user_url(user)'

What if we want 'member' on collection, works without the id? That will be:
  resources :users do
    collection do
      get :tigers
    end
  end
The URL will then be:
  /users/tigers

See http://guides.rubyonrails.org/routing.html for more.
Stats and a follow form: Add failing tests for the following/follower statistics (which are links as well) on the Home page