Skip to content

Instantly share code, notes, and snippets.

@MadBomber
Created May 22, 2023 14:41
Show Gist options
  • Save MadBomber/620d8c10f0e6b6a9ad449774c4ee64ee to your computer and use it in GitHub Desktop.
Save MadBomber/620d8c10f0e6b6a9ad449774c4ee64ee to your computer and use it in GitHub Desktop.
Auto-connection switching between read and write database
# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
# When true, the database automatically switches connections
@@auto_switch_connection = true
# returns the name of the read replica if one is defined in database.yml, otherwise returns :primary
def self.reading_db
db_configs = ApplicationRecord.configurations.configs_for(env_name: Rails.env, include_replicas: true)
db_configs.find(&:replica?)&.name&.to_sym || :primary
end
connects_to database: {
writing: :primary,
reading: ApplicationRecord.reading_db
}
before_save :switch_to_primary_database
before_destroy :switch_to_primary_database
after_commit :switch_to_read_replica
after_rollback :switch_to_read_replica
# These cannot be private if we want to call them
# from inside a sidekiq worker or rails console.
# private
def switch_to_primary_database(switch_connection=nil)
ApplicationRecord.switch_to_primary_database(switch_connection)
end
def self.switch_to_primary_database(switch_connection=nil)
unless switch_connection.nil?
self.auto_switch_connection = switch_connection
ApplicationRecord.switch_to :writing
return
end
ApplicationRecord.switch_to(:writing) if ApplicationRecord.auto_switch_connection?
end
def switch_to_read_replica(switch_connection=nil)
ApplicationRecord.switch_to_read_replica(switch_connection)
end
def self.switch_to_read_replica(switch_connection=nil)
unless switch_connection.nil?
self.auto_switch_connection = switch_connection
ApplicationRecord.switch_to :reading
return
end
ApplicationRecord.switch_to(:reading) if ApplicationRecord.auto_switch_connection?
end
def self.switch_to(role)
unless [:reading, :writing].include? role
raise BadParameterError, "database rple is incorrect: #{role}"
end
ActiveRecord::Base.default_role = role
end
def self.auto_switch_connection?
!!@@auto_switch_connection
end
def self.auto_switch_connection=(a_boolean)
@@auto_switch_connection = a_boolean
end
def self.db_role
ActiveRecord::Base.default_role
end
def self.db_name
ActiveRecord::Base.connection.current_database
end
end
@MadBomber
Copy link
Author

The only mod to config/application.rb is to set the initial default_role to :reading.

  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 6.1

    config.active_record.default_role = :reading

    # ...
  end

@MadBomber
Copy link
Author

I feel like I'm treating the connection as if there is only one and not a pool of concurrent connections. For example the auto_switch_connection is defined as a class variable. As such it impacts all sub-classes of the ApplicationRecord.

The cakk backs are really instance related. Those instances being of sub-classes of ApplicationRecord.

So maybe `auto_switch_connection should be an instance variable (or a class variable on the sub-class) so that querys to one table can be on the reading role while queries to another table could be on the writing role.

@Nealsoni00
Copy link

When implementing this and adding the read only line in my application:
config.active_record.default_role = :reading
I am unable to run migrations. Any idea how to get migrations to run with a writer role?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment