ActiveRecord::UnionRelation

I wrote a gem back in 2020 called active_record-union_relation, and I’m finally writing a blog post describing it. This is a very small gem that provides the ability to define Active Record relations from UNION queries. The resulting records that are returned when the queries are executed are polymorphic (i.e. they can be instances of different classes). I’ll show how this works and why you might want to use it.

Background for SQL

First, it’s important to understand what a UNION query does. A UNION query combines the results of two or more SELECT statements into a single result set. Those SELECT statements can be from completely different tables. Note that for most databases, the columns in the SELECT statements must be the same.

For example, let’s say you have comments, posts, and tags tables. They are layed out like this:

+-----------------+    +-----------------+    +-----------------+
| comments        |    | posts           |    | tags            |
+-----------------+    +-----------------+    +-----------------+
| id              |    | id              |    | id              |
| post_id         |    | title           |    | name            |
| body            |    | published       |    +-----------------+
+-----------------+    +-----------------+

Assume for our purposes that the column types follow your intuition. For example, comments.post_id is a foreign key to posts.id. Now, let’s say you want to find all of the records that match a certain search query. You would write something like this:

SELECT 'comment' as 'type', id, body AS 'value' FROM comments WHERE body LIKE '%foo%'
UNION
SELECT 'post' as 'type', id, title AS 'value' FROM posts WHERE title LIKE '%foo%'
UNION
SELECT 'tag' as 'type', id, name AS 'value' FROM tags WHERE name LIKE '%foo%'

This query will return a result set that looks like this:

+---------+----+-------+
| type    | id | value |
+---------+----+-------+
| post    | 1  | foo   |
| tag     | 1  | foo   |
| tag     | 2  | foo   |
| comment | 1  | foo   |
+---------+----+-------+

In this way you can use a single query to search across multiple tables. This is a very simple example, but you can imagine how this could be useful in a more complex scenario. There are more options like UNION ALL and other complications that can be used, but I won’t go into those here.

Background for Active Record

An important thing to remember about the Active Record ORM is that records can represent table rows in very different states with regard to persistence. For example, a record that is brand new can have all of its fields be nil without error, whereas a record fetched from the database should in theory have its fields populated.

However, the values that Active Record uses for populating fields is entirely up to the query that fetches the fields. You can use ActiveRecord::Relation#select to modify the fields that are fetched and Active Record will happily store them in instances variables when the database returns them. For example you could User.select("id * 2 AS 'id'") and you will get back User objects with double their ID.

In essence, when Active Record records are fetched from the database, they are functioning more as views into the results of a query than as a direct representation of table rows. The query doesn’t even necessarily need to correspond to the same table or have any of the same columns — the records will conveniently return fields using #method_missing when necessary.

For example, let’s take our database schema from the SQL backend section and wrap it up in Active Record objects. Let’s presume we have:

class Comment < ActiveRecord::Base
  belongs_to :post
end

class Post < ActiveRecord::Base
  has_many :comments
end

class Tag < ActiveRecord::Base
end

Most queries that folks write would look something like Post.all. This breaks down simple select: SELECT * FROM posts. However, you could just as easily have written Post.from("SELECT id, body AS 'title' FROM comments"). This would return Post objects with the id and title fields populated from the comments table.

Background for Single Table Inheritance (STI)

There is one place in Active Record that returns polymorphic arrays when a query is executed: single table inheritance. Single table inheritance is a feature of Active Record that allows you to have a single table represent multiple classes. For example, let’s say you have a posts table that looks like this:

+-----------------+
| posts           |
+-----------------+
| id              |
| type            |
| title           |
| published       |
+-----------------+

You can have classes that look like:

class Post < ActiveRecord::Base
end

class VideoPost < Post
end

class AudioPost < Post
end

If you were to iterate over Post.all, you would get back an array of VideoPost and AudioPost objects. It does this by looking at the type column and instantiating the appropriate class. Importantly, it does this by looking at the #inheritance_column method. It then will call the #instantiate method, and eventually the #instantiate_instance_of method, which is where the split on type happens.

ActiveRecord::UnionRelation

In applications that I have written, I have found it helpful to use UNION queries to provide basic search functionality. (If the application grows or the search becomes more complicated then full-text options become more attractive.) There isn’t a native way to do this in Active Record. This is where the active_record-union_relation gem comes in.

Effectively this gem provides a DSL to combine multiple subqueries into a single relation that can be used like any other relation. The resulting relation will return polymorphic records when the query is executed by taking advantage of the same mechanism used by single table inheritance. Let’s take, for example, the models defined above. If we wanted to search the various text columns for a specific query, we could write:

term = "foo"

relation =
  ActiveRecord.union(:id, :post_id, :matched) do |union|
    union.add(
      Post.where(published: true).where("title LIKE ?", "%#{term}%"),
      :id, nil, :title
    )

    union.add(
      Comment.where("body LIKE ?", "%#{term}%"),
      :id, :post_id, :body
    )

    union.add(
      Tag.where("name LIKE ?", "%#{term}%"),
      :id, nil, :name
    )
  end

relation.order(matched: :asc)

In the above code, we call ActiveRecord::union with the names of the common columns that will be returned by each subquery. Then within the given block, we add each subquery in turn. By adding a subquery we provide a relation and then each of the fields that should be mapped to the common columns of the overall union (using nil to mean we don’t have something for that value). The resulting relation will return Post, Comment, and Tag objects that have id, post_id, and matched fields populated with the appropriate values.

Notice that in the code above we call #order on the relation. This works because the returned object is an ActiveRecord::Relation object. As such, all other query methods will work as expected.

Wrapping up

As a quick disclaimer: I probably wouldn’t pull in an entire separate gem just to provide this functionality. At the end of the day the code boils down to about 100 lines with lots of comments so this is more in the realm of copy-paste-able code. However, I think it’s a neat trick and I hope you do too. Mostly I like this because it mirrors Ruby’s idea of duck-typing. Effectively we’re treating each record as an object fulfilling a role rather than a nominal type. This feels very at home in a Ruby application.

If you have any questions or comments, feel free to reach out to me on Twitter @kddnewton or GitHub kddnewton.

← Back to home