SwitchPoint
Switching database connection between readonly one and writable one.
Maintenance notice
switch_point won't support upcoming ActiveRecord v6.1 or later. Developers should use the builtin multiple database feature introduced in ActiveRecord v6.0. https://guides.rubyonrails.org/active_record_multiple_databases.html Thus the supported ActiveRecord version is v3.2, v4.0, v4.1, v4.2, v5.0, v5.1, and v5.2.
switch_point won't accept any new features. Bug fixes might be accepted. If you'd like to add a new feature (and/or support ActiveRecord >= v6.1), feel free to fork switch_point gem.
Migration from switch_point to ActiveRecord multiple database feature
- Upgrade your activerecord gem to v6.0
- ActiveRecord v6.0 is the only series which supports both builtin multiple database feature and switch_point.
- Change your application to use ActiveRecord multiple database feature
- If you'd like to keep the number of connections during this step, it would require some tricks.
- Remove switch_point gem from your Gemfile
- Upgrade your activerecord gem to v6.1 or later
Installation
Add this line to your application's Gemfile:
gem 'switch_point'
And then execute:
$ bundle
Or install it yourself as:
$ gem install switch_point
Usage
Suppose you have 4 databases: db-blog-master, db-blog-slave, db-comment-master and db-comment-slave. Article model and Category model are stored in db-blog-{master,slave} and Comment model is stored in db-comment-{master,slave}.
Configuration
In database.yml:
production_blog_master:
adapter: mysql2
username: blog_writable
host: db-blog-master
production_blog_slave:
adapter: mysql2
username: blog_readonly
host: db-blog-slave
production_comment_master:
...
In initializer:
SwitchPoint.configure do |config|
config.define_switch_point :blog,
readonly: :"#{Rails.env}_blog_slave",
writable: :"#{Rails.env}_blog_master"
config.define_switch_point :comment,
readonly: :"#{Rails.env}_comment_slave",
writable: :"#{Rails.env}_comment_master"
end
In models:
class Article < ActiveRecord::Base
use_switch_point :blog
end
class Category < ActiveRecord::Base
use_switch_point :blog
end
class Comment < ActiveRecord::Base
use_switch_point :comment
end
Switching connections
Article.with_readonly { Article.first } # Read from db-blog-slave
Category.with_readonly { Category.first } # Also read from db-blog-slave
Comment.with_readonly { Comment.first } # Read from db-comment-slave
Article.with_readonly do
article = Article.first # Read from db-blog-slave
article.title = 'new title'
Article.with_writable do
article.save! # Write to db-blog-master
article.reload # Read from db-blog-master
Category.first # Read from db-blog-master
end
end
Note that Article and Category shares their connections.
Query cache
Model.cache
and Model.uncached
enables/disables query cache for both
readonly connection and writable connection.
switch_point also provide a rack middleware SwitchPoint::QueryCache
similar
to ActiveRecord::QueryCache
. It enables query cache for all models using
switch_point.
# Replace ActiveRecord::QueryCache with SwitchPoint::QueryCache
config.middleware.swap ActiveRecord::QueryCache, SwitchPoint::QueryCache
# Enable query cache for :nanika1 only.
config.middleware.swap ActiveRecord::QueryCache, SwitchPoint::QueryCache, [:nanika1]
Notes
auto_writable
auto_writable
is disabled by default.
When auto_writable
is enabled, destructive queries is sent to writable connection even in readonly mode.
But it does NOT work well on transactions.
Suppose after_save
callback is set to User model. When User.create
is called, it proceeds as follows.
- BEGIN TRANSACTION is sent to READONLY connection.
- switch_point switches the connection to WRITABLE.
- INSERT statement is sent to WRITABLE connection.
- switch_point reset the connection to READONLY.
- after_save callback is called.
- At this point, the connection is READONLY and in a transaction.
- COMMIT TRANSACTION is sent to READONLY connection.
connection-related methods of model
Model has several connection-related methods: connection_handler
, connection_pool
, connected?
and so on.
Since only connection
method is monkey-patched, other connection-related methods doesn't work properly.
If you'd like to use those methods, send it to Model.switch_point_proxy.model_for_connection
.
Internals
There's a proxy which holds two connections: readonly one and writable one. A proxy has a thread-local state indicating the current mode: readonly or writable.
Each ActiveRecord model refers to a proxy.
ActiveRecord::Base.connection
is hooked and delegated to the referred proxy.
When the writable connection is requested to execute destructive query, the readonly connection clears its query cache.
Special case: ActiveRecord::Base.connection
Basically, each connection managed by a proxy isn't shared between proxies. But there's one exception: ActiveRecord::Base.
If :writable
key is omitted (e.g., Nanika1 model in spec/models), it uses ActiveRecord::Base.connection
as writable one.
When ActiveRecord::Base.connection
is requested to execute destructive query, all readonly connections managed by a proxy which uses ActiveRecord::Base.connection
as a writable connection clear query cache.
Contributing
- Fork it ( https://github.com/eagletmt/switch_point/fork )
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create a new Pull Request