Andrew Luchuk
Posted on May 30, 2020
Welcome back! In order to build the basic backend of our forum app, we need to generate the basic models and set up their associations. We will be using polymorphic associations to increase the flexibility of some of our associations, making our code much more reusable.
TL;DR: Generate the base models we will use in our database: User
, Topic
, Reply
, Like
. The Reply and Like models have polymorphic associations to allow them to be added to either a topic or a reply. The whole code is available here.
Models
The base unit of any forum is the topic. Each topic centers around one particular question. Other users can write answers to the question and comment on other users’ replies.
Right away, it is clear that we will have at least three basic models: User
, Topic
, and Reply
. Users can have many topics and each topic can have many replies. For this example, each reply to a topic can have many replies itself.
Let’s add a little bit more complexity: likes. Users can like topics or comments that they find particularly useful or interesting.
As it currently stands, we now have four models, users, topics, replies, and likes. User
is a base object which has many topics and many replies, which we are going to call comments for the sake of clarity. Topics can have many replies and many likes. They must also have a title and contents. Replies have content, can have many likes, and belong to a user and to a topic or reply.
Generating the New Rails App
Prerequisites
Before generating a new rails app, we need to make sure that PostgreSQL is installed. On Mac or Linux you can install it with Homebrew like so:
brew install postgresql
On Mac, you can start postgres with the following command:
brew services start postgresql
Unfortunately, Homebrew does not support homebrew services on linux, so you will not be able to start PostgreSQL on linux with the above command. Instead, you will have to create a systemd service. See this link for my implementation of a PostgreSQL systemd service.
Building the Backend
Navigate to the directory where the new app will live and run the following command:
rails new miniforum --api -d postgresql --skip-bundle
This will create a new slimmed down rails application using PostgreSQL as its database engine. Skip bundle install
for the moment because we will update the Gemfile immediately and will need to run bundle install anyway.
Once the command has completed, navigate into the new app directory, open the Gemfile and add the following lines:
# Uncomment the line with BCrypt
gem 'bcrypt'
gem 'jwt'
gem 'dotenv-rails'
gem 'graphql'
Now that we’ve specified our additional dependencies, go ahead and run bundle install
.
Once bundler has finished installing the dependencies, we will need to install the GraphQL gem into our application. Run the following command to do that:
rails g graphql:install
We’re not going to use GraphQL just yet, but installing it now will mean that we'll be ready to develop with it later.
We’ll start by generating the User
model:
rails g model User email username name password_digest
We’re leaving all of these attributes as strings by default as BCrypt requires password digests to be stored as strings. BCrypt requires the password field in the database to be called password_digest
in order to use the has_secure_password
macro.
Next, we’ll generate the Topic
model:
rails g model Topic title content:text user:belongs_to
In the topic, we want the content to be a text type as the text type is designed to hold longer data than a plain string.
Next, generate the Reply
model:
rails g model Reply content:text user:references post:references{polymorphic}
Since a reply can belong either to a topic or to another reply, we want to make sure that we set up a polymorphic relationship. Essentially, polymorphic associations allow the same association to refer to more than one different kind of model. In our case, since there are two models that reply can belong to (Topic
or Reply
), we need a polymorphic association to represent this behavior. I highly recommend this article for more details about polymorphic associations.
When building a polymorphic association, the name of the field should be an abstraction that represents all of the different kinds of models that will be associated. In our case, we’re calling the field that represents a topic/reply association “post” since both topics and replies are kinds of posts.
Finally, generate the Like
model with the following command:
rails g model Like user:references post:references{polymorphic}
Again, like the Reply
model, likes can belong to either a topic or a reply, so we’re setting up the same kind of polymorphic association that we set up above.
Now that we’ve generated all our models, we need to create our database and get migrate our models:
rake db:create
rake db:migrate
I use rake db:migrate
instead of rails db:migrate
simply because using rake
cuts out a little bit of the code that rails has to run to figure out what task to complete. Rake gets straight to the task, and as a result should run a little bit faster.
Adding associations and validations
In order to get our associations set up properly, we still have some work to do.
User Model
Add the following code to the User
model:
# app/models/user.rb
class User < ApplicationRecord
has_secure_password
has_many :topics
has_many :comments, class_name: "Reply"
has_many :likes
validates :email, :name, :username, :password, presence: true
validates :username, :email, uniqueness: true
validates :username, format: {with: /\w/, message: "only alphanumeric characters allowed"}
validates :email, format: {with: /[\w\.\+]+@\w+(?:\.\w+)+/, message: "must be a valid email address"}
end
Note in line two, the has_many :comments
this is a reference to the comments that the user has made on other posts. The class_name: “Reply”
argument tells Active Record that the class to find the comments attribute is actually the Reply
class
The validations ensure that each user has an email, name, username, and password. Usernames and emails must be unique. The third validation line uses a regular expression to validate that the username is alphanumeric. Finally, the last line uses a regular expression to validate that the email address roughly follows the format of hello@world.com
.
If you need help with regular expressions, check out this regex tester and this excellent article on JavaScript regular expressions (which mostly carry over to Ruby regular expressions).
Topic Model
Next add the following code to the Topic
model:
# app/models/topic.rb
class Topic < ApplicationRecord
belongs_to :user
has_many :replies, as: :post
has_many :likes, as: :post
validates :title, :content, :user, presence: true
end
Notice the has_many :replies, as: :post
and has_many :likes, as: :post
. These lines set up the associations between topics and and replies. Active Record knows to look in the replies table for the ID of the topic to find all the associated objects, but since the association is polymorphic, Active Record will not be able to find the correct :topic_id
field in the table.
The as: :post
argument tells Active Record to look in the replies table, but to look for the field :post_id
instead of a :topic_id
field. Setting up the model in this way means that we’ll be able to access a topic’s list of replies and likes in the normal way.
Reply Model
For the Reply
model, we’ll need to do something similar:
# app/models/reply.rb
class Reply < ApplicationRecord
belongs_to :user
belongs_to :post, polymorphic: true
has_many :replies, as: :post
validates :content, :user, :post, presence: true
end
Note that here, in addition to the polymorphic “replies” field, we also have the line belongs_to :post, polymorphic: true
line. This tells Active Record that the post field in the database should be polymorphic. Without the polymorphic: true
argument, Active Record would try to find a table named “posts” and would raise an error because we didn’t define such a table.
Like Model
Finally, for the Like
model, all we need to add is this:
# app/models/like.rb
class Like < ApplicationRecord
belongs_to :user
belongs_to :post, polymorphic: true
validates :user, :post, presence: true
end
Again, we have the polymorphic: true
argument, but otherwise this is the simplest model in our database.
Everything is good to go for this post. I hope you that it was instructive for getting the basics of the backend setup. Polymorphic associations can be difficult to understand at first, but they add a whole lot more power to database associations.
The whole code for this part of the series can be found here.
Posted on May 30, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.