1. we will first generate a micropost model.
$ rails generate model Micropost content:string user_id:integer
note, if you the content is longer, you can use type "text" instead of string
2. next, since we expect to retrieve all microposts associated with a user, we need to add index on user_id and created_at columns.
add_index :microposts, [:user_id, :created_at]
also, note this line
t.timestamps
4. next, we will specify attr_accessible attrs.
we don't want user to edit the user_id attr through web, so we only add :content to attr_accessible
attr_accessible :content
5. but this will create a difficulty for us, how to assign the user id when create a micropost?
go on with this question.
we will create user/micropost associations.
we start from test, TDD, yes.
describe Micropost do
before(:each) do
@user = Factory(:user)
@attr = { :content => "value for content" }
end
it "should create a new instance given valid attributes" do
@user.microposts.create!(@attr)
end
describe "user associations" do
before(:each) do
@micropost = @user.microposts.create!(@attr)
end
it "should have a user attribute" do
@micropost.should respond_to(:user)
end
it "should have the right associated user" do
@micropost.user_id.should == @user.id
@micropost.user.should == @user
end
end
end
describe User do
.
.
.
describe "micropost associations" do
before(:each) do
@user = User.create(@attr)
end
it "should have a microposts attribute" do
@user.should respond_to(:microposts)
end
end
end
6. all this test can pass after we add belongs_to and has_many methods to user.rb and micropost.rb.
after adding the two methods, we will have these methods for use:
micropost.user Return the User object associated with the micropost.
user.microposts Return an array of the user’s microposts.
user.microposts.create(arg) Create a micropost (user_id = user.id).
user.microposts.create!(arg) Create a micropost (exception on failure).
user.microposts.build(arg) Return a new Micropost object (user_id = user.id).
note the last method, it will return a new object that is not saved yet, but the user_id is already assigned.
7. to test the microposts that belong to a user, we need to factory some sample micropost records:
Factory.define :micropost do |micropost|
micropost.content = "foo bar"
micropost.association = :user
end
then in the test code, we can
@mp1 = Factory(:micropost, :user => @user, :created_at => 1.day.ago)
@mp2 = Factory(:micropost, :user => @user, :created_at => 1.hour.ago)
you can see, factory not only allow us to mass assign to bypass attr_accessible,
it also allow us to assign created_at and updated_at.
for normal ActiveRecord, it won't alow us to do so, rails magic will assign the timestamp automatically.
here is the test code in user_spec.rb:
describe "micropost associations" do
before(:each) do
@user = User.create(@attr)
@mp1 = Factory(:micropost, :user => @user, :created_at => 1.day.ago)
@mp2 = Factory(:micropost, :user => @user, :created_at => 1.hour.ago)
end
it "should have a microposts attribute" do
@user.should respond_to :microposts
end
it "should have the right microposts in the right order" do
@user.microposts.should == [@mp2, @mp1]
end
end
to make it pass, we need to order the microposts:
default_scope :order => 'microposts.created_at DESC'
this is the first time we encounter scope, we will get familiar later.
note, we use
microposts.created_at
instead of
micropost.created_at
8. next, we will add test code to test that after destroying a user, the related micropost will be destroed automatically too.
it "should destroy associated microposts" do
@user.destroy
[@mp1, @mp2].each do |micropost|
Micropost.find_by_id(micropost.id).should be_nil
end
end
notes:
Micropost.find_by_id(micropost.id)
will return nil if not found.
But
Micropost.find(micropost.id)
will raise an exception
lambda do
Micropost.find(micropost.id)
end.should raise_error(ActiveRecord::RecordNotFound)
9. the code to make the dependent destroy is very simple:
has_many :microposts, :dependent => :destroy
10. micropost validations:
again, start from test:
describe "validations" do
it "should require a user id" do
Micropost.new(@attr.should_not be_valid)
end
it "should require nonblank content" do
@user.Microposts.build(:content => " ").should_not be_valid
end
it "reject long content" do
@user.Microposts.build(:content => "a" * 141).should_not be_valid
end
end
note, when contruct a new micropost object using associated user, we use
build()
instead of
Micropost.new