Rails 6 adds ability to block writes to a database


Rails 6 introduced a new functionality to configure and manage multiple databases in our application.

The best examples for multiple database configuration are:

  • Primary replica configuration, where all writes are executed on the primary and all reads on replica database.
  • Application has multiple databases to deal with. For e.g, we store orders of user in one database and their archived orders in different database.

To set multiple databases in our Rails application we need to set our database.yml as below

development:
  primary:
    <<: *default
    database: primary_database

  primary_replica:
    <<: *default
    database: primary_database_replica
    replica: true

  secondary:
    <<: *default
    database: secondary_database

Here, we have created two databases primary and secondary. We have also added one replica to the primary database.

The reads for primary database will be performed from primary_database_replica and writes from primary_database. But, for secondary both reads and writes will be performed on secondary_database.

There can be cases when we want to block the writes on the database:

  • While performing a read operation.
  • While executing a code block or transaction.

Examples for this use case can be:

  • Converting a database from a single DB to a primary/replica setup. As shown in the above example, if we try to split secondary database into secondary_database (for write operation) and secondary_database_replica (for read operation).
  • Switching between databases i.e., from primary to replica or replica to primary and want to make sure that writes are not performed on replica.

To tackle the above issue, Rails 6 added ability to block writes to a database, even if the database user has permission to write (the database is a primary and not a replica).

ActiveRecord::Base.connected_to(role: :writing) do
  User.connection.while_preventing_writes do
    # will raise because we're blocking writes
    User.create!
  end
end

ActiveRecord::Base.connected_to(role: :reading) do
  User.connection.while_preventing_writes do
    # will not raise because we're not writing
    User.first
  end
end

As per the above example, database user can perform write operation (role: :writing). But, if we call while_preventing_writes and try to create a User using User.create!, it will raise an exception.

If we are in read role and call while_preventing_writes, no error is raised while running User.first, since we are not writing.

Rails internally identifies whether a query is a read or write. So, when SQL queries like INSERT, UPDATE are executed and writes are blocked the ActiveRecord::StatementInvalid error will be raised.