DEV Community

Cover image for Following Users: Because Stalking is so 2010
Anthony Lombardi
Anthony Lombardi

Posted on • Originally published at t0nylombardi.dev

Following Users: Because Stalking is so 2010

How to Become a Professional Social Stalker with Ruby on Rails

In the world of social networking, being able to keep tabs on your friends, favorite influencers, and the occasional ex is practically an Olympic sport. So why not embrace it and learn how to implement follow/unfollow functionality in your Ruby on Rails app? Let's dive into the wonderfully creepy world of user stalking and build something truly captivating.

Modeling User Relationships: It's Complicated (Like All Good Relationships)

So you want users to follow each other, huh? Sounds simple enough, but buckle up, because it's about to get as complicated as a Facebook relationship status. We'll start by trying to model it with a has_many relationship, but surprise, surprise, it's not that easy. Turns out, we need a little more finesse to make this work without creating a data model mess.

Alright, so picture this: you're diving headfirst into the wild world of user stalking... uh, I mean, following. At first glance, you might think, "Hey, I'll just slap a has_many relationship on there and call it a day." But hold your horses, because this ain't your grandma's social network. Turns out, there's a twist in the tale, and we're about to embark on a journey through the mystical land of has_many :through. It's like discovering that the secret ingredient in grandma's famous cookies is actually unicorn tears. Intrigued? Let's dive in and uncover the magic behind building a data model that'll make even Dumbledore raise an eyebrow.

Imagine you're strolling through the digital streets of your favorite social platform. You've got Morty, your average... piece of defication, just minding his own business. Then there's Rick, the cool cat everyone wants to hang with. Now, Morty decides he wants to be part of Rick's entourage, so he hits that follow button faster than you can say "wubba lubba dub dub." Boom! Morty's now a follower, and Rick? Well, he's officially followed by Morty.

Now, let's talk about labels. You see, in the world of Rails, everything's gotta have a label. So, naturally, Morty's got himself a sweet array of followers, because who wouldn't want to follow the guy who's pals with the Rick, right? But here's where things get a bit wonky. By default, Rails wants to call the folks Morty's following the followeds. Yeah, try saying that three times fast without tripping over your own tongue. We're not about that life. So, we're taking a page out of X(formally know as Twitter)'s playbook. X might not be perfect(let's face it, it's a dumpster fire), but they got one thing right: calling them "followeds" just sounds wrong. We'll adopt their convention and stick with "following" for those you're stalking and "followers" for your loyal fans.

This discussion suggests modeling the followed users, with a following table and a has_many association. Since user.following should be a collection of users, each row of the following table would need to be a user, as identified by the followed_id, together with the follower_id to establish the association. In addition, since each row is a user, we would need to include the user’s other attributes, including the name, email, password, etc.

For simplicity, we omit the following table’s id column.

First Diagram

Alright, let's talk about being DRY. You know, that feeling you get when you realize you've got the same info stored in three different places? Yeah, not so ideal. Our data model is suffering from a severe case of redundancy. Each row in our following table not only contains the followed user's ID but also a whole bunch of other info that's already chilling in the users table. And don't even get me started on the nightmare of trying to keep everything up to date. I mean, imagine having to update every single row in both the following and followers tables just because someone changed their username. Talk about a headache.

But fear not, my friends, because where there's a problem & there's a solution. And in this case, it's all about finding the right abstraction. When one user decides to follow another, what's really happening? It's not rocket science. We're creating a relationship, plain and simple. And when that relationship ends? We're destroying it. It's the circle of life(Sing it Elton!).

Active vs. Passive Relationships: Morty Can Stalk Rick Without Rick Even Noticing

In the world of stalking...sorry, I mean following, relationships can be a bit one-sided. Morty might be obsessed with Rick, but Rick might not even know Morty exists. We'll dive into the nuances of active and passive relationships, so you can stalk... I mean, follow, with confidence.

Unlike your typical Facebook-style friendships, where it's all about being BFFs forever, Twitter-style following is a bit more... flexible. Morty can follow Rick without Rick feeling obligated to return the favor. It's like having a one-sided bromance, and it's totally cool. So, to keep things straight, we're talking active and passive relationships. Morty's the active one, hitting that follow button like it's going out of style, while Rick's just chilling on the passive side, soaking up all the love.

We're going to take those active relationships and turn them into something beautiful: an active_relationships table. No more redundant info cluttering up the place. Just clean, efficient data storage, the way it should be. And just like that, we've got ourselves a data model that's as sleek and stylish:

second diagram

Because we’ll end up using the same database table for both active and passive relationships, we’ll use the generic term relationship for the table name, with a corresponding Relationship model. The result is the Relationship data model shown in the picture below. We’ll see how to use the Relationship model to simulate both Active Relationship and Passive Relationship models.

third diagram

To get started with the implementation, we first generate a migration:

$ rails generate model Relationship follower_id:integer followed_id:integer
Enter fullscreen mode Exit fullscreen mode

This will generate:

# db/migrate/[timestamp]_create_relationships.rb

# frozen_string_literal: true

class CreateRelationships < ActiveRecord::Migration[7.1]
  def change
    create_table :relationships do |t|
      t.integer :follower_id
      t.integer :followed_id

      t.timestamps
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Because we will be finding relationships by follower_id and by followed_id, we should add indexes on each column for efficiency:

# frozen_string_literal: true

class CreateRelationships < ActiveRecord::Migration[7.1]
  def change
    create_table :relationships do |t|
      t.integer :follower_id
      t.integer :followed_id

      t.timestamps
    end
    add_index :relationships, :follower_id
    add_index :relationships, :followed_id
    add_index :relationships, [:follower_id, :followed_id], unique: true
  end
end
Enter fullscreen mode Exit fullscreen mode

Welcome to the wild and wacky world of multiple-key indexes, where we're about to make each relationship between users unique. We'll start with pairs of IDs, one for the follower and one for the followed, locked in an eternal embrace of digital love. We're saying, "Hey, you two, you're special. No one else can have a relationship quite like yours." It's like the social media version of monogamy. But here's the kicker: if some sneaky punk tries to slide in and create a duplicate relationship, we're shutting that down faster than you can say "Wubba Dubba Dub Dub." Because in the world of data integrity, there's no room for duplicates. It's like trying to wear the same underwear to two different parties—just not gonna fly (and gross).

So, with our trusty multiple-key index, we're keeping those relationships clean, unique, and error-free. Just like Granny's sense of humor—it's one of a kind. And we're not afraid to kick some database butt to make sure everything runs smoothly. So buckle up, because we're about to embark on a trip through the world of data integrity, so let's do this! 🦄

Migrate the relationships table to the database:

$ rails db:migrat
Enter fullscreen mode Exit fullscreen mode

User/Relationship Associations

Before we dive into the juicy stuff like following and followers, we gotta set the stage. It's all about establishing the association between users and relationships. Think of it like setting up a blind date, but instead of awkward small talk, we're talking database relationships.

So, here's the deal: a user is like the puppet master pulling the strings, and relationships are like the marionettes dancing to their tune. Each user has_many relationships, because let's face it, we're all social butterflies in this digital age. And since relationships involve two players, it's a two-way street. That's right, a relationship belongs_to both a follower and a followed user. It's like the ultimate love triangle, but without all the drama (hopefully). So, strap on, gents & ladies! We're about to embark on a wild ride through the tangled web we weaved of user relationships.

We will create new relationships using the user association, with code such as:

user.active_relationships.build(followed_id: ...)

At this point, you might expect application code is similar, but there are two key differences.

First, in the case of the user/post association, we could write

class User < ApplicationRecord
   has_many :posts
   .
   .
   .
end
Enter fullscreen mode Exit fullscreen mode

We're about to dive into the inner workings of Rails. So, here's the deal: when Rails sees something like has_many :posts, it's not just randomly picking words out of a hat. No, sir, no siree bob! There's some serious magic going on behind the scenes. You see, Rails has this little trick up its sleeve called the classify method. It's like the Houdini of class naming, taking something like "foo_bars" and turning it into "FooBar" faster than han a knife fight in a phone booth. So when you see has_many :posts, just know that Rails is working its classify magic behind the scenes, making sure everything's running smoother than a baby's backside.

has_many :active_relationships

Even though the underlying model is called Relationship. We will thus have to tell Rails the model class name to look for.

Second, we will write this in the Post model:

class Post < ApplicationRecord
   belongs_to :user
   .
   .
   .
end
Enter fullscreen mode Exit fullscreen mode

Alright, let's take a stroll through the labyrinth of database connections, where foreign keys rule the roost and Rails is the master of puppets(🤘) pulling all the strings. When you see that posts table cozying up to a user_id attribute, that's not just some random hookup—it's a full-blown love affair. You see, in the land of databases, that user_id is like a secret handshake, linking those tables together faster than a toupee in a hurricane.

Rails has this slick little trick tucked up its sleeve called the underscore method. It's like the magician of class naming, waving its wand and transforming "FooBar" into "foo_bar". And just like that, Rails knows exactly where to find those foreign keys. But hold onto your hats, because when it comes to users following other users, we're throwing a curveball with that follower_id. Yeah, we're shaking things up, keeping Rails on its toes. By default, Rails expects a foreign key of the form <class>_id, where <class> is the lowercase version of the class name. In the present case, although we are still dealing with users, the user following another user is now identified with the foreign key follower_id. So, next time you're knee-deep in database drama, just remember: Rails may be the mastermind, but we're the ones calling the shots. And with a touch of charm and a boat, anything is possible!

# app/models/user.rb

# frozen_string_literal: true

class User < ApplicationRecord
  has_many :posts, dependent: :destroy
  has_many :active_relationships, class_name:   "Relationship",
                                  foreign_key:  "follower_id",
                                  dependent:    :destroy
  .
  .
  .
end
Enter fullscreen mode Exit fullscreen mode

Adding the belongs_to associations to the Relationship model.

# ap/models/relationship.rb

# frozen_string_literal: true

class Relationship < ApplicationRecord
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"
end
Enter fullscreen mode Exit fullscreen mode

Technically, we don't really need the followed association just yet. But, if you know me, I like to keep things symmetrical and sleek, like a well-tailored grundle area. So, why not go the extra mile and implement both follower and followed structures at the same time? It's like having peanut butter and jelly in the same sandwich! Sure, you could have one without the other, but together they just make life a whole lot tastier.

By bringing in both sides of the equation, we're not just building a system, we're crafting a work of art.

A summary of user/active relationship association methods:

Method Purpose
active_relationship.follower returns the follower
active_relationship.followed returns the followed user
user.active_relationships.create (followed_id: other_user.id) creates an active relationship associated with user
user.active_relationships.create! (followed_id: other_user.id) creates an active relationship associated with user (exception on failure)
user.active_relationships.build (followed_id: other_user.id) returns a new relationship object associated with user

Adding the Relationship model validations

We’ll add a couple of Relationship model validations for for HR transparancy.

# app/models/relationship.rb

# frozen_string_literal: true

class Relationship < ApplicationRecord
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"
  validates :follower_id, presence: true
  validates :followed_id, presence: true
end
Enter fullscreen mode Exit fullscreen mode

Followed Users

Buckle up, folks, because we're about to dive into the heart of the Relationship associations: following and followers. We're going to bust out the big guns and using has_many :through for the first time. It's like upgrading from a tricycle to a Ferrari. We're about to kick things into 4th gear. A user has many following through relationships. We're forging connections through the digital ether, linking users together in a beautiful web of social interaction. By default, Rails looks for a foreign key that matches the singular version of the association. Hey, let's face it, user.followeds is about as clunky as a grown out mullet. So instead, we're going with user.following. Smooth, elegant, like a fine whiskey🥃🎩

But wait, there's more! Rails is all about customization. So, if we want to spice things up even further, we can use the source parameter to explicitly tell Rails where to find the source of our following array. It's like giving directions to a lost puppy except in this case, we're guiding Rails to the set of followed ids. Because let's be real, nobody likes getting lost in the database.

Adding the User model following association.

# app/models/user.rb

# frozen_string_literal: true

class User < ApplicationRecord
   has_many :posts, dependent: :destroy
   has_many :active_relationships, class_name:  "Relationship",
                                   foreign_key: "follower_id",
                                   dependent:   :destroy
   has_many :following, through: :active_relationships, source: :followed
   .
   .
   .
end
Enter fullscreen mode Exit fullscreen mode

The association leads to a powerful combination of Active Record and array-like behavior. For example, we can check if the followed users collection includes another user with the include? method, or find objects through the association:

user.following.include?(other_user)
user.following.find(other_user)

We can also add and delete elements just as with arrays:

user.following << other_user
user.following.delete(other_user)

Although in many contexts we can treat following like your favorite pair of jeans, comfortable and familiar. Rails is a sneaky little devil that's smarter than you think. Take, for example, code like following.include?(other_user). On the surface, it looks like we're about to embark on a database spelunking expedition, pulling all the followed users out of the database to do a comparison. But hold onto your hats, because Rails has a trick up its sleeve. You see, Rails is like a ninja in the night, silently orchestrating things behind the scenes. Instead of dragging every followed user out into the spotlight, it arranges for the comparison to happen directly in the database.

To manipulate following relationships, we’ll introduce follow and unfollow utility methods so that we can write, e.g., user.follow(other_user). We’ll also add an associated following? boolean method to test if one user is following another.

Utility methods are like the punchlines. Sometimes you see them coming, sometimes you don't, but either way, it's all part of the fun. With experience comes the ability to predict these bad boys in advance, but even if you're caught off guard, don't sweat it. Software development is a journey of trial and error, where you write code, test it out, and if it starts to look as ugly as a chimichanga that's been sitting in the sun too long, you roll up your sleeves and refactor it. So, lets keep hacking away, there's always another joke, another punchline, and another chance to make it right 🍕🔥

Utility methods for following.

# app/models/user.rb

# frozen_string_literal: true

class User < ApplicationRecord
   .
   .
   .
   # Follows a user.
   def follow(other_user)
      following << other_user unless self == other_user
   end

   # Unfollows a user.
   def unfollow(other_user)
      following.delete(other_user)
   end

   # Returns true if the current user is following the other user.
   def following?(other_user)
      following.include?(other_user)
   end

   private
   .
   .
   .
end
Enter fullscreen mode Exit fullscreen mode

Followers

Like putting together a jigsaw puzzle, instead of a picture of a sunset, we're crafting a masterpiece of database wizardry. We're adding a user.followers method to complement the user.following method. It's like having yin without yang. All the juicy details we need to extract an array of followers are already chilling in the relationships table. And we're flipping the script, reversing the roles of follower_id and followed_id, and introducing passive_relationships to the mix. Like turning the world upside down and seeing it from a whole new perspective. The end result is going to be more beautiful than a unicorn riding a rainbow through a field of chimichangas 🦄🌈🔥

fourth diagram

Implementing user.followers using passive relationships.

# app/models/user.rb

# frozen_string_literal: true


class User < ApplicationRecord
   has_many :posts, dependent: :destroy
   has_many :active_relationships,   class_name:   "Relationship",
                                     foreign_key:  "follower_id",
                                     dependent:    :destroy
   has_many :passive_relationships,  class_name:   "Relationship",
                                     foreign_key:  "followed_id",
                                     dependent:    :destroy
   has_many :following, through: :active_relationships, source: :followed
   has_many :followers, through: :passive_relationships, source: :follower
   .
   .
   .
end
Enter fullscreen mode Exit fullscreen mode

It’s worth noting that we could actually omit the :source key for followers using simply:

has_many :followers, through: :passive_relationships

This is because, in the case of a :followers attribute, Rails will singularize “followers” and automatically look for the foreign key follower_id in this case. This keeps the :source key to emphasize the parallel structure with the has_many :following association.

Sample Following Data

I find it convenient to use rails db:seed to fill the database with seed data. Here we somewhat arbitrarily arrange for the first user to follow users 3 through 51, and then have users 4 through 41 follow that user back. The resulting relationships will suffice for developing the application interface:

db/seeds.rb

# Users
User.create!(name: "Example User",
             email: "example@t0nylombardi.dev",
             password: "foobar",
             password_confirmation: "foobar",
             admin: true,
             activated: true,
             activated_at: Time.zone.now)

99.times do |n|
  name = Faker::Name.name
  email = "example-#{n+1}@t0nylombardi.dev"
  password = "password"
  User.create!(name: name,
               email: email,
               password: password,
               password_confirmation: password,
               activated: true,
               activated_at: Time.zone.now)
end

# Posts
users = User.order(:created_at).take(6)
50.times do
  content = Faker::Lorem.sentence(5)
  users.each { |user| user.posts.create!(content: content) }
end

# Create following relationships.
users = User.all
user = users.first
following = users[2..50]
followers = users[3..40]
following.each { |followed| user.follow(followed) }
followers.each { |follower| follower.follow(user) }
Enter fullscreen mode Exit fullscreen mode

To execute this code, we will reset and seed the database:

$ rails db:migrate:reset
$ rails db:seed
Enter fullscreen mode Exit fullscreen mode

Adding following and followers actions to the Users controller.

# config/routes.rb

# frozen_string_literal: true

Rails.application.routes.draw do
   .
   .
   .
   resources :users do
      member do
         get :following, :followers
      end
   end
   .
   .
   .
end
Enter fullscreen mode Exit fullscreen mode

The intricacies of routing in Rails. Like a well-choreographed tango, each step is perfectly timed and executed. The URLs for following and followers /users/1/following and /users/1/followers, respectively are straightforward. With both pages showcasing data, we opt for the HTTP verb GET, ensuring the URLs respond just the way we want them to. And here's where it gets interesting: the member method ensures our routes respond to URLs containing the user ID, while the collection method works its magic without the need for IDs. So, whether you're stalking or being stalked, rest assured that Rails has got your back.

For more details on such routing options, see the Rails Guides article on Rails Routing from the Outside In .

HTTP request method URL Action Named route
GET /users/1/following following following_user_path(1)
GET /users/1/followers followers followers_user_path(1)

Adding the routes for user relationships.

# config/routes.rb

# frozen_string_literal: true

Rails.application.routes.draw do
   root     "static_pages#home"

   resources :users do
     member do
        get :following, :followers
     end
   end
   resources :relationships, only: [:create, :destroy]
end
Enter fullscreen mode Exit fullscreen mode

Skills to the Test

Now that you've mastered the art of stalking... I mean, following, it's time to show off your skills. We'll add some slick actions to your controllers so you can see who's stalking... I mean, following, you and who you're stalking... I mean, following. It's like a social networking power play, but with less drama and more Ruby.

With these tips and tricks, you'll be the master of social stalking... I mean, following, in no time. So go forth, my wayward son, build that social network of your dreams. And remember: with great power comes great stalking... responsibility... and a whole lot of cat videos. Happy coding! 🚀🐱

Cheers

Top comments (0)