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

Author profile picture

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 instanceCyrus-Kiprop. 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.

Create a new user:

➜ new_user = User.create(first_name: "John", last_name: "Doe", email: "[email protected]", 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

The Noonification banner

Subscribe to get your daily round-up of top tech stories!

read original article here