12. following user, 12.1 relationship model

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:
user.follow!(other_user)   ===> it will throw an exception if failure.
user.following?(other_user)
but before that, we will write test to see what the two method do:
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
note:
  @user.following.should include(@followed)
is equivalent with:
@user.following.include?(@followed).should be_true
 but more cleare.

next, we can implement the two methods:
def following?(followed)
    relationships.find_by_followed_id(followed)
  end

  def follow!(followed)
    relationships.create!(:followed_id => followed.id)
  end
  of course, we also need unfollow! method.
the test is:
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
 the code of unfollow! is straight forward.
def unfollow!(followed)
    relationships.find_by_followed_id(followed).destroy
  end
 
12. followers:
the part is tricky, let's write some test first:
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
 we won't use a new table to hold the followers, we will use the same table, but with different relationships:

has_many :reverse_relationships, :foreign_key => "followed_id", :class_name => "Relationship", :dependent => :destroy
 note, the foreign key is different, and we need to specify the class name, or rails will try to find 
ReverseRelationship class.

then we can have:
has_many :followers, :through => :reverse_relationships, :source => :follower
 
ok,  we finally done with data model of relationship and following and followers.

你可能感兴趣的:(Model)