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