rubyonrails.org에서 더 보기:

다음은 한국어 번역입니다:

이 파일을 GITHUB에서 읽지 마세요. 가이드는 https://guides.rubyonrails.org에 게시되어 있습니다.

Active Record에서 다중 데이터베이스 사용하기

이 가이드는 Rails 애플리케이션에서 다중 데이터베이스를 사용하는 방법을 다룹니다.

이 가이드를 읽고 나면 다음을 알 수 있습니다:

  • 다중 데이터베이스를 위한 애플리케이션 설정 방법
  • 자동 연결 전환이 작동하는 방식
  • 다중 데이터베이스를 위한 horizontal sharding 사용 방법
  • 지원되는 기능과 아직 진행 중인 기능

애플리케이션의 인기와 사용량이 증가하면서, 새로운 사용자와 그들의 데이터를 지원하기 위해 애플리케이션을 확장해야 합니다. 애플리케이션이 확장되어야 하는 한 가지 방법은 데이터베이스 수준입니다. Rails는 다중 데이터베이스를 지원하므로 데이터를 한 곳에 모두 저장할 필요가 없습니다.

현재 다음 기능들이 지원됩니다:

  • 다중 writer 데이터베이스와 각각의 replica
  • 작업 중인 model에 대한 자동 연결 전환
  • HTTP verb와 최근 쓰기 작업에 따른 writer와 replica 간 자동 전환
  • 다중 데이터베이스 생성, 삭제, 마이그레이션 및 상호작용을 위한 Rails 태스크

다음 기능들은 (아직) 지원되지 않습니다:

  • replica 로드 밸런싱

1 애플리케이션 설정하기

Rails는 대부분의 작업을 대신 처리해주려 하지만, 애플리케이션에서 여러 개의 데이터베이스를 사용하기 위해서는 여전히 몇 가지 단계를 수행해야 합니다.

예를 들어 하나의 writer 데이터베이스가 있는 애플리케이션에서 새로운 테이블을 위한 새 데이터베이스를 추가해야 한다고 가정해 봅시다. 새 데이터베이스의 이름은 "animals"입니다.

config/database.yml은 다음과 같습니다:

production:
  database: my_primary_database
  adapter: mysql2
  username: root
  password: <%= ENV['ROOT_PASSWORD'] %>

두 번째 데이터베이스인 "animals"와 두 데이터베이스의 replica들을 추가해봅시다. 이를 위해서는 config/database.yml을 2단계 구성에서 3단계 구성으로 변경해야 합니다.

만약 primary 설정 키가 제공되면, 이는 "기본" 구성으로 사용됩니다. primary라는 이름의 구성이 없다면, Rails는 각 환경에서 첫 번째 구성을 기본값으로 사용합니다. 기본 구성은 기본 Rails 파일명을 사용합니다. 예를 들어, primary 구성은 schema 파일로 db/schema.rb를 사용하는 반면, 다른 모든 항목들은 db/[CONFIGURATION_NAMESPACE]_schema.rb를 파일명으로 사용합니다.

production:
  primary:
    database: my_primary_database
    username: root 
    password: <%= ENV['ROOT_PASSWORD'] %>
    adapter: mysql2
  primary_replica:
    database: my_primary_database
    username: root_readonly
    password: <%= ENV['ROOT_READONLY_PASSWORD'] %>
    adapter: mysql2
    replica: true
  animals:
    database: my_animals_database 
    username: animals_root
    password: <%= ENV['ANIMALS_ROOT_PASSWORD'] %>
    adapter: mysql2
    migrations_paths: db/animals_migrate
  animals_replica:
    database: my_animals_database
    username: animals_readonly 
    password: <%= ENV['ANIMALS_READONLY_PASSWORD'] %>
    adapter: mysql2
    replica: true

여러 데이터베이스를 사용할 때는 몇 가지 중요한 설정이 있습니다.

첫째, primaryprimary_replica의 데이터베이스 이름은 동일한 데이터를 포함하고 있기 때문에 같아야 합니다. 이는 animalsanimals_replica의 경우에도 마찬가지입니다.

둘째, writer와 replica의 사용자 이름은 달라야 하며, replica 사용자의 데이터베이스 권한은 읽기 전용으로만 설정되어야 하고 쓰기 권한은 없어야 합니다.

replica 데이터베이스를 사용할 때는 config/database.yml의 replica 항목에 replica: true 설정을 추가해야 합니다. 이는 Rails가 어떤 것이 replica이고 어떤 것이 writer인지 알 수 있는 다른 방법이 없기 때문입니다. Rails는 migration과 같은 특정 작업을 replica에 대해 실행하지 않습니다.

마지막으로, 새로운 writer 데이터베이스의 경우 해당 데이터베이스의 migration을 저장할 디렉토리로 migrations_paths 키를 설정해야 합니다. migrations_paths에 대해서는 이 가이드의 뒷부분에서 자세히 살펴보겠습니다.

schema_dump를 사용자 정의 스키마 파일 이름으로 설정하거나 schema_dump: false로 설정하여 스키마 덤프를 완전히 건너뛰도록 구성할 수도 있습니다.

이제 새로운 데이터베이스가 생겼으니 connection model을 설정해보겠습니다.

primary 데이터베이스 replica는 ApplicationRecord에서 다음과 같이 구성할 수 있습니다:

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true

  # writing과 reading connection을 각각 primary와 primary_replica 데이터베이스에 연결합니다
  connects_to database: { writing: :primary, reading: :primary_replica }  
end

애플리케이션 레코드에 다른 이름의 클래스를 사용하는 경우, primary_abstract_class를 대신 설정해야 합니다. 이를 통해 Rails는 어떤 클래스가 ActiveRecord::Base와 연결을 공유해야 하는지 알 수 있습니다.

class PrimaryApplicationRecord < ActiveRecord::Base
  primary_abstract_class

  # primary 데이터베이스와 primary_replica 데이터베이스에 연결
  connects_to database: { writing: :primary, reading: :primary_replica }
end

이러한 경우에는 primary/primary_replica에 연결하는 클래스들이 일반적인 Rails 애플리케이션이 ApplicationRecord를 상속받는 것처럼 당신의 primary abstract class를 상속받을 수 있습니다:

class Person < PrimaryApplicationRecord
end

반면, "animals" 데이터베이스에 저장된 모델들을 설정해야 합니다:

class AnimalsRecord < ApplicationRecord
  self.abstract_class = true

  connects_to database: { writing: :animals, reading: :animals_replica }
end

해당 모델들은 그 공통된 abstract class를 상속해야 합니다:

class Dog < AnimalsRecord
  # animals 데이터베이스와 자동으로 통신합니다.  
end

기본적으로 Rails는 primary와 replica에 대해 각각 writingreading이라는 데이터베이스 role을 예상합니다. 기존 시스템에서 이미 변경하고 싶지 않은 role이 설정되어 있을 수 있습니다. 이 경우 애플리케이션 config에서 새로운 role 이름을 설정할 수 있습니다.

config.active_record.writing_role = :default
config.active_record.reading_role = :readonly

여러 개의 테이블에 대해 각각의 모델에서 동일한 데이터베이스에 연결하는 대신, 하나의 모델에서 데이터베이스에 연결하고 다른 테이블들은 그 모델을 상속받는 것이 중요합니다. 데이터베이스 클라이언트는 열 수 있는 연결 수에 제한이 있으며, 이를 어기면 Rails가 connection specification name에 모델 클래스 이름을 사용하기 때문에 연결 수가 배가 될 것입니다.

이제 config/database.yml과 새로운 모델을 설정했으니, 데이터베이스를 생성할 차례입니다. Rails는 다중 데이터베이스를 사용하는 데 필요한 모든 명령어를 제공합니다.

bin/rails --help를 실행하면 사용 가능한 모든 명령어를 볼 수 있습니다. 다음과 같은 내용이 표시될 것입니다:

$ bin/rails --help
...
db:create                          # DATABASE_URL 또는 config/database.yml에서 데이터베이스 생성...
db:create:animals                  # 현재 환경의 animals 데이터베이스 생성
db:create:primary                  # 현재 환경의 primary 데이터베이스 생성 
db:drop                            # DATABASE_URL 또는 config/database.yml에서 데이터베이스 삭제...
db:drop:animals                    # 현재 환경의 animals 데이터베이스 삭제
db:drop:primary                    # 현재 환경의 primary 데이터베이스 삭제
db:migrate                         # 데이터베이스 마이그레이션 (옵션: VERSION=x, VERBOSE=false, SCOPE=blog)
db:migrate:animals                 # 현재 환경의 animals 데이터베이스 마이그레이션
db:migrate:primary                 # 현재 환경의 primary 데이터베이스 마이그레이션
db:migrate:status                  # 마이그레이션 상태 표시
db:migrate:status:animals          # animals 데이터베이스의 마이그레이션 상태 표시
db:migrate:status:primary          # primary 데이터베이스의 마이그레이션 상태 표시  
db:reset                           # 현재 환경의 모든 데이터베이스를 삭제하고 스키마에서 재생성한 뒤 시드 데이터를 로드
db:reset:animals                   # 현재 환경의 animals 데이터베이스를 삭제하고 스키마에서 재생성한 뒤 시드 데이터를 로드
db:reset:primary                   # 현재 환경의 primary 데이터베이스를 삭제하고 스키마에서 재생성한 뒤 시드 데이터를 로드
db:rollback                        # 스키마를 이전 버전으로 롤백 (STEP=n으로 단계 지정)
db:rollback:animals                # 현재 환경의 animals 데이터베이스를 롤백 (STEP=n으로 단계 지정)
db:rollback:primary                # 현재 환경의 primary 데이터베이스를 롤백 (STEP=n으로 단계 지정)
db:schema:dump                     # 데이터베이스 스키마 파일 생성 (db/schema.rb 또는 db/structure.sql...)
db:schema:dump:animals             # 데이터베이스 스키마 파일 생성 (db/schema.rb 또는 db/structure.sql...)
db:schema:dump:primary             # 모든 DB에서 이식 가능한 db/schema.rb 파일 생성...
db:schema:load                     # 데이터베이스 스키마 파일 로드 (db/schema.rb 또는 db/structure.sql...)
db:schema:load:animals             # 데이터베이스 스키마 파일 로드 (db/schema.rb 또는 db/structure.sql...)
db:schema:load:primary             # 데이터베이스 스키마 파일 로드 (db/schema.rb 또는 db/structure.sql...)  
db:setup                           # 모든 데이터베이스를 생성하고 모든 스키마를 로드한 뒤 시드 데이터로 초기화 (db:reset을 사용하면 먼저 모든 데이터베이스를 삭제)
db:setup:animals                   # animals 데이터베이스를 생성하고 스키마를 로드한 뒤 시드 데이터로 초기화 (db:reset:animals를 사용하면 먼저 데이터베이스를 삭제)
db:setup:primary                   # primary 데이터베이스를 생성하고 스키마를 로드한 뒤 시드 데이터로 초기화 (db:reset:primary를 사용하면 먼저 데이터베이스를 삭제)
...

bin/rails db:create와 같은 명령어를 실행하면 primary 데이터베이스와 animals 데이터베이스가 모두 생성됩니다. 데이터베이스 사용자를 생성하는 명령어는 없으며, replica를 위한 읽기 전용 사용자를 지원하려면 수동으로 생성해야 합니다. animals 데이터베이스만 생성하려면 bin/rails db:create:animals를 실행하면 됩니다.

2 스키마와 마이그레이션 관리 없이 데이터베이스에 연결하기

스키마 관리, 마이그레이션, seed 등의 데이터베이스 관리 작업 없이 외부 데이터베이스에 연결하고 싶다면, 데이터베이스별 설정 옵션에서 database_tasks: false를 설정할 수 있습니다. 기본값은 true로 설정되어 있습니다.

production:
  primary:
    database: my_database 
    adapter: mysql2
  animals:
    database: my_animals_database
    adapter: mysql2
    database_tasks: false

3 Generators and Migrations

여러 데이터베이스를 위한 migration은 설정의 데이터베이스 키로 접두사가 붙은 각자의 폴더에 위치해야 합니다.

또한 Rails가 migration을 찾을 위치를 알려주기 위해 데이터베이스 설정에 migrations_paths를 설정해야 합니다.

예를 들어 animals 데이터베이스는 db/animals_migrate 디렉토리에서 migration을 찾고 primarydb/migrate에서 찾습니다. Rails generator는 이제 파일이 올바른 디렉토리에 생성되도록 --database 옵션을 제공합니다. 다음과 같이 명령을 실행할 수 있습니다:

$ bin/rails generate migration CreateDogs name:string --database animals

Rails generators를 사용하는 경우, scaffold와 model generators는 abstract class를 생성해줍니다. 단순히 database key를 command line에 전달하기만 하면 됩니다.

$ bin/rails generate scaffold Dog name:string --database animals

Camelized된 데이터베이스 이름과 Record를 사용하여 클래스가 생성됩니다. 이 예제에서는 데이터베이스가 "animals"이므로 AnimalsRecord가 생성됩니다:

class AnimalsRecord < ApplicationRecord
  self.abstract_class = true

  connects_to database: { writing: :animals }
end

이는 abstract 클래스이며, :animals database에 연결하는 기본 클래스를 생성합니다.

생성된 모델은 자동으로 AnimalsRecord를 상속받습니다.

class Dog < AnimalsRecord
end

Rails는 writer에 대한 replica 데이터베이스가 무엇인지 알 수 없으므로 작업이 완료된 후 abstract 클래스에 이를 추가해야 합니다.

Rails는 AnimalsRecord를 한 번만 생성합니다. 새로운 scaffold에 의해 덮어쓰이거나 scaffold가 삭제되어도 삭제되지 않습니다.

이미 abstract 클래스가 있고 그 이름이 AnimalsRecord와 다른 경우, --parent 옵션을 전달하여 다른 abstract 클래스를 사용하고 싶다는 것을 지정할 수 있습니다:

$ bin/rails generate scaffold Dog name:string --database animals --parent Animals::Record

AnimalsRecord를 생성하는 것을 건너뛸 것입니다. Rails에게 다른 parent class를 사용하고 싶다고 지정했기 때문입니다.

4 자동 Role 전환 활성화

마지막으로, 애플리케이션에서 읽기 전용 replica를 사용하려면 자동 전환을 위한 middleware를 활성화해야 합니다.

자동 전환을 통해 애플리케이션은 HTTP verb와 요청하는 사용자의 최근 쓰기 여부를 기반으로 writer에서 replica로 또는 replica에서 writer로 전환할 수 있습니다.

애플리케이션이 POST, PUT, DELETE 또는 PATCH 요청을 받으면 자동으로 writer 데이터베이스에 쓰기를 수행합니다. 요청이 이러한 메서드가 아니더라도 애플리케이션이 최근에 쓰기를 수행했다면 writer 데이터베이스가 사용됩니다. 다른 모든 요청은 replica 데이터베이스를 사용합니다.

자동 연결 전환 middleware를 활성화하려면 다음과 같이 자동 전환 generator를 실행할 수 있습니다:

$ bin/rails g active_record:multi_db

그리고 다음 라인들의 주석을 해제하세요:

Rails.application.configure do
  config.active_record.database_selector = { delay: 2.seconds }
  config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
  config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
end

다음은 한국어로 번역한 내용입니다:

Rails는 "자신이 쓴 내용 읽기"를 보장하며, GET 또는 HEAD 요청이 delay 창 내에 있는 경우 writer로 요청을 보냅니다. 기본적으로 delay는 2초로 설정되어 있습니다. 이는 데이터베이스 인프라에 따라 변경해야 합니다. Rails는 delay 창 내에서 다른 사용자들의 "최근 쓰기 읽기"를 보장하지 않으며, 최근에 쓰기를 하지 않은 경우 GET과 HEAD 요청을 replica로 보냅니다.

Rails의 자동 커넥션 전환은 상대적으로 단순하며 의도적으로 많은 작업을 수행하지 않습니다. 목표는 앱 개발자가 커스터마이즈할 수 있을 만큼 유연한 자동 커넥션 전환 시스템을 구현하는 것입니다.

Rails의 설정을 통해 전환 방식과 기준이 되는 매개변수를 쉽게 변경할 수 있습니다. 예를 들어, 커넥션 전환을 결정할 때 session 대신 cookie를 사용하고 싶다면 다음과 같이 자신만의 클래스를 작성할 수 있습니다:

class MyCookieResolver < ActiveRecord::Middleware::DatabaseSelector::Resolver
  # 요청을 받아 cookie로부터 새 인스턴스를 생성합니다
  def self.call(request)
    new(request.cookies)
  end

  # cookie로 초기화합니다
  def initialize(cookies)
    @cookies = cookies
  end

  attr_reader :cookies

  # cookie에서 마지막 쓰기 타임스탬프를 읽어옵니다
  def last_write_timestamp
    self.class.convert_timestamp_to_time(cookies[:last_write])
  end

  # 현재 시간으로 마지막 쓰기 타임스탬프를 업데이트합니다
  def update_last_write_timestamp
    cookies[:last_write] = self.class.convert_time_to_timestamp(Time.now)
  end

  # 응답을 저장합니다
  def save(response)
  end
end

그리고 middleware에 전달하세요:

config.active_record.database_selector = { delay: 2.seconds }
config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
config.active_record.database_resolver_context = MyCookieResolver

이는 replica database를 위한 custom resolver context를 추가합니다. delay는 writeContext가 만료되기까지의 시간을 나타냅니다. resolver context는 요청이 읽기인지 쓰기인지를 판단하는 로직을 포함합니다.

5 수동 Connection 전환 사용하기

자동 connection 전환이 충분하지 않고, 애플리케이션이 writer나 replica에 연결해야 하는 경우가 있습니다. 예를 들어, POST request 경로에 있더라도 특정 요청을 항상 replica로 보내고 싶은 경우가 있을 수 있습니다.

이를 위해 Rails는 필요한 connection으로 전환할 수 있는 connected_to 메서드를 제공합니다.

ActiveRecord::Base.connected_to(role: :reading) do
  # 이 블록 내의 모든 코드는 reading role에 연결됩니다.
end

connected_to 호출의 "role"은 해당 connection handler(또는 role)에 연결된 connection들을 조회합니다. reading connection handler는 connects_to를 통해 reading role 이름으로 연결된 모든 connection들을 가지고 있습니다.

role을 사용한 connected_to는 기존 connection을 조회하고 connection specification 이름을 사용하여 전환한다는 점에 유의하세요. 이는 connected_to(role: :nonexistent)와 같이 존재하지 않는 role을 전달하면 ActiveRecord::ConnectionNotEstablished (No connection pool for 'ActiveRecord::Base' found for the 'nonexistent' role.)라는 에러가 발생한다는 것을 의미합니다.

Rails가 수행되는 모든 쿼리가 읽기 전용인지 확인하도록 하려면 prevent_writes: true를 전달하세요. 이는 단순히 쓰기처럼 보이는 쿼리가 데이터베이스로 전송되는 것을 방지합니다. replica 데이터베이스를 읽기 전용 모드로 실행하도록 구성해야 합니다.

ActiveRecord::Base.connected_to(role: :reading, prevent_writes: true) do
  # Rails는 각 쿼리가 읽기 쿼리인지 확인합니다.
end

6 Horizontal Sharding

Horizontal sharding은 각 데이터베이스 서버의 행 수를 줄이기 위해 데이터베이스를 분할하되, "shards" 전체에서 동일한 스키마를 유지하는 것입니다. 이는 일반적으로 "multi-tenant" sharding이라고 불립니다.

Rails에서 horizontal sharding을 지원하는 API는 Rails 6.0부터 존재했던 multiple database / vertical sharding API와 유사합니다.

Shards는 다음과 같이 3계층 구성으로 선언됩니다:

production:
  primary:
    database: my_primary_database
    adapter: mysql2
  primary_replica:
    database: my_primary_database
    adapter: mysql2
    replica: true
  primary_shard_one:
    database: my_primary_shard_one
    adapter: mysql2
    migrations_paths: db/migrate_shards
  primary_shard_one_replica:
    database: my_primary_shard_one
    adapter: mysql2
    replica: true
  primary_shard_two:
    database: my_primary_shard_two
    adapter: mysql2
    migrations_paths: db/migrate_shards
  primary_shard_two_replica:
    database: my_primary_shard_two
    adapter: mysql2
    replica: true

Model은 shards 키를 통해 connects_to API와 연결됩니다:

class ApplicationRecord < ActiveRecord::Base
  primary_abstract_class

  connects_to database: { writing: :primary, reading: :primary_replica }
end

class ShardRecord < ApplicationRecord
  self.abstract_class = true

  connects_to shards: {
    shard_one: { writing: :primary_shard_one, reading: :primary_shard_one_replica },
    shard_two: { writing: :primary_shard_two, reading: :primary_shard_two_replica }
  }
end

shards를 사용하는 경우, 모든 shard에 대해 migrations_pathsschema_dump가 변경되지 않는지 확인하세요. migration을 생성할 때 --database 옵션을 전달하고 shard 이름 중 하나를 사용할 수 있습니다. 모두 동일한 경로를 설정하므로 어떤 것을 선택하든 상관없습니다.

$ bin/rails g scaffold Dog name:string --database primary_shard_one

그런 다음 모델은 connected_to API를 통해 수동으로 shard를 교체할 수 있습니다. sharding을 사용하는 경우 roleshard 모두를 전달해야 합니다:

ActiveRecord::Base.connected_to(role: :writing, shard: :default) do
  @id = Person.create! # ":default"라는 이름의 shard에 레코드 생성
end

ActiveRecord::Base.connected_to(role: :writing, shard: :shard_one) do
  Person.find(@id) # 레코드를 찾을 수 없습니다. ":default"라는 이름의 
                   # shard에 생성되었기 때문입니다.
end

horizontal sharding API는 읽기 전용 복제본도 지원합니다. connected_to API를 사용하여 역할과 shard를 교체할 수 있습니다.

ActiveRecord::Base.connected_to(role: :reading, shard: :shard_one) do
  Person.first # shard one의 read replica에서 레코드를 조회합니다.
end

다음은 한국어 번역입니다:

7 Automatic Shard Switching 활성화하기

애플리케이션은 제공된 middleware를 사용하여 요청별로 shard를 자동으로 전환할 수 있습니다.

ShardSelector middleware는 shard를 자동으로 교체하기 위한 프레임워크를 제공합니다. Rails는 어떤 shard로 전환할지 결정하기 위한 기본 프레임워크를 제공하고, 필요한 경우 애플리케이션이 교체를 위한 커스텀 전략을 작성할 수 있도록 합니다.

ShardSelector는 middleware의 동작을 변경하는 데 사용할 수 있는 옵션 세트(현재는 lock만 지원됨)를 받습니다. lock은 기본적으로 true이며, 블록 내부에서 shard 전환을 금지합니다. lock이 false인 경우 shard 전환이 허용됩니다. tenant 기반 sharding의 경우, 애플리케이션 코드가 실수로 tenant 간에 전환하는 것을 방지하기 위해 lock은 항상 true여야 합니다.

database selector와 동일한 generator를 사용하여 자동 shard 전환을 위한 파일을 생성할 수 있습니다:

$ bin/rails g active_record:multi_db

그런 다음 생성된 config/initializers/multi_db.rb에서 다음 내용의 주석을 해제하세요:

Rails.application.configure do
  config.active_record.shard_selector = { lock: true }
  config.active_record.shard_resolver = ->(request) { Tenant.find_by!(host: request.host).shard }
end

애플리케이션에서는 application-specific model에 의존하므로 resolver의 코드를 제공해야 합니다. 다음은 resolver의 예시입니다:

config.active_record.shard_resolver = ->(request) {
  subdomain = request.subdomain  
  tenant = Tenant.find_by_subdomain!(subdomain)
  tenant.shard  
}

8 Granular Database Connection Switching

Rails 6.1부터는 모든 데이터베이스를 전역적으로 전환하는 대신 하나의 데이터베이스에 대한 연결을 전환하는 것이 가능합니다.

세분화된 데이터베이스 연결 전환을 통해, 모든 추상 연결 클래스는 다른 연결에 영향을 주지 않고 연결을 전환할 수 있습니다. 이는 ApplicationRecord 쿼리가 primary로 가도록 하면서 AnimalsRecord 쿼리는 replica에서 읽도록 전환하는 것과 같은 경우에 유용합니다.

AnimalsRecord.connected_to(role: :reading) do
  Dog.first # animals_replica에서 읽기를 수행합니다.
  Person.first  # primary에서 읽기를 수행합니다.
end

shard에 대해 세부적으로 connection을 교체하는 것도 가능합니다.

AnimalsRecord.connected_to(role: :reading, shard: :shard_one) do
  # shard_one_replica에서 읽어옵니다. shard_one_replica에 대한 연결이 존재하지 않는 경우,
  # ConnectionNotEstablished 에러가 발생합니다.
  Dog.first

  # primary writer에서 읽어옵니다. 
  Person.first
end

Primary 데이터베이스 클러스터만 전환하려면 ApplicationRecord를 사용하세요:

ApplicationRecord.connected_to(role: :reading, shard: :shard_one) do
  Person.first # primary_shard_one_replica에서 읽음
  Dog.first # animals_primary에서 읽음
end

ActiveRecord::Base.connected_to는 전역적으로 연결을 전환하는 기능을 유지합니다.

8.1 여러 데이터베이스 간의 Joins를 사용한 Association 처리

Rails 7.0+ 버전부터, Active Record는 여러 데이터베이스에 걸친 join을 수행하는 association을 처리하는 옵션을 제공합니다. has many through 또는 has one through association에서 joining을 비활성화하고 2개 이상의 쿼리를 수행하고 싶다면, disable_joins: true 옵션을 전달하세요.

예시:

class Dog < AnimalsRecord
  has_many :treats, through: :humans, disable_joins: true
  has_many :humans

  has_one :home  
  has_one :yard, through: :home, disable_joins: true
end

class Home
  belongs_to :dog
  has_one :yard
end

class Yard
  belongs_to :home
end

이전에 disable_joins 없이 @dog.treats를 호출하거나 disable_joins 없이 @dog.yard를 호출하면 데이터베이스가 클러스터 간 join을 처리할 수 없기 때문에 에러가 발생했습니다. disable_joins 옵션을 사용하면 Rails는 클러스터 간 join을 시도하지 않도록 여러 개의 select 쿼리를 생성합니다. 위의 association에서 @dog.treats는 다음과 같은 SQL을 생성합니다:

SELECT "humans"."id" FROM "humans" WHERE "humans"."dog_id" = ?  [["dog_id", 1]]
SELECT "treats".* FROM "treats" WHERE "treats"."human_id" IN (?, ?, ?)  [["human_id", 1], ["human_id", 2], ["human_id", 3]]

@dog.yard는 다음 SQL을 생성할 것입니다:

"homes"에서 "home"."id" 선택하되 "homes"."dog_id" = ?  경우 [["dog_id", 1]]
"yards"에서 "yards".* 선택하되 "yards"."home_id" = ?  경우 [["home_id", 1]]

이 옵션과 관련하여 알아야 할 중요한 사항들이 있습니다:

  1. (연관 관계에 따라) 하나의 join 대신 두 개 이상의 쿼리가 실행되므로 성능에 영향을 미칠 수 있습니다. 만약 humans에 대한 select가 많은 ID를 반환한다면 treats에 대한 select는 너무 많은 ID를 전송할 수 있습니다.
  2. 더 이상 join을 수행하지 않기 때문에, order나 limit이 있는 쿼리는 이제 메모리 내에서 정렬됩니다. 한 테이블의 order는 다른 테이블에 적용될 수 없기 때문입니다.
  3. 이 설정은 join을 비활성화하려는 모든 연관 관계에 추가되어야 합니다. Rails는 연관 관계 로딩이 지연(lazy)되기 때문에 이를 추측할 수 없습니다. @dog.treats에서 treats를 로드하기 위해서는 Rails가 어떤 SQL이 생성되어야 하는지 이미 알고 있어야 합니다.

8.2 Schema Caching

각 데이터베이스에 대한 schema cache를 로드하려면 각 데이터베이스 설정에서 schema_cache_path를 설정하고 애플리케이션 설정에서 config.active_record.lazily_load_schema_cache = true를 설정해야 합니다. 데이터베이스 연결이 설정될 때 cache가 지연 로드된다는 점에 유의하세요.

9 Caveats

9.1 Load Balancing Replicas

Rails는 replica들의 자동 load balancing을 지원하지 않습니다. 이는 인프라 환경에 매우 의존적입니다. 향후 기본적이고 원시적인 load balancing을 구현할 수 있지만, 대규모 애플리케이션의 경우 Rails 외부에서 애플리케이션이 처리해야 하는 부분입니다.



맨 위로