Combining Different Models in a Single Query in Rails

4 Nov, 2024
Pete Hawkins

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 and Video).
  • 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 calling ArticleList.
  • 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:

  1. Service Object: Encapsulate complex queries in a service object for cleaner code.
  2. Arel Queries: Use Arel to query and union data from multiple models.
  3. Pagination: Easily integrate kaminari for paginated results.
  4. 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!

Discussion (0)

To comment you need to sign up for a free account or sign in.

Get new videos in your inbox weekly!

Be the first to hear about new courses and videos launching. You’ll only receive relevant news, no spam.