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, andemailall to be present:validates :fname, :lname, :email, presence: trueSecond, 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:referencesNotice how Rails provides code to handle common tasks. In this case, the
generateprogram can parse the migration name (AddAuthorToPost) as well as the parameterauthor:referencesto 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 :postsand the following line to
models/post.rbbelongs_to :author, optional: trueThe
optional: trueis 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: trueparameter. 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_idfield 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.firstNotice how the
author_idon the post isnil! The column was added to thePosttable 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.allor 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 = abut 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.firstnotice 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 _formpartial:<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.eachloop place this item as the first element of the row:<td><%= (post.author.try(:lname) || "NA") %></td>finally, in the
views/posts/show.html.erbbe 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_idfield 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) endand now we wrap things up by adding additional logic in the
createmethod 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)