Rails for Multi database defines reading_request? in resolver
source link: https://blog.saeloun.com/2022/06/22/rails-allows-overriding-reading-request
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Rails for Multi database defines reading_request? in resolver
Jun 22, 2022 , by Murtaza Bagwala
3 minute read
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.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK