Django docs | Multiple databases

combi_jihoon·2022년 5월 9일
0

Django Docs

목록 보기
8/8
post-thumbnail

이 글은 Django Docs를 읽고 정리한 것입니다.


  • 장고 settings에 들어갈 수 있는 환경변수 중 DATABASE_ROUTERS가 있다. 이 환경 변수에는 마스터 라우터(django.db.router)가 사용해야 하는 라우터의 클래스 명이 리스트로 아래와 같이 주어진다.
    - DATABASE_ROUTERS = [<path to your custom router>]
  • 마스터 라우터는 장고 실행시 어떤 데이터베이스를 사용 해야 하는 지 결정하기 위해 사용 된다.
    - 즉, 쿼리 실행시 특정 데이터베이스를 이용해야 한다면 이 경우 가장 먼저 마스터 라우터를 호출하며 이 때 model과 hint를 마스터 라우터에 전달한다.
  • model, hint 인자를 받은 장고는 맞는 데이터베이스를 찾을 때까지 라우터를 실행한다.
    - 만약 맞는 데이터베이스가 없는 경우 hint에 제공되는 instance에 대해 instance._state.db를 실행해 맞는 데이터베이스를 찾는다.
    - 만약 hint instance도 제공 되지 않았다면 마스터 라우터는 'default'를 데이터베이스로 지정한다. 즉, 장고의 가장 기본 데이터베이스는 'default'이다.

DATABASE_ROUTERS를 특별히 지정하는 경우는 여러 개의 데이터베이스를 사용하는 상황이 될 것이다. 그렇다면 장고에서는 여러 개의 데이터베이스를 어떻게 구성해 사용할 수 있을까? 위의 <path to your custom router>에 들어가는 정보를 알아 보자.



Databases 정의

  • 기본적으로는 DATABASES의 key 값으로 'default'가 제공된다. 만약 다른 데이터베이스를 함께 사용하고 싶다면 key 값으로 alias를 아래와 같이 추가하면 된다.
  DATABASES = {
      'default': {
          'NAME': 'app_data',
          'ENGINE': 'django.db.backends.postgresql',
          'USER': 'postgres_user',
          'PASSWORD': 's3krit'
      },
      'users': {
          'NAME': 'user_data',
          'ENGINE': 'django.db.backends.mysql',
          'USER': 'mysql_user',
          'PASSWORD': 'priv4te'
      }
  }
  • 만약 사용자가 default를 사용하고 싶지 않다 해도 default는 없애면 안된다. 이럴 때는 없애는 대신 'default' = {}으로 비워 두어야 한다.
    - 이와 함께, DATABASE_ROUTERS 환경 변수를 정의해 각 앱에서 사용되는 모델이 어느 데이터베이스로 라우팅 되어야 하는 지를 정의해야 한다.
  • 정의 되지 않은 데이터베이스를 사용하는 경우 장고는 아래와 같은 에러를 던진다.
    - django.utils.connection.ConnectionDoesNotExist


Database routers

  • 여러 개의 데이터베이스를 사용하는 경우 데이터베이스 라우터를 오버라이딩 하지 않으면 장고는 어떤 데이터베이스를 사용해야 하는지 모르는 상황이 생길 수 있다.
    - 이 경우 모든 쿼리는 default 데이터베이스에서 실행 된다. 즉, 장고는 기본적으로 'default' 데이터베이스에 모든 쿼리를 실행하도록 되어 있다.
  • 그런데 만약 'read'와 'write'를 분리하고 싶은 경우, 즉 요청 량이 많은 read 요청이 특정 데이터베이스에서 이루어지도록 하고 싶은 경우 커스텀한 database router가 필요하다.

database router 클래스는 아래와 같이 4개의 메소드로 이루어져 있다. 이를 오버라이딩 해서 커스텀 라우터를 생성하면 된다. 이 때, 아래의 4개 메소드가 반드시 주어지지는 않아도 된다. 단, 메소드가 주어지지 않는 경우에 대해서는 장고가 관련성 체크시 해당 라우터를 건너 뛴다는 점은 주의해야 한다.

db_for_read

  • 파라미터: model, **hints
  • read 쿼리 실행시 model 타입의 객체에 사용해야 하는 데이터베이스를 알려주기 위해 사용 된다.
  • 만약 read 쿼리 실행시 어떤 데이터베이스를 선택해야 하는지 도움을 줄 수 있는 정보가 있다면 hints 파라미터에 담아서 줄 수 있다.
    - hints 파라미터에 들어갈 수 있는 정보로 가능한 것은 instance이다. 이 instance는 진행중인 읽기 또는 쓰기 쿼리에 관련 있는 객체 인스턴스로, 새롭게 저장해야 하는 인스턴스이거나 혹은 다대다 관계에 추가 되어야 하는 인스턴스 등 여러 가지가 있을 수 있다(물론 hint가 아예 없을 수도 있다).
    - router는 인스턴스 hint가 있는지 확인 하고 이 hint에 따라 라우팅을 변경해야 하는 지를 결정 한다.
  • read 쿼리를 위해 필요한 데이터베이스 정보가 없다면 None을 반환하면 된다.

db_for_write

  • 파라미터: model, **hints
  • db_for_read 메소드에서와 마찬가지로 write 쿼리 실행시 model 타입의 객체에 사용해야 하는 데이터베이스를 알려주기 위해 사용 된다.

allow_relation

  • 파라미터: obj1, obj2, **hints
  • obj1, obj2 사이에 관계가 있다면 True, 혹은 관계가 만들어 지지 않아야 하는 경우 False를 반환하며 아무런 정보가 없을 경우 None을 반환할 수도 있다.
  • 검증을 위해 사용하는 로직으로 두 객체 사이의 특정한 관계(외래키, 다대다 등등)가 결정 되어야 하는 지 파악하기 위해 사용한다.

allow_migrate

  • 파라미터: db, app_label, model_name=None, **hints
  • 이 메소드는 db를 alias로 갖는 데이터베이스에 migration을 해도 되는 지를 결정한다.
    - True/False/None을 리턴한다.
  • app_label은 migration 되는 앱의 label이다.
  • model_name은 migration 되는 모델의 모델명에 의해 결정 된다.
    - model._meta.model_name(model의 __name__의 소문자 버전과 동일함) 값에 해당한다.
    - hints 인자에 의해 값이 주어지지 않는다면 None이다.
    - 만약 hints 인자에 의해 model_name 인자 값이 주어지는 경우 hints는 일반적으로 'model' 키 값을 갖는다. 이 값은 보통 어떠한 속성이나 메소드 등을 갖지 않아 'model' 키의 _meta를 통해 model_name을 알아내야 한다.
  • 이 메소드는 주어진 데이터베이스에서 특정 모델을 사용할 수 있는 지를 결정하기 위해 사용 되기도 한다.
    - 즉, allow_migrate()이 False를 반환하면 주어진 model_name에 대한 마이그레이션은 실행 되지 않는다.


Database routers 사용하기

지금부터 나열되는 내용은 실제로 어떻게 사용할 수 있을 지에 대한 하나의 예시이며 독스에 나와 있는 예시이다.

독스에서는 auth db를 따로 관리하고 있는데 실제로 이 방법이 좋은 지는 모르겠다. 우리 서비스에서는 Aurora를 쓰며 라이터와 리더를 분리해 라이터는 읽기 + 쓰기 모두를, 리더는 읽기 작업만 할 수 있도록 하고 있다.
이 방법에 대해서는 파이콘 강의에서 본 내용을 토대로 정리해 놓았으며 추후 우리 서비스에서 사용한 방법과 결합해 조금 더 자세한 사용 방법을 적어 보려 한다.

settings에 추가하기

DATABASE_ROUTERS

아래와 같이 사용할 라우터를 추가하면 된다.

DATABASE_ROUTERS = ['path.to.AuthRouter', 'path.to.PrimaryReplicaRouter']

  • 인덱스 0번부터 순서대로 AuthRouter -> PrimaryReplicaRouter 순으로 실행 된다.
    - 따라서, 오버라이딩 되지 않도록 주의해야 한다.

DATABASES

사용할 데이터베이스 예시는 다음과 같다.

  • default, auth_db, primary, replica 2개이다.
DATABASES = {
    'default': {},
    'auth_db': {
        'NAME': 'auth_db_name',
        'ENGINE': 'django.db.backends.mysql',
        'USER': 'mysql_user',
        'PASSWORD': 'swordfish',
    },
    'primary': {
        'NAME': 'primary_name',
        'ENGINE': 'django.db.backends.mysql',
        'USER': 'mysql_user',
        'PASSWORD': 'spam',
    },
    'replica1': {
        'NAME': 'replica1_name',
        'ENGINE': 'django.db.backends.mysql',
        'USER': 'mysql_user',
        'PASSWORD': 'eggs',
    },
    'replica2': {
        'NAME': 'replica2_name',
        'ENGINE': 'django.db.backends.mysql',
        'USER': 'mysql_user',
        'PASSWORD': 'bacon',
    },
}

라우터 생성하기

AuthRouter

  • authcontenttypes의 경우 장고의 기본 앱으로, 각각 Auth, ContentType을 모델로 가지며 서로 연결 되어 있다.
  • 두 앱은 auth_db를 사용 한다.

아래 예시에서 auth, contenttypes 관련된 operation은 모두 auth_db로 향하도록 라우팅 되어 있다.

class AuthRouter:
    """
    auth와 contenttypes 앱의 모델에 일어나는 DB operation에 대한 라우터이다.
    """
    route_app_labels = {'auth', 'contenttypes'}  

    def db_for_read(self, model, **hints):
        """
        Attempts to read auth and contenttypes models go to auth_db.
        """
        if model._meta.app_label in self.route_app_labels:
            return 'auth_db'
        return None

    def db_for_write(self, model, **hints):
        """
        Attempts to write auth and contenttypes models go to auth_db.
        """
        if model._meta.app_label in self.route_app_labels:
            return 'auth_db'
        return None

    def allow_relation(self, obj1, obj2, **hints):
        """
        Allow relations if a model in the auth or contenttypes apps is
        involved.
        """
        if (
            obj1._meta.app_label in self.route_app_labels or
            obj2._meta.app_label in self.route_app_labels
        ):
           return True
        return None

    def allow_migrate(self, db, app_label, model_name=None, **hints):
        """
        Make sure the auth and contenttypes apps only appear in the
        'auth_db' database.
        """
        if app_label in self.route_app_labels:
            return db == 'auth_db'
        return None

PrimaryReplicaRouter

  • PrimaryReplicaRouter는 primary와 두 개의 replica를 사용한다.
    - primary는 쓰기 작업에, replica는 읽기 작업에 사용한다.
import random

class PrimaryReplicaRouter:
    def db_for_read(self, model, **hints):
        """
        replica를 랜덤하게 골라 사용한다(부하 분산).
        """
        return random.choice(['replica1', 'replica2'])

    def db_for_write(self, model, **hints):
        """
        읽기 작업은 primary로 라우팅 한다.
        """
        return 'primary'

    def allow_relation(self, obj1, obj2, **hints):
        """
        primary 또는 replica pool에 있는 객체들은 서로 관계를 갖고 있다(auth와 관련 되어 있지 않으면 전부 관계를 갖고 있다).
        """
        db_set = {'primary', 'replica1', 'replica2'}
        if obj1._state.db in db_set and obj2._state.db in db_set:
            return True
        return None

    def allow_migrate(self, db, app_label, model_name=None, **hints):
        """
        auth와 관련된 모델이 아니라면 migrate가 허용 된다. 
        auth는 auth_db에서 따로 관리한다.
        """
        return True


수동으로 database 선택하기

쿼리를 짤 때 수동으로 원하는 데이터베이스를 선택해야 할 때가 있다. 예를 들어, 간단한 로직이 있는데 그 안에 들어가는 쿼리가 리더만 조회하도록 하고 싶을 수 있다. 그러면 이 때는 using을 이용하면 된다.

using 이용해 database 선택하기

  • 아래와 같이 using을 사용하지 않는 경우 기본적으로 'default' 데이터베이스를 선택한다.
  • 만약 default를 명시적으로 선택하고 싶다면 using 안에 원하는 데이터베이스명을 쓰면 된다.
    - replica의 경우도 마찬가지로 'replica'를 using의 인자로 주면 된다.
Author.objects.all()
Author.objects.using('default').all()
Author.objects.using('replica').all()
profile
쿄쿄

0개의 댓글