Followings in Sinatra

By @zachfeldman
Written on Jul 23, 2014

Let’s say that you’re building an app using the wonderful Sinatra, a lightweight web application development framework. You’ve got a User model/table created and you’d like one user to be able to follow another user. This relationship has to be persisted in your database and saved somehow. You want to be able to make queries like:

1
2
3
4
5
User.last.followed #retrieve an array of users this user has followed User.last.followers #retrieve an array of users following this user

You  also want to easily setup one user to follow another inside of your Sinatra routes with a handy instance method:

1
2
3
4
5
6
get "/users/:id/follow" do user = User.find(params[:id]) current_user.follow!(user) if user flash[:notice] = "User followed successfully." redirect "/" end

This is what is known as a self-referential database relationship. It is similar to other join table relationships like the one between users, addresses, and usersaddresses (join table). However, instead of joining one table with the records in another table to form a many-to-many relationship, we have this self-referential relationship where one user is referencing another user. All that is required to make this relationship real is to create a relationships join table with a followerid an followed_id column, both integers, rather than having 3 tables in total. Our “third table” is just a reference back to the users table.

Assuming you’re using ActiveRecord with Sinatra, create a new migration:

1
rake db:create_migration NAME=create_relationships_table

then fill it with the following:

1
2
3
4
5
6
7
8
class CreateRelationshipsTable < ActiveRecord::Migration def change create_table :relationships do |t| t.integer :follower_id t.integer :followed_id end end end

Run the migration to add the table to your database:

1
rake db:migrate

The next step is to make ActiveRecord aware of this relationship. Normally, say in the users/addresses example, we’d do something like the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class User < ActiveRecord::Base has_many :users_addresses has_many :addresses, through: :users_addresses end class Address < ActiveRecord::Base has_many :users_addresses has_many :users, through: :users_addresses end # this join table has an address_id and user_id column for each row of data, associating one user with one address # for each row, but allowing us to associate multiple addresses with multiple users and vice versa by adding more rows class UsersAddress < ActiveRecord::Base belongs_to :address belongs_to :user end

Notice how each of the tables needs to first establish a relationship to the join table, then establish the relationship to the table being joined to in the second line of code. We need to do a similar thing for our relationships join table and the users table, to setup a relationship that goes in one way, then the reverse of that way.

Let’s start with the followed relationship:

1
2
3
4
5
6
7
8
class User < ActiveRecord::Base has_many :relationships, foreign_key: :follower_id has_many :followed, through: :relationships, source: :followed end class Relationship < ActiveRecord::Base belongs_to :followed, class_name: 'User' end

This one isn’t so bad to setup. First, we’ll setup the relationship to the join table using the first line. We need to specify exactly which foreignkey for the relationship to use. Normally, when we declare a relationship to a join table, the foreign key is inferred using the name of the model itself. In this case, since the followerid is not just a user_id, we need to be explicit.

Next up, we create the self-referential relationship back to the User model as we normally would when creating a join table relationship with ActiveRecord. The difference here is that we mention a source parameter, letting us know which belongsto statement in the Relationship model we’re referring to. Note that in that Relationship model, we’ve added a belongsto statement that doesn’t address the users table, but instead the followed relationship declaration found in the User model on line 3. We have to explicitly give ActiveRecord the class name for this belongs_to relationship on line 7, since it cannot be inferred.

To test out that this worked correctly, try setting up a followed relationship between two existing users:

1
User.find(1).followed << User.find(2)

Then try to query that relationship:

1
2
User.find(1).followed => [#]

This should also create an entry in our join table, which is what actually establishes that relationship:

1
2
Relationship.first => #

That takes care of finding out who users have followed. But what about the followers relationship, which tells us which users are following a specific user? This one is a little more complicated.

1
2
3
4
5
6
7
8
class User < ActiveRecord::Base has_many :reverse_relationships, foreign_key: :followed_id, class_name: 'Relationship' has_many :followers, through: :reverse_relationships, source: :follower end class Relationship < ActiveRecord::Base belongs_to :follower, class_name: 'User' end

To make this relationship work, we have to setup another has many through style relationship. However, we’ve already used up the “normal” way to do this before, when we used the hasmany :relationships to setup our first self-referential join. In order to create another connection to the relationships table, we have to specify the classname of our join table explicitly in addition to the foreign_key we’re using on that table.

Next up, you’ll see a similar line to our last setup that brings the relationship back to the User model from the Relationship model. Notice that the source has changed and a corresponding belongs_to statement has been setup in the Relationship model that also explicitly specifies class name.

To test out that this worked correctly, try setting up a following relationship between two existing users:

1
User.find(1).followers << User.find(2)

Then try to query that relationship:

1
2
User.find(1).followers => [#]

This should also create an entry in our join table, which is what actually establishes that relationship:

1
2
Relationship.first => #

You should also be able to query the last relationship we setup, except the other way around!

1
2
User.find(2).followers => [#]

The next step is to make an easy to use method to have one user follow another, as if concatenation wasn’t easy enough. This can go in our User model:

1
2
3
def follow!(user) followed << user end

To make this work using a web interface, we can just use our awesome and easy to use method. There are conflicting opinions on what kind of request should fulfill this HTTP call but a GET request works fine for illustrative purposes:

1
2
3
4
5
6
get "/users/:id/follow" do user = User.find(params[:id]) current_user.follow!(user) if user flash[:notice] = "User followed successfully." redirect "/" end

We hope this post is helpful to you! It was inspired by the Michael Hartl tutorial for Rails but we found that a similar, simple guide for Sinatra that didn’t incorporate testing did not exist. Testing is of course important, but understanding this concept at its most basic can be a very good learning exercise. Here’s an accompany GitHub repo with all the code written in this post and a really simple set of routes setup:

https://github.com/nycda/sinatra-activerecord-followings

X-posted from the New York Code + Design Academy blog.





Sign up for our e-mail list to hear about new posts.