Basic Models and Polymorphic Associations

speratus

Andrew Luchuk

Posted on May 30, 2020

Basic Models and Polymorphic Associations

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
Enter fullscreen mode Exit fullscreen mode

On Mac, you can start postgres with the following command:

brew services start postgresql
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
speratus
Andrew Luchuk

Posted on May 30, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related