Rails 6 had added the support for multiple database connections using which we can configure the separate databases for reads and writes.
For example, we have two databases write_database
and read_database
.
/config/database.yml
default: &default
adapter: postgresql
encoding: unicode
host: <%= ENV['PG_HOST'] || 'localhost' %>
pool: 5
username: <%= ENV['PG_USER'] || 'postgres' %>
password: <%= ENV['PG_PASSWORD'] || 'postgres' %>
development:
write_database:
<<: *default
database: <%= "write_database" %>
read_database:
<<: *default
database: <%= "read_database" %>
Now, in order to use the above-defined databases, we need to set up the Active Record model.
app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
connects_to database: { writing: :write_database, reading: :read_database }
end
app/models/product.rb
class Product < ApplicationRecord
validates :name, presence: true
validates :description, presence: true
end
To enable the automatic switching between the databases based on the HTTP Verb, we need to add the below configuration:
config/application.rb
module MultiDBApp
class Application < Rails::Application
config.active_record.database_selector = { delay: 2.seconds }
config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
end
end
Using the above configuration,
Rails looks for the reading_request?
method
defined in the middleware ActiveRecord::Middleware::Middleware::DatabaseSelector
.
Its default implementation is true for GET
and HEAD requests,
which means for POST, PUT, DELETE,
or PATCH request,
the application will automatically write to the write_database
and for read, it will use read_database
.
Note:- According to the
docs
we are supposed to add the above configuration in
initializers(/config/initializers/multi_db.rb)
but this is broken in Rails latest release so as suggested here in the
Rails issue,
we need to add the config in application.rb
Before
Let us call a create_product
(POST) API to create a Product.
POST:- http://localhost:3000/products
Body:- { name: "Detergent", description: "A mixture of surfactants with cleansing properties" }
Response:-
{
"id": 1,
"name": "Detergent",
"description": "A mixture of surfactants with cleansing properties",
"created_at": "2022-06-14T06:25:25.877Z",
"updated_at": "2022-06-14T06:25:25.877Z"
}
So, as expected, the product is created successfully in write_database
.
If we call a get_product
(GET) API,
it says product not found because,
the GET request will be redirected to read_database
,
and that does not have a Product that we created recently.
GET:- http://localhost:3000/products/1
Response:-
{
"error": "product with id 1 not found"
}
So, this is working as expected. Now suppose, if we call a GraphQL API to read the particular product.
GraphQL API:-
{
product(id: 1) {
id
name
description
}
}
Response:-
{
"data": {
"product": {
"id": "1",
"title": "Detergent",
"description": "A mixture of surfactants with cleansing properties"
}
}
}
Whoa!!! We get the Product details back.
Ideally, it should respond with a not_found
error
because it is a read request,
but GraphQL API uses the POST HTTP method
and
according to the default implementation POST requests get redirected to the write_database
.
After
To fix this,
Rails has moved reading_request?
method from the ActiveRecord::Middleware::Middleware::DatabaseSelector
to the ActiveRecord::Middleware::DatabaseSelector::Resolver
class,
so that we can override
and create custom Resolver.
Let us create a custom resolver
and override the reading_request?
method to add the validations for a GraphQL API.
class CustomResolver < ActiveRecord::Middleware::DatabaseSelector::Resolver
def reading_request?(request)
graphql_read = request.post? && request.path == "/graphql" && !request.params[:query]&.include?("mutation")
graphql_read || super
end
end
module MultiDBApp
class Application < Rails::Application
config.load_defaults 7.1
config.active_record.database_selector = { delay: 2.seconds }
config.active_record.database_resolver = CustomResolver
config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
end
end
So, our custom reading_request?
checks if the request is POST,
type is GraphQL and params doesn’t have mutation
then consider it as a read request
and redirect it to the read_database
.
Now, if we call a GraphQL API again to read the particular product.
GraphQL API:-
{
product(id: 1) {
id
name
description
}
}
Response:-
{
"errors": [
{
"message": "Couldn't find Product with 'id'=1"
}
]
}
It responds with a not_found
message,
which is expected because read_database
doesn’t have a Product record.
Note: The enhancement is yet to be released in the official Rails version
Check out the PR for more details.