Database Views in Rails with Scenic
This article describes how database views can be a solution for an application with complex relationships and how Scenic gem simplifies the task of implementing those views in your Rails application.
The App
The app we are currently working on is a customized management platform for software development agencies. Clients may have multiple projects, projects may have multiple developers, designers etc. The tree branches out pretty broadly in terms of a developer to project assignment as this is the core feature of the application.
The Problem
Our client asked for a view representing people’s availability for assignment to a new project. An employee is perceived as available if he meets any of the following criteria:
- is currently on an internal project
- is in an external project that ends in less than one month in the future
- is not assigned to any project
The database structure in terms of user-to-project relation looks as follows:
Possible solutions
Combined Scopes
The first solution that came to us was to use multiple scopes and combine them together. This would be the most “rails way” approach. The drawback of the solution turned out to be the level of complexity. The building scopes would need to be scattered across the user and the project scopes. The query built by ActiveRecord would be far from optimal and simple. As a result, the code could be harder to understand and maintain.
Flag record on the users table
Another solution was to implement a flag that is going to store information if a user is available or not. The drawback of the solution would be the need to maintain a quite complicated callback structure that is going to update the user model every time the end_date changes.
The end_date
stands for the end of an assignment for a developer. Not always the project end date is the same as the developer’s assignment end date. There are situations where a developer may be assigned only for a short period of time to complete a task or to help the team to meet a deadline.
The implementation would not be complicated but we want to avoid callbacks as they are difficult to maintain and blur the transparency of the database - model layer flow.
Database Views
The final solution that we came up with was to use database views to join multiple queries. This seemed to fit all our needs, as there would be a single query executed only if needed and there would be no callbacks required. A database view works like a “virtual” table and can be joined with other “physical” tables. We agreed that this solution was the most fitting and we gave it a shot.
Implementation
We’ve searched for a ready-made gem which implements an interface for managing database views and we’ve stumbled upon the Scenic gem from Thoughtbot:
https://github.com/thoughtbot/scenic
The gem had everything we needed and the implementation was somehow natural with Rails. The view creation is very similar to a schema update and is triggered by db:migrate
. Another great feature is that any created view works with ActiveRecord as a regular model and comes with all its native features. Scenic turned out be a perfect fit for our scenario.
Installation:
Add scenic
to your Gemifle
Run: bundle install
To generate a view simply run:
rails generate scenic:view view_name
In our case, the view_name was available_people
The last command created a new empty schema migration file in the following path: ~/app/db/views/available_people_v01.sql
Contents of our availablepeoplev01.sql file:
SELECT users.id FROM users
JOIN assignments on users.id = assignments.user_id
JOIN projects on assignments.project_id = projects.id
WHERE assignments.end_date < (now() + '1 month'::interval) AND projects.internal = false
UNION
SELECT users.id FROM users
JOIN assignments on users.id = assignments.user_id
JOIN projects on assignments.project_id = projects.id
WHERE projects.internal = true AND (assignments.end_date > now() OR assignments.end_date is NULL)
UNION
SELECT users.id FROM users
LEFT JOIN assignments on users.id = assignments.user_id
WHERE assignments is NULL;
We can access the view by using a simple sql query in a model or a scope. An advantage of this solution is putting the workload in the DB and keeping the implementation in the model to a bare minimum.
After the file was edited and filled in with our queries we were able to apply the view to our database by simply running:
rake db:migrate
The process for creating a view is similar to creating new table.
The model layer
We had two options to access the data from the view in the model. We could create a new model in Rails or use a direct query from the User
model. In this case, we only needed to see if a given user_id was present in our view and scope the user model by it.
# app/models/user.rb
scope :available, -> { joins("JOIN available_people on users.id = available_people.id") }
Now, we could use our database view with the User
model. We can always create a model for the database view in case we need to do any data manipulation on it.
Conclusions
Scenic has much more to offer and it’s a very useful tool to have in your toolbox. I can highly recommend getting acquainted with it and utilise database views in your everyday backend work.