Làm việc với nhiều database trên cùng một dự án Rails
Bài đăng này đã không được cập nhật trong 6 năm
Từ trước tới nay, trong một dự án Rails, mình vẫn thường quen với việc chỉ quản lý và làm việc với duy nhất một database mà thôi. Khi đó thì mọi cấu hình cho db, chúng ta để trong file config/database.yml
; và tất cả thông tin cũng như việc migrate các bảng trong db sẽ được đặt trong thư mục db/
có cấu trúc như sau:
Khi cần thay đổi gì trong cấu trúc của db, ta lại sinh ra một file trong thư mục db/migrate/
rồi chạy bundle exec rake db:migrate
là xong. Chắc hẳn chúng ta đã quá quen thuộc với việc này.
Trong bài viết này mình xin trình bày một bài toán mới: dự án Rails phải làm việc đồng thời với nhiều database cùng lúc. Với một dự án lớn, có độ phức tạp cao hơn thì ta sẽ hay gặp phải những tình huống như thế.
Và tất nhiên, cấu hình và việc migrate cho các db này cũng hoàn toàn độc lập với nhau. Nhưng khi làm việc thì bạn vẫn có thể kết hợp được dữ liệu từ các db này để đưa ra kết quả.
Nghe qua thì có vẻ nó phức tạp, nhưng khi bắt tay vào làm, bạn sẽ thấy nó cũng không phức tạp lắm đâu. Bắt đầu nhé.
Bài toán.
Ví dụ của mình là chỉ làm việc với 2 db mà thôi:
-
db_main
-
db_second
Để đơn giản thì cấu hình và thông tin của db db_main
ta sẽ chứa trong file config/database.yml
và thư mục db/
sẵn có của Rails.
Cấu hình.
File config/database.yml
Mình chỉ ví dụ với môi trường development, các môi trường khác cũng hoàn toàn tương tự nhé.
development:
adapter: mysql2
encoding: utf8mb4
collation: utf8mb4_unicode_ci
pool: 5
database: db_main
host: host_main
username: user_main
password: password_main
Tạo file config/database_second.yml
để cấu hình cho db db_second
:
development:
adapter: mysql2
encoding: utf8mb4
collation: utf8mb4_unicode_ci
pool: 5
database: db_second
host: host_second
username: user_second
password: password_second
Tạo thêm thư mục db_second/
mới để chứa thông tin của db db_second
.
Thêm custom generator.
Ruby có ActiveRecord::Generators::MigrationGenerator
để định nghĩa việc migration db. Tuy nhiên, mặc định khi chạy lệnh rails generate migration create_...
thì nó sẽ tạo ra file db trong thư mục db/migrate/
cho db chính.
Để tiến hành migration file db cho riêng db_second
, ta sẽ phải tạo mới để khai báo việc đó.
File lib/generators/second_migration_generator.rb
require 'rails/generators/active_record/migration/migration_generator'
class SecondMigrationGenerator < ActiveRecord::Generators::MigrationGenerator
migration_file = File.dirname(
ActiveRecord::Generators::MigrationGenerator
.instance_method(:create_migration_file)
.source_location.first
)
source_root File.join(migration_file, "templates")
def create_migration_file
set_local_assigns!
validate_file_name!
migration_template @migration_template, "db_second/migrate/#{file_name}.rb"
end
end
Với khai báo trên, ta sẽ chạy câu lệnh với second_migration
để generate ra file db như sau:
framgias-Mac-mini:test vo.tai.tri$ rails g second_migration create_tribeo
create db_fueling/migrate/20180315082949_create_tribeo.rb
Sau khi chạy câu lệnh này thì file mới sẽ được sinh ra trong thư mục db_second/migrate/
Thêm custom rake task.
Đến đây ta đã định nghĩa được vị trí thư mục chứa các file db. Tiếp đến, ta muốn việc tạo, migrate, schema của db db_second
độc lập với db chính nên sẽ phải tạo ra file rake task để khai báo cho việc đó.
Tạo mới file lib/tasks/db_second.rake
như sau:
namespace :second do
namespace :db do |ns|
%i(drop create setup migrate rollback seed version).each do |task_name|
task task_name do
Rake::Task["db:#{task_name}"].invoke
end
end
namespace :schema do
%i(load dump).each do |task_name|
task task_name do
Rake::Task["db:schema:#{task_name}"].invoke
end
end
end
# append and prepend proper tasks to all the tasks defined here above
ns.tasks.each do |task|
task.enhance ['second:set_custom_config'] do
Rake::Task['second: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_second/schema.rb'
Rails.application.config.paths['db'] = ['db_second']
Rails.application.config.paths['db/migrate'] = ['db_second/migrate']
Rails.application.config.paths['db/seeds'] = ['db_second/seeds.rb']
Rails.application.config.paths['config/database'] = ['config/database_second.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
Với khai báo namespace :second
và :db
cùng với các task :create
, :drop
, :migrate
... như trên, ta sẽ có thể thao tác đến db_second
bằng lệnh với second:db:
như sau:
bundle exec rake second:db:create
Đến bước này thì ta đã có thể quản lý được thông tin cũng như tạo bảng mới, thay đổi cấu trúc bảng,... của db_second
Và cuối cùng là bước connect dự án đến db_second
để lấy dữ liệu và làm việc.
Connect db.
Tạo file config/initializers/db_second.rb
như sau:
config=YAML::load(ERB.new(File.read(Rails.root.join('config','database_second.yml'))).result)
DB_SECOND = config[Rails.env]
File này định nghĩa biến DB_SECOND
lưu thông tin của db_second
cho những model nào cần connect đến
Tạo model app/models/db_second/connection.rb
để connect đến db_second
, model này chỉ là một abstract_class
mà thôi, code như sau:
class DbSecond::Connection < ActiveRecord::Base
self.abstract_class = true
establish_connection DB_SECOND
end
Định nghĩa model cho db_second
, ta chỉ cần kế thừa nó từ DbSecond::Connection
đã định nghĩa bên trên và chỉ ra table_name
cho nó như sau:
class DbSecond::Tribeo < DbSecond::Connection
self.table_name = 'tribeo'
...
end
Đến đây thì ta có thể sử dụng model DbSecond::Tribeo
như mọi model khác của dự án Ruby.
[2] pry(main)> DbSecond::Tribeo
=> DbSecond::Tribeo(id: integer, name: string, created_at: datetime, updated_at: datetime)
[3] pry(main)> DbSecond::Tribeo.first
DbSecond::Tribeo Load (4.4ms) SELECT `tribeo`.* FROM `tribeo` ORDER BY `tribeo`.`id` ASC LIMIT 1
=> #<DbSecond::Tribeo:0x007fc8710ff6b8
id: 1,
name: "champion",
created_at: Fri, 09 Mar 2018 13:09:18 JST +09:00,
updated_at: Fri, 09 Mar 2018 16:09:19 JST +09:00>
Tham khảo.
Hi vọng bài viết sẽ có ích với bạn.
Cám ơn vì đã đọc bài viết!
All rights reserved