Basic Models and Polymorphic Associations
Tags: webdev, ruby, rails
Published: 2020-05-30 14:53:50

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][code].

# 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][brew] like so:
```bash
brew install postgresql
```

On Mac, you can start postgres with the following command:
```bash
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][systemd] 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:
```bash
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:

```ruby
# 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:
```bash
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:
```bash
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:
```bash
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:
```bash
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][polymorphism] 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:
```bash
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:
```bash
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:

```ruby
# 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][regtest] and [this excellent article][regart] on JavaScript regular expressions (which mostly carry over to Ruby regular expressions).

## Topic Model

Next add the following code to the `Topic` model:
```ruby
# 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:

```ruby
# 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:

```ruby
# 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][code].

[systemd]: https://gist.github.com/speratus/b2b65f647629ec088e4ae244a42edcf1#file-postgres-service
[code]: https://github.com/speratus/miniforum/tree/part1-basic-models
[brew]: https://brew.sh/
[regart]: https://dev.to/chrisachard/intro-to-regex-for-web-developers-2fj4
[regtest]: https://regex101.com/
[polymorphism]: https://launchschool.com/blog/understanding-polymorphic-associations-in-rails