Rails 6.1 adds support for role switching and sharding in database


Rails 6.1 adds the ability to switch a role or shard for an application with multiple databases. This means it is possible to switch connections for one database instead of all databases globally.

To use this feature, we need to set the below config in our application.

config.active_record.legacy_connection_handling = false

Let’s say we have two databases, primary and vehicles. And we have shards and replica configured for each of them in database.yml as below:

production:
  primary:
    database: primary_database
    adapter: mysql
  primary_replica:
    database: primary_database
    adapter: mysql
    replica: true
  primary_shard_one:
    database: primary_shard_one
    adapter: mysql
  primary_shard_one_replica:
    database: primary_shard_one
    adapter: mysql
    replica: true

  vehicles:
    database: vehicles_database
    adapter: mysql
  vehicles_replica:
    database: vehicles_database
    adapter: mysql
    replica: true
  vehicles_shard_one:
    database: vehicles_shard_one
    adapter: mysql
  vehicles_shard_one_replica:
    database: vehicles_shard_one
    adapter: mysql
    replica: true

We have two models User and Car. User is stored in the primary database and Car in the vehicles database. The corresponding model and abstract classes will look as below:

# primary database
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

# User class
class User < ApplicationRecord
end

# vehicles database
class VehiclesRecord < ApplicationRecord
  self.abstract_class = true

  connects_to shards: {
    default: { writing: :vehicles, reading: :vehicles_replica },
    shard_one: { writing: :vehicles_shard_one, reading: :vehicles_shard_one_replica }
  }
end

# Car class
class Car < VehiclesRecord
end

When legacy_connection_handling is set to false, any abstract connection class will be able to switch connections without affecting other connections.

Before

ActiveRecord::Base.connected_to(role: :reading) do
  User.first # Reads from primary replica
  Car.first  # Reads from vehicles replica

  VehiclesRecord.connected_to(role: :reading, shard: :shard_one) do
    User.first  # Reads from shard_one_replica of primary database
    Car.first   # Reads from shard_one_replica of vehicles database
  end

  ApplicationRecord.connected_to(role: :reading, shard: :shard_one) do
    User.first  # Reads from shard_one_replica of primary database
    Car.first   # Reads from shard_one_replica of vehicles database
  end
end

In both the above VehiclesRecord.connected_to and ApplicationRecord.connected_to blocks the connection was changed to shard_one of the corresponding database.

After

ActiveRecord::Base.connected_to(role: :reading) do
  User.first # Reads from primary replica
  Car.first  # Reads from vehicles replica

  VehiclesRecord.connected_to(role: :reading, shard: :shard_one) do
    User.first  # Reads from primary replica
    Car.first   # Reads from shard_one_replica of vehicles database
  end

  ApplicationRecord.connected_to(role: :reading, shard: :shard_one) do
    User.first  # Reads from primary_shard_one_replica
    Car.first   # Reads from vehicles_primary
  end
end

As seen above, when we connect VehiclesRecord to shard_one only Car queries are executed on shard_one whereas User queries are executed on primary database.

Summary

The addition of this granular switching is useful in cases like:

  • Suppose replica or shard_one is not configured for primary database. A connection is made to shard_one and User queries are executed under the shard_one block, then an error would be thrown. This is because the connection was changed globally across all databases.
  • If two shards shard_one and shard_two are configured for both databases, user_a exists on shard_one and user_b on shard_two. If VehiclesRecord connection is changed to shard_two and we query for user_a under that block it will return incorrect result.