环境

  • rails: 5.0.1
  • ruby: 2.3.3

以前的方法

在写下这篇日志之前,我在Rails中配置多个数据库的方法比较简单粗暴:

# database.yml
default: &default
  adapter: mysql2
  pool: 5
  timeout: 5000
  encoding: utf8
  username: user
  password: xxx

log:
  database: foo_log
  adapter: mysql2
  pool: 5
  timeout: 5000
  encoding: utf8
  username: user
  password: xxx

development:
  <<: *default
  database: foo_development

test:
  <<: *default
  database: foo_test

production:
  <<: *default
  database: foo

生成model: rails g model LoginLog, 编辑migration文件:

# 20170227102449_create_login_logs.rb
class CreateLoginLog < ActiveRecord::Migration[5.0]

  # 连接log数据库
  def connection
    @connection = ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['log']).connection
  end

  def change
    create_table :login_logs do |t|
      t.references :user, index: true
      t.timestamps
    end
    # 连接默认数据库
    @connection = ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[Rails.env]).connection
  end
end
# login_log.rb
class UserSigninLog < ApplicationRecord
  # 连接log数据库
  establish_connection(ActiveRecord::Base.configurations['log'])
  belongs_to :user
end

这个方法在使用过程中遇到以下几个问题:

  • 不能生成 schema.rb
  • 不能使用 rails db:rollback
  • 不能在不同环境下使用对应的数据库(development/test/production)

考虑到这几个问题会影响到项目后续的需求,不得不寻找别的解决办法。

现在的方法

假设有两个数据库: foo(默认) 和 foo_log

建立配置文件

将配置文件分为: database.ymldatabase_log.yml, 文件格式和默认数据库一样

default: &default
  adapter: mysql2
  pool: 5
  timeout: 5000
  encoding: utf8
  username: user
  password: xxx

development:
  <<: *default
  database: foo_log_development

test:
  <<: *default
  database: foo_log_test

production:
  <<: *default
  database: foo_log

建立对应的db目录

# 在项目根目录执行
mkdir db_log
touch db_log/schema.rb
touch db_log/seeds.rb

目录结构如下:

├── db
│   ├── migrate
│   ├── schema.rb
│   └── seeds.rb
├── db_log
│   ├── migrate
│   ├── schema.rb
│   └── seeds.rb

建立Rake tasks

通过 set_custom_config 设置对应foo_log数据库的tasks,并在最后通过revert_to_original_config还原默认数据库。

# lib/tasks/db_log.rake
namespace :log 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 ["log:set_custom_config"] do
        Rake::Task["log: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_log/schema.rb"
    Rails.application.config.paths['db'] = ["db_log"]
    Rails.application.config.paths['db/migrate'] = ["db_log/migrate"]
    Rails.application.config.paths['db/seeds.rb'] = ["db_log/seeds.rb"]
    Rails.application.config.paths['config/database'] = ["config/database_log.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

测试一下创建数据库和执行migrate,db_log/schema.rb文件会被生成。

rails log:db:create
rails log:db:migrate

自定义迁移文件生成器

# lib/generators/log_migration_generator.rb
require 'rails/generators/actions/create_migration'
require 'rails/generators'
require 'rails/generators/active_record/migration/migration_generator'

class LogMigrationGenerator < 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_log/migrate/#{file_name}.rb"
  end
end

有了这个文件,就可以用如下命令生成迁移文件了

rails log_migration AddSomeColumnsToLoginLog

为Model建立数据库连接

下面要做的和以前的方法一样,不过每个Model都要加,代码也不太好看,所以可以这样做:

# config/initializers/multiple_db.rb
# 如再有数据库要连接,同样可以加到这个文件里
DB_LOG = YAML::load(ERB.new(File.read(Rails.root.join("config","database_log.yml"))).result)[Rails.env]
# app/models/application_log_record.rb
class ApplicationLogRecord < ActiveRecord::Base
  self.abstract_class = true
  establish_connection DB_LOG
end
# app/models/login_log.rb
class LoginLog < ApplicationLogRecord
  belongs_to :user
end

通过以上的方法,每个数据库可以做到真正的独立操作,像我目前在做的项目需要通过配置文件来对数据库对应的相关功能进行开关,而且数据库还要做远程跨机房同步,这个方法就会避免很多麻烦。

同理,如果有更多数据库需要使用,就继续建立更多配置文件和对应的task、generator文件即可。

参考