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