Rails’ ActiveRecord models can now be configured with composite primary keys, making it easier to work with complex data models. This feature was introduced in Rails 7.1.2 and allows you to define a primary key that consists of multiple columns.
ActiveRecord has always been a powerful tool for working with databases, but it has traditionally been limited to using a single column as the primary key. Back in November of 2022, Rails added query_constraints
to allow models to be queried by a combination of keys for basic functions such as find
, update
and reload
. This was a step in the right direction but the primary key would still be a single column, which is not a complete solution for all cases.
Before
Let’s consider a data model that contains a basket and a cart. A basket has many carts and a cart belongs to a basket. Each basket is identified by both its id
and it’s manufacturer_id
. It might look like this,
Here we use the query_constraints
method to define how a Basket
can be queried. Similarly, we use the query_constraints
option in the belongs_to
association to define how a Cart
can be queried.
However a basket can not be identified by just its id
alone, it also needs the manufacturer_id
.
This is where composite primary keys come in. Unfortunately the current implementation does not support composite primary keys.
When basket.id
is called, it returns the id
value of the basket, which is not a complete representation of the primary key.
This can be confusing and misleading when working with complex data models.
In a real world scenario, id
might not carry any value by itself and it’s the combination of id
and manufacturer_id
that defines a basket.
After
Now thanks to this PR, a primary_key
can be an array. This cleans up associations and queries making it far easier to both understand and convey relationships.
Let’s look at this example with composite primary keys.
It is important to understand that this is only an ActiveRecord level change. The underlying database still considers the primary key to be a single column, the id
which is an auto-incrementing integer.
With the self.primary_key = [:id, :manufacturer_id]
option it’s obvious that a basket is identified by both its id
and manufacturer_id
. This makes it easier to understand the model and its relationships.
Now when basket.id
is called, it returns an array of the primary key values, instead of the underlying database id
value. This is a more accurate representation of our primary key in this example.
This change also makes it easier to define associations, the query_constraints
option is no longer needed in the belongs_to
association. The has_many
association in Basket
now uses the composite primary key to define the relationship.