Combining Different Models in a Single Query in Rails
When building out new features in Rails, you’ll often find yourself needing to combine data from multiple models. Recently, I faced a similar scenario while adding a blog section to rapidruby.com. I already had Video
models with blog-style content attached to them and had just created an Article
model for blog posts. To maintain a smooth user experience, I wanted both Articles and Videos to appear together on the blog page, ordered by date, without splitting them into separate lists.
This post will walk you through how I tackled this using Arel, a service object, and kaminari
for pagination. Let’s dive in!
Why Combine Queries?
A straightforward approach could be to query the Article
and Video
models separately. However, I wanted a unified list to simplify ordering by date and to make pagination a breeze. By combining both models into a single query, I could achieve an elegant, seamless flow for the content, all while making the code cleaner and easier to maintain.
Step 1: Setting Up a Service Object with Arel
To keep the code organized, I created a service object called ArticleList
. Service objects are a Rails design pattern that helps encapsulate logic that doesn’t belong in the model, view, or controller. Here’s what our service object structure looks like:
# app/services/article_list.rb
class ArticleList
def self.all
new.all
end
def all
combined_query = Article
.from("(#{union_query.to_sql}) AS combined_records")
.select("id, title, body, author_id, created_at, type")
.order(created_at: :desc)
end
private
def articles_table
Article.arel_table
end
def videos_table
Video.arel_table
end
def articles_query
articles_table.project(articles_table[:id], articles_table[:title], articles_table[:body], articles_table[:author_id], articles_table[:created_at], Arel.sql("'Article' AS type"))
end
def videos_query
videos_table.project(videos_table[:id], Arel.sql("videos.name as title"), videos_table[:body], Arel.sql("videos.creator_id as author_id"), videos_table[:created_at], Arel.sql("'Video' AS type")).where(videos_table[:blog_post].eq(true))
end
def union_query
articles_query.union(videos_query)
end
end
Breaking Down the Code
- Arel Tables: First, I set up Arel tables for each model (
Article
andVideo
). - Building Queries: Using Arel, I project and map the columns I needed for both tables, as you can see article and video tables are slightly different, so I map them into a common format.
- Union and Ordering: I then union both queries and apply an order by
created_at DESC
to get the latest content first.
Tip: Arel is an advanced way to build SQL queries in Rails, especially useful when you need flexibility beyond what ActiveRecord offers directly.
Step 2: Pagination with kaminari
Once the query is ready, adding pagination with kaminari
is straightforward. Since the combined query returns a single dataset, pagination will work out of the box without any additional setup for both models.
Here’s how you can integrate kaminari
at the controller level:
# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
skip_before_action :authenticate
def index
@articles = ArticleList.all.includes(:author).page(params[:page]).per(4)
end
# ...
end
Explanation
- This depends on the kaminari gem.
- Pagination Parameters: By adding a
page
parameter, you can specify the page number when callingArticleList
. per
Method: The.per(4)
sets the number of items per page, which you can adjust as needed.
Alternative Approach: Using Database Views with Scenic
If the blog content grows in complexity or volume, creating a database view might be more efficient. The Scenic gem allows you to create database views in Rails, offering a performant way to handle more complex queries. This approach, however, can add additional complexity to the setup and might be overkill for simple use cases.
For now, the Arel and union approach is straightforward and performant enough for this scenario. However, if you plan on scaling, Scenic is a powerful alternative worth exploring.
Wrapping It Up
Combining data from different models in Rails can make your app’s content feel cohesive and improve the user experience. Here’s a quick recap of what we covered:
- Service Object: Encapsulate complex queries in a service object for cleaner code.
- Arel Queries: Use Arel to query and union data from multiple models.
- Pagination: Easily integrate
kaminari
for paginated results. - Future Scalability: Consider Scenic for larger datasets or more complex queries.
This approach offers a flexible way to manage and display data from multiple sources without overcomplicating your codebase. Give it a try in your own Rails projects, and let me know how it works for you!
Have any questions or alternative methods? Drop them in the comments below. Happy coding!