ActiveRecord in its current state is designed to work with the primary key column (the default is the id column). Hence, if you notice the update, destroy, and reload actions on an ActiveRecord::Base object, it fetches the data based on the id column, and would not have the flexibility to choose any different set of constraints while performing those 3 actions.

Here's an example of reload action on the Post model instance(post.reload).

Post Load (1.3ms)  SELECT "posts".* FROM "posts" WHERE "posts"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]

If the app we have set up has good composite indexes, before the ActiveRecord query_constraints config (introduced in Rails 7.1), the ActiveRecord could not apply such constraints by default & utilize the composite indexes, especially with the update, destroy, and reload actions.

The new ActiveRecord config query_constraints introduced in PR changes this behavior and allows us to specify specific columns whenever fetching/performing actions on an ActiveRecord::Base object, we'll learn how.


Let's understand the need of query_constraints with a relatively simple example of an eCommerce SaaS model of Store & Product:

# == Schema Information
#
# Table name: products
#
#  id          :integer(4)      not null, primary key
#  store_id    :integer(4)
#  category_id :integer(4)
#  title       :string(255)

# Indexes
# :id -> Unique, Primary Key
# [:store_id, :category_id] -> Composite Index

class Product < ActiveRecord::Base
 belongs_to :store
end

product = Product.first
# => SELECT "products"."*" from "products" LIMIT 1

product.update(title: "Lord of the Rings")
# => UPDATE "products" SET "title" = 'Lord of the Rings' WHERE "products"."id" = 1


In the above example, we see that the id column was used in the where clause, and we were not able to properly utilize the composite indexes store_id and category_id.

Now let's see the behavior after adding the query_constraints to the same setup:

# == Schema Information
#
# Table name: products
#
#  id          :integer(4)      not null, primary key
#  store_id    :integer(4)
#  category_id :integer(4)
#  title       :string(255)

# Indexes
# :id -> Unique, Primary Key
# [:store_id, :category_id] -> Composite Index

class Product < ActiveRecord::Base
 query_constraints :store_id, :category_id, :id
 belongs_to :store
end

product = Product.first
# => SELECT "products"."*" from "products" LIMIT 1

product.update(title: "Lord of the Rings")
# => UPDATE "products" SET "title" = 'Lord of the Rings'\
# WHERE "products"."store_id" = 1 AND "products"."category_id"='11' AND "products"."id" = 1

In this example, with query_constraints configured, the query automatically and effectively uses our composite indexes store_id and category_id.

The query_constraints might be vital for the apps that are designed to work with composite primary keys, for which a prime example would be a Multi-Tenant sharded database design.


What a Multi-Tenant sharded database is?

A database can be designed depending on performance, scaling, and maintenance requirements.

In a multi-tenant sharded database pattern, the table schemas inside each database have a tenant key in the primary key of tables that stores the tenant data. This "tenant key" enables each individual database to have 1 or many tenants.

A tenant's data can be distributed across multiple databases or shards, where all the data for a single tenant is contained in one shard.


Coming back to our above example of the eCommerce SaaS model of Store & Product, consider if we decided to have Database sharding on the store_id column, the default constraints become a must-have.

To conclude:

With the changes like Allow specifying columns to use in ActiveRecord::Base object queries, the Rails framework is being prepared to provide generic solutions to the Multi-Tenant Sharded database implementations. With this, the conventional dependency of having an id column for an ActiveRecord::Base object will go away and provide us the ability to tread the composite columns as the primary key(like in our example it was [:store_id, :category_id]).