Rail Demo 3: Associating Authors and Posts
-
Let’s now assume that we want to have authors associated with the Posts. We can start by creating an Author entity w/scaffolding:
rails g scaffold Author fname:string lname:string email:string thumbnail:string
-
Now that we know how to validate our models, let’s go ahead and add some validations to our author model in
models/author.rb
. Please note that we should probably first write out our model requirements, than write the tests, then write the actual validates methods in our model. However, for demonstration purposes we’ll go right to the model code:First, let’s require
fname
,lname
, andemail
all to be present:validates :fname, :lname, :email, presence: true
Second, make sure a unique and properly formatted email address is provided:
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i validates :email, presence: true, length: {maximum: 105}, uniqueness: { case_sensitive: false }, format: { with: VALID_EMAIL_REGEX }
Third, validate the thumbnail making sure the filename ends in
gif
,jpg
, orpng
.validates :thumbnail, allow_blank: true, format: {with: %r{\.(gif|jpg|png)\Z}i, message: 'must be a URL for GIF, JPG or PNG image.'}
-
Amazing stuff, eh? Now let’s go ahead and run the generated authors migration, run our server and examine our handiwork:
rake db:migrate rails server
-
Now its time to formally associate authors with posts. First we need to create a new migration to update our database schema:
rails g migration AddAuthorToPost author:references
Notice how Rails provides code to handle common tasks. In this case, the
generate
program can parse the migration name (AddAuthorToPost
) as well as the parameterauthor:references
to automatically generate the migration class (look indb/migrate
). The idea is for you to have to spell out only the minimal amount of information necessary to accomplish the task. The parts that are common to the task (the timestamp, setting up the class, etc.) are automated. -
Now we need to inform our models of this relationship. We do this by adding the following line to
models/author.rb
:has_many :posts
and the following line to
models/post.rb
belongs_to :author, optional: true
The
optional: true
is temporary. In the long-term every post must have an author. However, if we try to run the migration without making the author reference optional, then the database will complain because it won’t know which author to link with existing posts. Once all posts have authors assigned, remove theoptional: true
parameter. See this StackOverflow post: https://stackoverflow.com/questions/3170634/how-to-solve-cannot-add-a-not-null-column-with-default-value-null-in-sqlite3 -
Now go ahead and run the migration, and then examine the new
author_id
field in the posts table by examining thedb/schema.rb
.rake db:migrate
-
Now let’s explore what we’ve accomplished in our rails console and note the magic that these minor tweaks has brought about!
rails console p = Post.first
Notice how the
author_id
on the post isnil
! The column was added to thePost
table when we ran the migration above; but, the data has not yet been populated. Let’s manually do that! Let’s take a look at the authors that are available to us:authors = Author.all
or better yet, let’s just pick out the one with id = 2
a = Author.find(2)
now we can do an assignment like this:
p.author = a
but in order to save it we still have to commit to the database by calling the save method!
p.save!
now if we do a query for the first post you will see that the author_id has been updated to 2!
p = Post.first
notice that we can simply reference the author attribute to access the name of the author:
puts "authored by #{p.author.fname} #{p.author.lname}"
Similarly, we can get the array of all posts by a given author by simply asking for them:
a = Author.find(2) ap = a.posts
-
Now, we need to update the views to handle the association between author and post. First let’s go ahead and add a author selection drop down in the
Post _form
partial:<div class="field"> <%= form.label :author_id %> <%= form.select :author_id, options_for_select(Author.all.collect {|a| ["#{a.lname}, #{a.fname}", a.id]}, selected: (@post.author ? @post.author.id : Author.first.id)), {} %> </div>
-
Next, we add the author info to the Post listing view in
views/posts/index.html.erb
. We can add a column heading in the first row of the table:<th>Author</th>
and inside the
@posts.each
loop place this item as the first element of the row:<td><%= (post.author.try(:lname) || "NA") %></td>
finally, in the
views/posts/show.html.erb
be sure to add markup to show the post’s author:<p> <strong>Author:</strong> <%= (@post.author.try(:lname) || "NA")%> </p>
-
Almost done, now we need to update the Post controller so it saves the association between post and author. First we need to allow the new
author_id
field in the form variables the controller allows by modifying this private method oncontrollers/post_controller.rb
:def post_params params.require(:post).permit(:title, :article, :likes, :status, :author_id) end
and now we wrap things up by adding additional logic in the
create
method to build out the association by placing the following immediately after creating the new Post instance:author = Author.find(post_params[:author_id]) @post.build_author(:id => author.id)