How to Fix Rails 6.1 Relation `merge` Deprecation
Recently, while working on a Rails 6.1 to 7.0 upgrade, we encountered the following deprecation warning regarding changes made to ActiveRecord::Relation’s merge
method:
"Merging (#{node.to_sql}) and (#{ref.to_sql}) no longer maintains both conditions, and will be replaced by the latter in Rails 7.0. To migrate to Rails 7.0's behavior, use relation.merge(other, rewhere: true)."
In this article, we will talk about the expected behavior of merge
, how it has changed and what to do in order to use the new behavior if you find yourself looking at this deprecation.
What is merge
?
Let’s first take a look at the expected behavior of merge .
Using merge
is actually pretty useful for when you want to combine two complex relation objects that each contain multiple conditions, joins, and/or includes.
You can call merge
on a relation object and pass another relation object as its argument. Rails then merges the two relation objects into a new relation object that includes the conditions, joins, includes, and other clauses from both relation objects.
Say you’d like to have two relation objects, posts
and featured_posts
posts = Post.where(published: true)
featured_posts = Post.where(featured: true)
However, instead you would like to return a single relation object of all published posts that have been featured, you can do:
Post.where(published: true).merge(Post.where(featured: true))
This will execute a single query with the AND clause and return a single relation object
SELECT "posts".* FROM "posts" WHERE "posts"."published" = ? AND "posts"."featured" = ?
=> #<ActiveRecord::Relation [#<Post id: 1, title: "testing 1", body: "Hello world!!", published: true, featured: true, author_id: 1, created_at: "2023-05-07 18:51:11.370457000 +0000", updated_at: "2023-05-07 18:51:46.974839000 +0000">, #<Post id: 3, title: "testing 3", body: "This is the third post", published: true, featured: true, author_id: 3, created_at: "2023-05-07 18:51:11.373971000 +0000", updated_at: "2023-05-07 18:51:54.961656000 +0000">]>
What is even better is that this new relation object can then be further queried and/or executed upon if necessary.
This is more efficient as you can avoid duplicating any conditions or joins.
New behavior in Rails 7.0
According to the Ruby on Rails 7.0 Release notes :
"Merging conditions on the same column no longer maintain both conditions, and will be consistently replaced by the latter condition."
With the following examples:
# Rails 6.1 (IN clause is replaced by merger side equality condition)
Author.where(id: [david.id, mary.id]).merge(Author.where(id: bob)) # => [bob]
# Rails 6.1 (both conflict conditions exists, deprecated)
Author.where(id: david.id..mary.id).merge(Author.where(id: bob)) # => []
# Rails 6.1 with rewhere to migrate to Rails 7.0's behavior
Author.where(id: david.id..mary.id).merge(Author.where(id: bob), rewhere: true) # => [bob]
# Rails 7.0 (same behavior with IN clause, mergee side condition is consistently replaced)
Author.where(id: [david.id, mary.id]).merge(Author.where(id: bob)) # => [bob]
Author.where(id: david.id..mary.id).merge(Author.where(id: bob)) # => [bob]
So what does this all mean?
In the new behavior, when attempting to merge two Active Record relation objects that have conflicting conditions on the same column, the conditions of the first relation object (mergee) will be discarded and only the second relation object’s (merger) conditions will be retained. This creates an unexpected result since part of the query is ignored, only returning the latter condition’s results.
In some cases, even if conditions are performed on the same column, there will be no deprecation warning but the new behavior of merge
is maintained, which means the returned relation object will only satisfy the mergers condition.
Cases that do not raise a warning
Queries written using strings as the conditional statements
For example:
Post.where("created_at <= ?", 3.months.ago).merge(Post.where("created_at >= ?", 3.months.ago))
The generated query maintains both conditions combined with an AND
clause:
SELECT "posts".* FROM "posts" WHERE (created_at <= '2023-02-08 14:53:26.967529') AND (created_at >= '2023-02-08 14:53:26.970391')
Or let’s say we used a string for one condition and a key/value pair for the mergee:
Post.where("created_at <= ?", 3.months.ago).merge(Post.where(created_at: 3.months.ago))
SELECT "posts".* FROM "posts" WHERE (created_at <= '2023-02-09 14:43:20.344537') AND "posts"."created_at" = ?
Both examples do not raise the deprecation because Rails does not know that the same attributes are being compared, hence there is no conflict Rails is aware of, and they behave the same way in both Rails 6.1 and 7.0.
The merger and the mergee both perform IN
or =
on the same attribute
In this case, merge
will behave as expected in Rails 7 by replacing the mergee with the mergers condition:
Post.where(created_at: "2023-05-02").merge(Post.where(created_at: "2022-05-08"))
SELECT "posts".* FROM "posts" WHERE "posts"."created_at" = ?
=> #<ActiveRecord::Relation [#<Post id: 1, title: "testing 1", body: "Hello world!!", published: true, featured: true, author_id: 1, created_at: "2022-05-08 00:00:00.000000000 +0000", updated_at: "2023-05-08 14:16:51.947757000 +0000">]>
-or-
Post.where(created_at: ["2023-05-02", "2023-02-01"]).merge(Post.where(created_at: "2022-05-08"))
SELECT "posts".* FROM "posts" WHERE "posts"."created_at" = ?
=> #<ActiveRecord::Relation [#<Post id: 1, title: "testing 1", body: "Hello world!!", published: true, featured: true, author_id: 1, created_at: "2022-05-08 00:00:00.000000000 +0000", updated_at: "2023-05-08 14:16:51.947757000 +0000">]>
Cases that raise a deprecation warning
This occurs when there is a combination of clauses performed on the same column, causing a conflict in the query.
For example:
Post.where(created_at: 1.year.ago..3.months.ago).merge(Post.where(created_at: 1.week.ago..))
> ActiveSupport::DeprecationException (DEPRECATION WARNING: Merging ("posts"."created_at" BETWEEN ? AND ?) and ("posts"."created_at" >= ?) no longer maintain both conditions, and will be replaced by the latter in Rails 7.0. To migrate to Rails 7.0's behavior, use `relation.merge(other, rewhere: true)`. (called from irb_binding at (irb):19))
If we take a look at the query generated by the mergee, we can see a BETWEEN
as well as a >=
condition:
SELECT "posts".* FROM "posts" WHERE "posts"."created_at" BETWEEN ? AND ? AND "posts"."created_at" >= ?
Therefore, the merger condition creates a conflict and raises the deprecation.
To fix this deprecation warning to use the new Rails 7 behavior, the rewhere: true
option must be passed to merge
:
Post.where(created_at: 1.year.ago..3.months.ago).merge(Post.where(created_at: 1.week.ago...), rewhere: true)
which will then query the merger condition and return:
SELECT "posts".* FROM "posts" WHERE "posts"."created_at" >= ?
=> #<ActiveRecord::Relation [#<Post id: 3, title: "testing 3", body: "This is the third post", published: true, featured: true, author_id: 3, created_at: "2023-05-02 00:00:00.000000000 +0000", updated_at: "2023-05-08 14:09:51.369571000 +0000">]>
In the case where there is a need to maintain both conditions, writing a statement similar to the first example would be best:
Post.where("created_at IN ? AND ?", 1.year.ago, 3.months.ago).merge(Post.where("created_at >= ?", 1.week.ago))
With both conditions maintained:
SELECT "posts".* FROM "posts" WHERE (created_at BETWEEN '2022-05-08 17:06:17.771827' AND '2023-02-08 17:06:17.772156') AND (created_at >= '2023-05-01 17:06:17.773653')
Conclusion
Utilizing the merge
method to build efficient and complex queries can be extremely beneficial. However, with the recent changes introduced in Rails 7.0, you might find yourself looking at unexpected results, or a deprecation warning.
It is important to further investigate what Active Record is trying to do through the SQL queries generated to further understand the best way to solve the issue and get the expected results.
Need help upgrading Ruby or Rails to the latest stable version? We have some availability to help your team upgrade Ruby/Rails !