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 anid
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]
).