Hackernoon logoRuby on Rails Hidden Secrets: How To Get The Most Out Of Active Record Associations by@cyrus-kiprop

Ruby on Rails Hidden Secrets: How To Get The Most Out Of Active Record Associations

Cyrus-Kiprop Hacker Noon profile picture

@cyrus-kipropCyrus-Kiprop

Hello folks! In this article, we are going to unravel the mystery behind the Rails Active Record class. To be honest, I struggled a lot with Rails models as a beginner. I spent a lot of time reading the docs, read a couple of medium articles, watched some youtube videos but all in vain. I have chosen to draft a nice article that constitutes of baby steps that is suitable for aspiring Rails Engineers.

I chose a Facebook database model since it is a little bit advanced as well as it encompasses the majority of the rails association concepts.

Deliverables

  1. User-post Association
  2. User Friendship Association

Terminologies:

Active Record: the layer responsible for representing business data and logic

Object Relation Mapping(ORM): this is a technique used to connect objects of an application to tables in a Relationship Database management system. With this, we can store and retrieve data without writing a single SQL statement.

Pre-requisites

  1. Basic knowledge of Ruby programming language.
  2. The latest version of Ruby. Click here for more info on how to install Ruby.
  3. Node.
  4. Rails.
  5. SQlite / Postgresql
  6. Problem-solving attitude.

Getting Started

Enough with the chit chat, let's get our hand dirty.

#1 Fire your terminal {ctr + Alt + T} on a Unix system and generate a new Rails app with the name

facebook-Db-clone

rails new facebook-db-clone && cd facebook-db-clone

2 -> Fire rails server to confirm that everything works :)

rails server

#3 -> Scaffold the User, Post, and Comments model for a start. if you are confused, here is an ERD {Entity Relationship Diagram} diagram that will help you visualize the overall structure of the models.

Let's start by generating the user model.

Here we design the user model by explicitly naming the user attributes and passing them to the
 rails g model 
command. The User in this case is the model class. The command, in turn, generates several files by invoking the active_record method. The files consist mainly of
unit tests
, migration file ( a blueprint that helps developers to define changes to their database schema, making it possible to use a version control system to keep things synchronized with the actual code ) and a
user.rb
file.

Having cleared the air, it is time we replicate the same procedure for the other two models:- the

post
and
comments
. Run the following commands in successions.

➜ rails g model Post body:text
➜ rails g model Comments content:text

After running the above commands, there should be three files with their names prefixed with a timestamp,

stimestamp_my_new_migration.rb
 in the
db/migrate/ 
directory where the timestamp is the UTC formatted date and time that the migration was generated. The naming convention adopted by rails here proves to be useful when determining the order of operation later when we ran our migrations.

Here, I have included a snapshot of the three migration files.

Each and every class inherits from the
ActiveRecord::Migration 
class. The 3 classes each holds a recipe of how Rails is creates the tables and rows of our User, Posts, and Comments in the database. Rails is actually good at abstracting the tedious work of writing SQL statements, Pretty sweet right. For example, to the left of the snapshot, Rails is creates a table named
users
and rows
 first_name, last_name, email and password 
and so forth. In some cases, migration files might contain other advanced methods i.e
up
and
down
methods, they describe the transformation required to implement or remove the migrations. click here for more info.

#4 Tattooing the Database

By default, Rails uses

SQlite
out of the box, as it primary database coupled with zero configuration. Thanks to this, one can focus on grasping the problem at hand. For production purposes, you might need to consider other options i.e
Postgresql
since SQLite is barely supported on cloud services i.e Heroku. Our next move is to execute the instruction held by the classes. To achieve this, we ran the following commands.

➜ rails db:create
➜ rails db:migrate

rails db:create creates a database based on the sqlite config file whereas rails db:migrate creates the tables and rows used to store the user data.

Associations:

spoiler Alert: Nitty-gritty staff

Association: - The connection between two or more Active Record models i.e user vs post model.

Rails support six associations:

belongs_to
,
has_one
,
has_many
,
has_many :through,
has_one :through
&&
has_and_belongs_to_many
.

Relationships are implemented using macros-style calls. this allows you to declaratively add features to your models with ease.

One-to-many Relationship.

belongs_to: User vs post model Association:

class User < ApplicationRecord
  # write your association staff here
end

class Post < ApplicationRecord
  # write your association staff here
end
  • User can have zero{
    nil}
    or multiple Posts instances.
  • A post instance can only belong to only one instance of a User and it cannot exist on it's own. By this I mean, If we destroy a User, all the posts related to that user should also be destroyed.
From the look of things, this is a
one-to-many
relationship. To implement this type of relationship, Rails provides us with
has_many
&&
belongs_to
methods. These methods instructs rails to maintain a Primary key - Foreign Key relationship between the instances of the two models.

#1 In your model's folder, open

user.rb
and
 post.rb
file. Add the following methods.

class User < ApplicationRecord
  # write your association staff here
  has_many: posts
end
class Post < ApplicationRecord
  # write your association staff here
  belongs_to: user
end

#2 Generate a new migration that adds a primary key - foreign key reference to both the User and Post model.

#3 Edit the newly created migration file

db/migrate/time_stamp_add_UserId_to_post.rb
. Add the following content.

class AddUserIdToPosts < ActiveRecord::Migration[5.2]
  def change
    add_column :posts, :user_id, :string
    add_index :posts, :user_id
  end
end
This migration is pretty straightforward. It adds a column
:user_id
to the
:posts
table and also sets the
user_id 
column as the
foreign key
pointing to the user instance{author}. Run
rails db:migrate
to persist the changes.

Rails console 
allows you to interact with your rails application from the command line. Open a new terminal session at the root of your app and run.

rails console

Run

reload!
to apply recent changes.

reload!

Create a new user:

➜ new_user = User.create(first_name: "John", last_name: "Doe", email: "example@gmail.com", password: "password")

➜ new_user.save

Create two new posts, authored by the above user.

➜ user = User.first
➜ new_post1 = user.posts.build(body:"This is my first Article, please give it a star")
➜ new_post2 = user.posts.build(body: "This is my second Article, please give it a star if you like")
➜ new_post1.save
➜ new_post2.save

Access all the posts authored by John Doe

➜ user = User.where("first_name = ?", "John")
➜ user.posts

Access the author of a Post:

p1 = Post.first
p1.user

Congratulations!!! That is it for a one-to-many relationship. Be sure to visit Rails docs for a better understanding.

Many-to-many: Association

Friendships This section is a little tricky and rough. It is upon you to stick with me on this one. Let's think for a second, how do we model this? which tables are involved? For sure, there are multiple ways to achieve this, but the main goal here is to understand the concept behind friendships.

  1. Users can send and receive friend requests from other users.
  2. Users can multiple friends.
  3. Users can accept friend requests as well as reject any sort of invitation.
  4. We need to delete a friendship/invitation record upon rejection/unfriend action from the user.
  5. Confirmed friendship is reciprocal to both parties.
The difficulty here lies in creating two join tables(the general norm in many-to-many relationships) that reference both the users and the friend(user). This is because the
users
table is going to be referencing itself. Wait, what! self referencing, yeah you read it right.

Our Plan:

  • Create a friendship table to keep track of the requests/invitations.
  • Keep track of the status of the invitation by adding an is_comfirmed column to the friendship table.

Action

#1 Scaffold the Friendship model

rails g model Friendship user:references friend:references confirmed:bolean

We all know the output of the above rails command. Yeah, what about the

references
keyword? The keyword is a fancy way of hooking up different models by adding foreign key columns to the newly created table that act as pointer.

The odd part of the command is that it is trying to reference a non-existing friends table. To fix this, open your migration files under db/migration directory and change it to resemble this.

class CreateFriendships < ActiveRecord::Migration[5.2]
  def change
    create_table :friendships do |t|
      t.references :user, foreign_key: true
      t.references :friend, index: true
      t.boolean :confirmed

      t.timestamps
    end
    add_foreing_key :friendships, :users, column: :friend_id
  end
end

using the

add_foreing_key
method above allows us to reference the user table as a friends table. Go ahead and perform the data migrations.

rails db:migrate

#2 Update the Relationships on the Friendship model

Edit the Friendship class to be similar to this.

class Friendship < ApplicationRecord
  belongs_to :user
  belongs_to :friend, :class_name => 'User'
end

Belongs_to :friend,  :class_name => 'User' 
points the
friends
table to the
users
table since a friend is also a user.

#3 Update the Relationship in the User Model

class User < ApplicationRecord
  has_many :posts
  has_many :friendships
  has_many :reciprocal_friends, :class_name => "friendships", :foreign_key => "friend_id"
end

#4 Built custom Helper methods to:

Open your User model and add the following methods;

Method 1: Get all friends of a specific user.

 def friends
    direct_friends = friendships.map { |friendship| friendship.friend if friendship.confirmed }
    inverse_friends = reciprocal_friends.map { |friendship| friendship.user if friendship.confirmed }
    (direct_friends + inverse_friends).compact
  end

Method 2: Keep track of pending invitations.

# pending invitations
  def pending_friend_requests
    friendships.map { |friendship| friendship.friend unless friendship.confirmed }.compact
  end

Method 3: Incoming friend Requests.

	# incoming friend requests
  def incoming_friend_requests
    reciprocal_friends.map { |friendship| friendship.user unless friendship.confirmed }.compact
  end

Method 4: Confirm Friend Requests

def confirm_friend_request?(user)
    friend = reciprocal_friends.find { |friendship| friendship.user == user }
    if friend
      friend.confirmed = true
      friend.save
      true
    else
      false
    end
  end

Method 5: Check whether a user is already a friend.

def is_friend?(user)
    friends.include?(user)
  end

Well, that was quite a lot to take in. The above code is pretty straightforward for a Rubyist. This is a bare minimum backend configuration that you can use as a template of your next social site. Stay tuned for the next couple of Episodes on

comments
and
likes
part of the associations.

Tags

Join Hacker Noon

Create your free account to unlock your custom reading experience.