Rails makes database selection and association loading explicit
Rails 6.1 RC1 adds horizontal sharding and granular connection switching, while strict loading catches unintended association queries.
A model method can look local while its database work depends on context elsewhere in the application. The same lookup may target a writer or a replica. Reading an association may issue an additional query. Rails 6.1's first release candidate gives applications more control over both decisions, with horizontal sharding, connection switching scoped to an abstract model class, and strict association loading.
Eileen Uchitelle's November 2 announcement explains the connection limitation behind the sharding work. Earlier support allowed multiple databases with different schemas, but a model class could have only one connection per role. Horizontal partitions need multiple connections with the same role and schema. A shard identifier supplies the additional distinction. This is RC1, so the announcement asks applications to exercise the candidate and report bugs before the final release.
I like Ruby's concise collection and object syntax, but database access is one place where concision can hide consequential work. These changes make the destination of that work and the permission to perform it more visible. They also expose responsibilities that Rails cannot infer from a model name.
Roles and shards describe different choices
The multiple database guide shipped in RC1 distinguishes a shard from a replica. Shards divide rows while retaining a common schema. A replica provides a reading connection for a particular database. Choosing a reading role therefore does not answer which shard contains a customer's data.
The configuration maps these two dimensions independently. Database names below refer to entries in database.yml, including replica entries marked with replica: true.
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
connects_to shards: {
default: {
writing: :primary,
reading: :primary_replica
},
shard_one: {
writing: :primary_shard_one,
reading: :primary_shard_one_replica
}
}
end
The model hierarchy can continue to describe one kind of record across those partitions. A block selects the connection context for the operation. Under the documented sharding API, the call supplies both a role and a shard.
ActiveRecord::Base.connected_to(
role: :reading,
shard: :shard_one
) do
Person.first
end
This reads through the configured replica for shard_one. The connection-handling source rejects a shard argument without a role. Its documentation also specifies that an unknown connection produces ActiveRecord::ConnectionNotEstablished. The API selects a configured pool and restores the prior context when the block ends.
An identifier alone is insufficient to describe the location of a record in this arrangement. The guide illustrates creating a record in one shard and failing to find it in another. Application code needs to retain the appropriate shard context when it follows identifiers. Rails provides connection selection, but RC1 does not supply an automatic strategy that derives that context from a request. The guide assigns shard switching to application middleware or an around_action.
Switching one database hierarchy
Multiple database support also needs to distinguish independent model hierarchies. An application might store people in its primary database and animals in a separate database. Switching animal queries to a replica should not necessarily change where person queries execute. Rails 6.0's role switch affected all database connections, according to the announcement.
RC1 supports granular switching when the application disables legacy connection handling.
config.active_record.legacy_connection_handling = false
With separate abstract connection classes configured, the switch can name the class whose descendants should use the selected role. The guide uses AnimalsRecord for this purpose.
AnimalsRecord.connected_to(role: :reading) do
Dog.first
Person.first
end
Here Dog inherits through AnimalsRecord and reads its replica. Person, under the separate primary hierarchy, continues reading the primary writer. Calling ActiveRecord::Base.connected_to retains global switching. The distinction gives a block a narrower effect without requiring every query in that block to state a database name.
The implementation enforces the class boundary. With legacy handling enabled, connected_to belongs on ActiveRecord::Base. With the newer handling, the receiver may also be an abstract class. A concrete model cannot become an arbitrary connection-switching boundary merely because a caller invokes the method on it. Grouping related models under an abstract connection class remains part of the database design.
I find that restriction useful because it keeps connection ownership in the model hierarchy. A block's reading role should have a defined set of affected models. Otherwise, a helper that changes connection context can alter unrelated database operations several calls below it.
The guide also recommends database credentials that enforce read-only access for replicas. Rails' connection code describes write prevention as protection against accidental writes and explicitly distinguishes it from a read-only database user. Role names communicate application intent, while database permissions enforce the server's access policy. Replica selection also leaves replication delay relevant to what a read can observe.
Strict loading controls implicit queries
Strict loading addresses another kind of hidden database work. A record's association can remain unloaded until code accesses it. Iterating over many records and accessing an unloaded association for each produces the familiar N+1 query pattern. RC1 adds a way to reject that lazy association loading instead of allowing it silently.
The record implementation exposes strict_loading! and strict_loading?. The relation API allows the choice when selecting records.
user = User.strict_loading.first
user.comments.to_a
# Raises ActiveRecord::StrictLoadingViolationError
This example assumes the comments association has not already loaded. Strict loading does not select the associations an application needs or eagerly fetch them by itself. It establishes a constraint on subsequent association access. The query that obtains a user and the association load that obtains comments remain distinct operations.
The association loader checks the owner's strict-loading flag when finding an association target. It also checks whether the association declaration itself requests strict loading. These checks occur before the loader executes its target query. The announcement notes support for enabling strict loading by default as well, so applications can choose a broader policy instead of marking individual records.
Strict loading detects the prohibited access even when a particular run would load only one extra association. That is useful because an N+1 defect depends on repetition, while the source of the defect is the unplanned lazy load. A small input can expose the access without needing enough records to make a request visibly slow. Explicit queries elsewhere remain outside this particular constraint.
I prefer this kind of restriction to relying on readable model code as evidence of predictable database work. Shard selection establishes where an operation runs, while strict loading makes certain additional operations fail at their point of use. RC1 still leaves shard routing and eager-loading choices with the application, and its guide explicitly excludes automatic replica load balancing and cross-database joins from the supported database behavior.