Настройка подключений к нескольким базам данных в Rails.
Решил сделать перевод на русский язык статьи Setting up multiple databases in Rails: the definitive guide
(By Roberto Ostinelli / December 2, 2015).
Переводчик я непрофессиональный, поэтому в некоторых местах буду позволять себе вольности, но общий смысл будет сохранен.
Статья написана для Rails 4, но работает и в Rails 5 тоже, по крайней мере, на момент перевода статьи.
———————
Может быть множество разных причин, зачем вам может понадобиться подключение к нескольким базам данных в вашем Ruby in Rails приложении. В моём случае необходимо было хранить большое количество данных, отражающих поведение пользователя: клики, посещаемые страницы, изменения истории и т. д.
Такие типы баз данных обычно не критичны для основной цели, а разрастаются намного быстрее (и до гораздо бОльших размеров), чем другие БД.
Требования к ним достаточно разнообразны: к примеру, они требуют больше пространства, более терпимы к аппаратным и программным сбоям, а также у них в приоритете идёт запись в базу, а не чтение. По этим причинам иногда выгодно отделить такие БД от основной базы вашего приложения. Зачастую для таких задач используются нереляционные СУБД, что уже выходит за пределы темы, по которой писалась данная статья.
Я нашел и прочитал множество различных решений, но я так и не смог найти такого, которое бы полностью выполняло следующие условия:
- Должны быть различные и изолированные друг от друга миграции и схемы для каждой базы данных
- Должны использоваться генераторы Rails для создания новых миграций для каждой БД, независимо друг от друга
- Должны присутствовать специфические для баз данных задания rake для основных операций над БД (например такие же, как и для основной БД)
- Должна быть интеграция с заданиями
spec
RSpec’а. - Должно работать с Database Cleaner
- Должно работать на Heroku
Это мой взгляд на то, как удовлетворить все вышеуказанные требования и получить полностью рабочее Rails-приложение с подключением к нескольким БД.
Создаём необходимые файлы
Для нужд этой инструкции мы собираемся установить подключение ко второй БД. Назовём её Stats. Чтобы это сделать, мы продублируем файл подключения к основной базе данных и не будем забывать о конвенциях.
Для начала создадим файл config/database_stats.yml
и заполним его так же, как и основной config-файл. У вас он будет выглядеть примерно вот так:
development: adapter: postgresql encoding: utf8 host: localhost pool: 10 database: myapp_stats_development username: postgres password: test: adapter: postgresql encoding: utf8 host: localhost pool: 10 database: myapp_stats_test username: postgres password: production: adapter: postgresql encoding: utf8 url: <%= ENV["DATABASE_STATS_URL"] %> pool: <%= ENV["DB_POOL"] || 5 %>
Теперь мы создадим директорию, в которую будет содержаться схема и все миграции базы данных Stats, так что у неё будут свои собственные файлы, изолированные от основной БД. Мы, просто-напросто, продублируем директорию основной БД Rails db
.Отмечу, что базам данных я дал специфические имена, пытаясь настолько сильно, насколько возможно следовать принятым конвенциям наименований в Rails. Также URL для базы данных в окружении production я установил в переменную DATABASE_STATS_URL
. Это позволит нам легко установить эту переменную на вторичную базу данных при развертывании на Heroku.
Создадим директорию db_stats
в корне Rails-приложения и удостоверимся, что скопировали структуру и файлы внутри директории db
основной базы данных. У вас должно получиться что-то типа такого:
-- db |-- migrate schema.rb seeds.rb -- db_stats |-- migrate schema.rb seeds.rb
Созданные файлы schema.rb
и seeds.rb
, также как и директория migrate
, должны быть пустыми.
Создаём задания Rake
Для работы с базой данных Stats, а также, чтобы иметь возможность её содавать, делать миграции, сохранять схему БД и выполнять другие функции, нам понадобятся собственные Rake задачи. Эти задачи предоставят нам тот же самый функционал, который Rails предоставляет нам для основной БД.
Создайте новый файл lib/tasks/db_stats.rake
, и вставьте в него следующее:
task spec: ["stats:db:test:prepare"] namespace :stats do namespace :db do |ns| task :drop do Rake::Task["db:drop"].invoke end task :create do Rake::Task["db:create"].invoke end task :setup do Rake::Task["db:setup"].invoke end task :migrate do Rake::Task["db:migrate"].invoke end task :rollback do Rake::Task["db:rollback"].invoke end task :seed do Rake::Task["db:seed"].invoke end task :version do Rake::Task["db:version"].invoke end namespace :schema do task :load do Rake::Task["db:schema:load"].invoke end task :dump do Rake::Task["db:schema:dump"].invoke end end namespace :test do task :prepare do Rake::Task["db:test:prepare"].invoke end end # append and prepend proper tasks to all the tasks defined here above ns.tasks.each do |task| task.enhance ["stats:set_custom_config"] do Rake::Task["stats:revert_to_original_config"].invoke end end end task :set_custom_config do # save current vars @original_config = { env_schema: ENV['SCHEMA'], config: Rails.application.config.dup } # set config variables for custom database ENV['SCHEMA'] = "db_stats/schema.rb" Rails.application.config.paths['db'] = ["db_stats"] Rails.application.config.paths['db/migrate'] = ["db_stats/migrate"] Rails.application.config.paths['db/seeds'] = ["db_stats/seeds.rb"] Rails.application.config.paths['config/database'] = ["config/database_stats.yml"] end task :revert_to_original_config do # reset config variables to original values ENV['SCHEMA'] = @original_config[:env_schema] Rails.application.config = @original_config[:config] end end
Здесь потребуется немного пояснений: давайте разделим этот файл на несколько главных секций. Сначала мы просто предоставляем «прокси» для стандартных Rails-задач для БД. В только что созданном stats:db
:
task :drop do Rake::Task["db:drop"].invoke end task :create do Rake::Task["db:create"].invoke end task :setup do Rake::Task["db:setup"].invoke end task :migrate do Rake::Task["db:migrate"].invoke end [...]
Далее мы зациклим все эти задачи и убедимся, что задача stats:set_custom_config
запускается до, а задача stats:revert_to_original_config
запускается после каждой из этих «прокси»-задач
# append and prepend proper tasks to all tasks defined in stats:db namespace ns.tasks.each do |task| task.enhance ["stats:set_custom_config"] do Rake::Task["stats:revert_to_original_config"].invoke end end
Нам приходится делать это потому, что Rails, к сожалению, не очень хорошо поддерживает соединение с несколькими БД, поэтому приходится использовать такие вот «хаки», чтобы всё заработало. По этой причине нам нужно установить значение среды и конфигурационных переменных на собственные значения, которые будут совпадать с нашей базой данных Stats, до того, как мы запустим «прокси-задания», а потом убедимся, что оригинальные значения вернулись назад как только задание было запущено. Следующие два задания делают именно это:
task :set_custom_config do # save current vars @original_config = { env_schema: ENV['SCHEMA'], config: Rails.application.config.dup } # set config variables for custom database ENV['SCHEMA'] = "db_stats/schema.rb" Rails.application.config.paths['db'] = ["db_stats"] Rails.application.config.paths['db/migrate'] = ["db_stats/migrate"] Rails.application.config.paths['db/seeds'] = ["db_stats/seeds.rb"] Rails.application.config.paths['config/database'] = ["config/database_stats.yml"] end task :revert_to_original_config do # reset config variables to original values ENV['SCHEMA'] = @original_config[:env_schema] Rails.application.config = @original_config[:config] end
Заметьте, что в строках 9-13 мы устанавливаем в качестве значений файлы и директории, которые мы создавали в предыдущих шагах.
И наконец, если вы используете RSpec, вы можете добавить одну зависимость в задание spec
, чтобы убедиться, что база данных Stats автоматически подготавливается (prepare
), когда запускается тест.
task spec: ["stats:db:test:prepare"]
После того, как всё это настроено, мы можем создать базу данных Stats и запустить её первую миграцию:
$ rake stats:db:create $ rake stats:db:migrate
Эти команды сгенерируют файл-схему db_stats/schema.rb
базы данных Stats
Добавляем собственный генератор
К сожалению, мы не можем просто взять и использовать генератор Rails ActiveRecord::Generators::MigrationGenerator
, потому что в нем захардкожена родительская директория миграции (обратите внимание на путь db/migrate
, который захардкожен на четвертой строке ниже):
def create_migration_file set_local_assigns! validate_file_name! migration_template @migration_template, "db/migrate/#{file_name}.rb" end
Поэтому нам понадобится собственный генератор, чтобы создавать миграции для БД Stats. Как бы то ни было, мы всё равно можем наследоваться от него (генератора Rails) и переопределить эту конкретную функцию. создадим следующий генератор в lib/generators/stats_migration_generator.rb
:
require 'rails/generators/active_record/migration/migration_generator' class StatsMigrationGenerator < ActiveRecord::Generators::MigrationGenerator source_root File.join(File.dirname(ActiveRecord::Generators::MigrationGenerator.instance_method(:create_migration_file).source_location.first), "templates") def create_migration_file set_local_assigns! validate_file_name! migration_template @migration_template, "db_stats/migrate/#{file_name}.rb" end end
В строке 9 мы устанавливаем путь к директории с файлами миграций для нашей БД Stats. Также в строке 4 мы инициализируем каталог с шаблонами и указываем на оригинальный, используемый генератором, с которого мы наследуемся.
После выполнения всех этих манипуляций мы можем генерировать миграции для нашей базы данных Stats следующим образом:
$ rails g stats_migration create_clicks
Вы увидите, что файл миграции создался в директории с миграциями нащей БД Stats, db_stats/migrate
. Вы можете отредактировать этот файл и затем запустить миграцию при помощи Rake-задания, которое мы создали на предыдущих шагах, так же, как и обычно для вашей основной БД:
$ rake stats:db:migrate
Собираем вместе соединения и модели
Итак, мы почти закончили. Добавляем новый файл инициализации config/initializers/db_stats.rb
и вставляем туда следующее:
# save stats database settings in global var DB_STATS = YAML::load(ERB.new(File.read(Rails.root.join("config","database_stats.yml"))).result)[Rails.env]
Заметьте, что мы ссылаемся на конфигурационный файл БД Stats, который мы создали в первом шаге. Сделав это, мы проинициализируем глобальную переменную DB_STATS, которая содержит конфигурацию текущего окружения базы данных Stats.
Наконец, мы можем создать подключить наши модели к этой конфигурации. К примеру, пусть у нас будет модель Click, которая соответствует миграции, которую мы делали выше в предыдущем шаге. Всё, что вам необходимо сделать — это добавить одну дополнительную строку, которая укажет, какое соединение использовать:
class Click < ActiveRecord::Base establish_connection DB_STATS end
Вот и всё. Ваша модель теперь использует базу данных Stats.
Если у вас несколько моделей, которые нужно присоединить к вашей БД, вам понадобится выполнить ещё один дополнительный шаг. Если бы у вас была ещё одна модель, устанавливающая свое собственное соединение с вашей БД, у ней был бы свой собственный пул соединений и мог бы возникнуть риск уткнуться в предел доступных соединений с базой данных Stats. Поэтому, если у вас есть несколько моделей, рекомендуется наследоваться с одной модели, и тогда все дочерние модели будут использовать один пул соединений.
Чтобы так сделать, создайте базовую модель, которая подключается к БД Stats:
class StatsBase < ActiveRecord::Base establish_connection DB_STATS self.abstract_class = true end
Теперь вы можете унаследовать от неё все остальные ваши модели:
class Click < StatsBase end class View < StatsBase end
Heroku
Как вы могли догадаться, последним шагом, который вам понадобится, чтобы сделать эту работу на Heroku — это установить переменную окружения DATABASE_STATS_URL
на базу данных, которую вы собираетесь использовать в качестве Stats. Например, если вы создали вторую БД с названием HEROKU_POSTGRESQL_TEAL_URL
, то всё, что вам нужно — установить значение переменной, используя Heroku toolbelt:
$ heroku config:set DATABASE_STATS_URL=postgres://gsdfjrthjsnaew:gry6OJF6drDjththjkSDngldsf@ec2-116-22-114-221.compute-1.amazonaws.com:5432/hmsrthj24dfgks
И всё.
Bonus: DatabaseCleaner
Если вы используете гем DatabaseCleaner, вы также можете настроить его для очистки моделей, которые используют БД Stats. К примеру, ваш spec/rails_helper.rb
будет выглядеть примерно вот так:
ENV["RAILS_ENV"] ||= 'test' require 'spec_helper' require File.expand_path("../../config/environment", __FILE__) require 'rspec/rails' Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } ActiveRecord::Migration.maintain_test_schema! RSpec.configure do |config| config.use_transactional_fixtures = false config.infer_spec_type_from_file_location! config.before(:suite) do DatabaseCleaner.clean_with(:truncation) DatabaseCleaner[:active_record, { model: Click }].clean_with(:truncation) end config.before(:each) do |example| unit_test = ![:feature, :request].include?(example.metadata[:type]) strategy = unit_test ? :transaction : :truncation DatabaseCleaner.strategy = strategy DatabaseCleaner[:active_record, { model: Click }].strategy = strategy DatabaseCleaner.start DatabaseCleaner[:active_record, { model: Click }].start end config.after(:each) do DatabaseCleaner.clean DatabaseCleaner[:active_record, { model: Click }].clean end end
Согласно README DatabaseCleaner’а, есть возможность установить опцию connection
, вместо model
. К сожалению, мои попытки сделать это были безуспешны.