Advanced multi-db techniques in Rails 6
Rails 6 is just around the corner. The release candidate two was recently released. This post is part of the Road to Rails 6 series which will prepare you for Rails 6.
Check the part one of this post where we saw how to setup an app with multiple databases in Rails 6 and deploy it to Heroku.
In this post, we will not discuss about multiple databases but we will discuss about read and write instances of same database and how to manage them within a Rails application.
I am using the terms read and write instances for read replica and primary/master instance of the database in this article. It means that read operations should be handled by read replica instance and write operations should be handled by the write/primary/master instance and should be synced to the replica instance for subsequent reads.
Read Replicas
We earlier saw that we can connect to the animals
database for both read and write operations.
class AnimalBase < ApplicationRecord
connects_to database: { writing: :animals }
end
We can start using a read replica for read operations from the animals database by passing a reading
role to the connects_to
call above.
class AnimalBase < ApplicationRecord
connects_to database: { writing: :animals, reading: :animals_replica }
end
Thisrole
is different than the database roles that are part of major SQL databases. Thisrole
specifies which kind of operation you want to perform on the database, read or write and it is specific to how Rails implements the read write operations.
We need to specify the animals_replica
database configuration in the database.yml
to make this work.
production:
primary:
adapter: postgresql
animals:
migrations_paths: db/animal_migrate
adapter: postgresql
url: <%= ENV["HEROKU_POSTGRESQL_OLIVE_URL"] %>
animals_replica:
adapter: postgresql
url: <%= ENV["HEROKU_POSTGRESQL_PURPLE_URL"] %>
replica: true
Make sure that you are not using same database instance for bothreading
andwriting
roles. Rails recommends that replica database instance is separate and has a readonly user created. Rails assumes that this is done by you manually and you are just providing the connection information for Rails to use.
We have not added the migrations_paths
configuration for the replica database but we have added replica: true
for it. The replica: true
configuration is important and conveys to Rails that this database configuration should be treated as replica database.
Now that we have specified which database should be used for write operations and which one to use for read operations, Rails will take care of switching between them based on the request type.
Automatic connection switching between read replica and write databases
If our application is getting any request which is modifying the database such as POST, PUT, DELETE, PATCH then that request is automatically sent to the write
database by Rails. If the application is getting any request which is only reading something from the database then it will go to replica. But if we are getting a read request immediately within 2 seconds after a write request then it will go to the primary database. If your application does not warrant showing real time data to the users, then you can get away with live data not getting updated in the read replica immediately but Rails provides a way to make sure that at-least for first two seconds after the write operation, read requests are handled by the write instance. This assumes that your read replica will get updated within 2 seconds with the live data. This setting can be controlled by following configuration.
config.active_record.database_selector = 2.seconds
We can configure it in the environments/production.rb
as per replica lag. The timestamp of last write operation is stored in the application session by Rails and is controlled by config.active_record.database_resolver_context
.
This automatic connection is included in the Rails app as a rack middleware so it is only available for HTTP requests to the application. For background jobs or rake tasks, by default Rails sends all queries to the write instance. Rails also allows to manually switch between read and write databases which we will see next.
Manual switching between write and read databases
Rails provides ActiveRecord::Base.connected_to
method to switch between read and write databases.
Post.connected_to(role: :reading) do
post_ids = Post.all.pluck(:id)
ProcessBulkPostRepliesWorker.perform(post_ids: post_ids)
end
This makes sure that the block uses read replica. It looks up all the connections connected to the reading
role and then performs the queries in the block using that connection. If the connection does not exist, it will make one before executing the block.
We can also pass database
to the connected_to
method.
ActiveRecord::Base.connected_to(database: animals) do
CollectLionsInformationWorker.perform(Lion.all.pluck(:id, :continent, :age))
end
We can also pass the role
under the database
key.
ActiveRecord::Base.connected_to(database: { reading: :animals }) do
CollectLionsInformationWorker.perform(Lion.all.pluck(:id, :continent, :age))
end
This will use the connection to the read replica of the animals
database and use it.
But we can't pass role
and database
both to the connected_to
method. We can pass only one of them. This means that, when we want to switch roles from reading to writing to reading then we are expected to call connected_to
on correct class. We can use any of the model like Post
which is connected to the database for which we want to switch the role. Or we can also use ActiveRecord::Base
and pass the reading
role if we want to connect to the read replica of the primary
database.
When we are passing database
key to the connected_to
method, we are switching the database altogether for eg. from primary
to animals
for the duration of the block. If we don't pass the role for the new database, connection is automatically made with the writing
role.
In this way we can manage connecting to different databases in ad-hoc scripts, background jobs or even for specific requests which can't rely on automatic connection switching provided by Rails.
What is not provided by the multi-db feature in Rails 6?
Features such as sharding, load balancing replicas, joining across multiple databases are not supported out of the box in Rails 6. Some of these features may be added in future.
Want to stay on the Road to Rails 6?
Subscribe to my newsletter and get a weekly email to know more about how to make your app ready for Rails 6.