March 08, 2019

Cấu hình và chuyển đổi kết nối giữa nhiều cơ sở dữ liệu trong Rails

rails rails-6

Trong thời kỳ các dịch vụ cloud phát triển mạnh mẽ, việc xây dựng cơ sở dữ liệu cho phép chia sẽ dữ liệu giữa nhiều ứng dụng khác nhau không có gì mới. Ví dụ như ứng dụng quản lý dân cư, quản lý trường học, bệnh viện,.. trong khu chung cư dùng chung database chứa users, người dùng trong users điều có thể đăng nhập, truy cập vào các ứng dụng trên.

Bài toán ?

Giả sử ta bắt đầu xây dựng một ứng dựng quản lý dân cư trong khu chung cư, có liên kết với một cơ sở dữ liệu chính primary và hai cơ sở dữ liệu secondarytertiary được chia sẻ với các ứng dụng khác:

Dữ liệu addresses sẽ được đọc từ database tertiary và được xử lý cập nhật tới database primary, thông tin người dùng users được hoàn toàn xử lý trên database secondary.

Để giải quyết bài toán trên ta sử dụng phương thức #establish_connection chỉ định kết nối model User ánh xạ tới bảng users của database secondary, phương thức mới cứng connects_to của rails 6.0.0 trong việc chuyển đổi kết nối đọc ghi model Address giữa database primarytertiary.

Cấu hình file database.yml ?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
default: &default
  adapter: mysql2
  encoding: utf8mb4
  host: db
  username: root
  password: 123456
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

development:
  primary:
    <<: *default
    database: app_primary_development
  secondary:
    <<: *default
    database: app_secondary_development
    migrations_paths: "db/secondary_migrate"
  tertiary:
    <<: *default
    database: app_tertiary_development
    replica: true

Do hai database primarysecondary là độc lập nên cần tách biệt với nhau khi chạy task migrate, migrations_paths sẽ chỉ định đường dẫn tới thư mục chứa file migrate, nếu không khai báo sẽ mặc định ở db/migrate, và khi chạy rails generate migration thêm lựa chọn --database=DATABASE để khai báo migrate tương ứng với database nào.

1
2
3
4
5
6
7
8
9
# Tạo file migrate cho database primary
rails g  migration CreateAddresses --database=primary
# Chạy task migrate cho database primary
rake db:migrate:primary

# Tạo file migrate cho database secondary
rails g  migration CreateUsers --database=secondary
# Chạy task migrate cho database secondary
rake db:migrate:secondary

Do database tertiary chúng ta chỉ dùng để đọc dữ liệu nên không cần sinh ra migration cho nó, với option replica: true các task migrate sẽ không ảnh hưởng tới chúng.

Cấu hình trong model ?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# application_record.rb
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
end

# address.rb
class Address < ApplicationRecord
  connects_to database: { writing: :primary, reading: :tertiary }

  has_one :user, :class_name => User.name, :foreign_key => :uid, :primary_key => :uid
end

# secondary_base.rb
class SecondaryBase < ActiveRecord::Base
  self.abstract_class = true
  establish_connection :secondary
end

# user.rb
class User < SecondaryBase
  belongs_to :address, :class_name => Address.name, :foreign_key => :uid, :primary_key => :uid
end

Model User lúc này được ánh xạ đến bảng users trong secondaryAddress sẽ được ánh xạ tới addresses trong primary, và có khả năng chuyển đối tới tertiary qua role: reading. Và chúng ta có thể sử dụng các association như bình thường.

Time to test !!!!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Dữ liệu từ primary database
Address.first
#=> Address id: 1, uid: "u001", address_name: "address in primary", created_at: "2019-03-01 00:00:00", updated_at: "2019-03-01 00:00:00"

# Với role: :reading sẽ chuyển đổi kết nối tới tertiary database
Address.connected_to(role: :reading) do
  Address.first
end
#=> Address id: 1, uid: "u001", address_name: "address in tertiary", created_at: "2019-03-01 00:00:00", updated_at: "2019-03-01 00:00:00"

# Sẽ sinh ra exception khi cố gắng ghi dữ liệu vào tertiary database
Address.connected_to(role: :reading) do
  Address.create!
end
#=> ActiveRecord::ReadOnlyError

Chúng ta cũng có thể đọc dữ liệu trong database tertiary và cập nhật vào database primary

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Dữ liệu từ primary database
Address.find(1)
#=> Address id: 1, uid: "u001", address_name: "address in primary", created_at: "2019-03-01 00:00:00", updated_at: "2019-03-01 00:00:00"

# Dữ liệu từ tertiary database
Address.connected_to(role: :reading) do
  @address = Address.find(1)
  #=> Address id: 1, uid: "u001", address_name: "address in tertiary", created_at: "2019-03-01 00:00:00", updated_at: "2019-03-01 00:00:00"
end
@address.address_name = @address.address_name + " to primary"
# Dữ liệu được cập nhật tới primary database
@address.save!
# Dữ liệu từ primary database
Address.find(1)
#=> Address id: 1, uid: "u001", address_name: "address in tertiary to primary", created_at: "2019-03-01 00:00:00", updated_at: "2019-03-12 08:51:02"

Các liên kêt association giữa các model vẫn được bảo toàn.

1
2
3
4
5
6
7
8
9
@user = User.find_by(uid: 'u001')
#=> User id: 1, uid: "u001", username: "Tokuda", created_at: "2019-03-01 00:00:00", updated_at: "2019-03-01 00:00:00"
@user.address
#=> Address id: 1, uid: "u001", address_name: "address in primary", created_at: "2019-03-01 00:00:00", updated_at: "2019-03-12 08:55:05"
Address.connected_to(role: :reading) do
  # Khi gọi @user.address ở trên bị cache, thêm reload vào để thực hiện query lại.
  @user.address.reload
end
#=> Address id: 1, uid: "u001", address_name: "address in tertiary", created_at: "2019-03-01 00:00:00", updated_at: "2019-03-01 00:00:00"
Demo time !!!!
  • Tải source code của rails 6.0.0.beta2 có sẵn docker-compose.yml, Dockerfile, dump file của tertiary database:
1
2
3
4
# Clone with SSH
root@localhost:~$ git clone git@github.com:saiury92/multiple-db-on-rails.git app/
# Clone with HTTPS
root@localhost:~$ git clone https://github.com/saiury92/multiple-db-on-rails.git app/
  • Tạo images nếu images chưa tồn tại, đồng thời tạo và khởi động các containers:
1
2
root@localhost:~$ cd app/
root@localhost:~/app$ docker-compose up
  • Truy cập vào web_app container và chạy task migrate để tạo database, sinh dữ liệu mẫu:
1
root@localhost:~/app$ docker exec -it app_web rails db:setup
  • Có thể truy cập vào rails console để chạy command ở phần test trên:
1
root@localhost:~/app$ docker exec -it app_web rails c
  • Chạy rails server và truy cập http://localhost:3000 để vào web app:
1
root@localhost:~/app$ docker exec -it app_web rails s

Comments