1. we need to use a relationships table to stand for the following relationship:
has_many :following, :through => :relationships, :source => "followed_id"
2. then
$ rails generate model Relationship follower_id:integer followed_id:integer
3. we need to add index to the table
add_index :relationships, :follower_id
add_index :relationships, :followed_id
add_index :relationships, [:follower_id, :followed_id], :unique => true
note the last index is a composite one, and is unique.
4. then rake db:migrate
rake db:test:prepare
5. we will follow a good practice here:
remember, it is good to define attr_accessible for every model to avoid mass assignment to some attrs that you don't want user to touch.
so here, we know we only want user to modify the followed_id, never the follower_id.
so
attr_accessible :followed_id
6. how to create a relationship:
we should use the user association to create relationship:
user.relationships.create!(:followed_id => "")
7. below is the test spec of relationship model:
describe Relationship do
before(:each) do
@follower = Factory(:user)
@followed = Factory(:user, :email => Factory.next(:email))
@relationship = @follower.relationships.build(:followed_id => @followed.id)
end
it "should create a new instance given valid attributes" do
@relationship.save!
end
end
note, @relationship.save! will throw an exception if fail.
we user @follower.relationships.build() to create a relationship.
in the user model, we also need to test it respond to the relationships method.
describe "relationships" do
before(:each) do
@user = User.create!(@attr)
@followed = Factory(:user)
end
it "should have a relationships method" do
@user.should respond_to(:relationships)
end
end
8. when we define
class User
has_many :microposts
end
class Micropost
belongs_to :user
end
because microposts table has a user_id to identify the user, so this is a foreign key, which is connecting two tables, and when the foreign key for a user object is user_id, rails can infer the association auto, by default, rails expects a foreign key of user_id, where user is the lower case of class User.
but now, although we are dealing with users, they are now identified with the foreign key follower_id, so we have to tell rails, that follower_id is a foreign key.
class User
has_many :relationships, :foreign_key => "follower_id", :dependent = :destroy
end
9. next, the relationship should belong to users, one relationship belong to 2 users.
a follower and a followed user.
let write test:
describe Relationship do
.
.
.
describe "follow methods" do
before(:each) do
@relationship.save
end
it "should have a follower attribute" do
@relationship.should respond_to(:follower)
end
it "should have the right follower" do
@relationship.follower.should == @follower
end
it "should have a followed attribute" do
@relationship.should respond_to(:followed)
end
it "should have the right followed user" do
@relationship.followed.should == @followed
end
end
end
next, we will make this test pass:
belongs_to :follower, :class_name => "User"
belongs_to :followed, :class_name => "User"
10. next we will add some validations to the relationship model:
the test is:
describe Relationship do
.
.
.
describe "validations" do
it "should require a follower_id" do
@relationship.follower_id = nil
@relationship.should_not be_valid
end
it "should require a followed_id" do
@relationship.followed_id = nil
@relationship.should_not be_valid
end
end
end
next, let's add the validations:
validates :follower_id, :presence => true
validates :followed_id, :presence => true
11. following:
the user object should respond to following method:
describe User do
.
.
.
describe "relationships" do
before(:each) do
@user = User.create!(@attr)
@followed = Factory(:user)
end
it "should have a relationships method" do
@user.should respond_to(:relationships)
end
it "should have a following method" do
@user.should respond_to(:following)
end
end
end
to make it pass, we need this line of code:
has_many :followeds, :through => :relationships
rails then can deduce it should use "followed_id" assemble an array.
but as you see, followeds is awkward english, so we want to overwrite the default name:
has_many :following, :through => :relationships, :source => :followed
then we will add some utility methods:
describe User do
.
.
.
describe "relationships" do
.
.
.
it "should have a following? method" do
@user.should respond_to(:following?)
end
it "should have a follow! method" do
@user.should respond_to(:follow!)
end
it "should follow another user" do
@user.follow!(@followed)
@user.should be_following(@followed)
end
it "should include the followed user in the following array" do
@user.follow!(@followed)
@user.following.should include(@followed)
end
end
end
@user.following.include?(@followed).should be_true
def following?(followed)
relationships.find_by_followed_id(followed)
end
def follow!(followed)
relationships.create!(:followed_id => followed.id)
end
it "should have an unfollow! method" do
@user.should respond_to :unfollow!
end
it "should unfollow a user" do
@user.follow!(@followed)
@user.unfollow!(@followed)
@user.should_not be_following(@followed)
end
def unfollow!(followed)
relationships.find_by_followed_id(followed).destroy
end
it "should have a reverse_relationships method" do
@user.should respond_to(:reverse_relationships)
end
it "should have a followers method" do
@user.should respond_to(:followers)
end
it "should include the follower in the followers array" do
@user.follow!(@followed)
@followed.followers.should include(@user)
end
has_many :reverse_relationships, :foreign_key => "followed_id", :class_name => "Relationship", :dependent => :destroy
has_many :followers, :through => :reverse_relationships, :source => :follower