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
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
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 thecommand. 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 ofrails g model
, 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 aunit tests
file.user.rb
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 theclass. 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 namedActiveRecord::Migration
and rowsusers
and so forth. In some cases, migration files might contain other advanced methods i.efirst_name, last_name, email and password
andup
methods, they describe the transformation required to implement or remove the migrations. click here for more info.down
#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.
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
nil}
or multiple Posts instances. From the look of things, this is arelationship. To implement this type of relationship, Rails provides us withone-to-many
&&has_many
methods. These methods instructs rails to maintain a Primary key - Foreign Key relationship between the instances of the two models.belongs_to
#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 columnto the:user_id
table and also sets the:posts
column as theuser_id
pointing to the user instance{author}. Runforeign key
to persist the changes.rails db:migrate
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
rails console
Run
reload!
to apply recent changes. reload!
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.
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.
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 thetable is going to be referencing itself. Wait, what! self referencing, yeah you read it right.users
Our Plan:
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.