NOT NULL 제약 조건

값으로 NULL을 허용하지 않아야 하는 모든 속성은 데이터베이스의 NOT NULL 열로 정의되어야 합니다.

응용 프로그램 논리에 따라 NOT NULL 열은 모델에서 presence: true 유효성이 정의되어야 하거나 데이터베이스 정의의 일부로 기본값이 있어야 합니다. 예를 들어, 후자는 항상 NULL이 아닌 값을 가져야 하지만 애플리케이션이 매번 강제하지 않아도 되는 불리언 속성에 대해 참인 기본값이 있을 수 있습니다(예: active=true).

NOT NULL 열이 있는 새로운 테이블 생성

새로운 테이블을 추가할 때, 모든 NOT NULL 열은 create_table 내에서 직접 정의되어야 합니다.

예를 들어, 두 개의 NOT NULL 열이 있는 테이블을 생성하는 마이그레이션을 고려해보세요. db/migrate/20200401000001_create_db_guides.rb:

class CreateDbGuides < Gitlab::Database::Migration[2.1]
  def change
    create_table :db_guides do |t|
      t.bigint :stars, default: 0, null: false
      t.bigint :guide, null: false
    end
  end
end

기존 테이블에 NOT NULL 열 추가

GitLab 13.0 이후에 최소 PostgreSQL 11이 되면서, NULL 및/또는 기본값이 있는 열을 추가하는 것이 훨씬 쉬워졌으며 모든 경우에 표준 add_column 도우미를 사용해야 합니다.

예를들어, db_guides 테이블에 새로운 NOT NULLactive를 추가하는 마이그레이션을 고려해보세요. db/migrate/20200501000001_add_active_to_db_guides.rb:

class AddExtendedTitleToSprints < Gitlab::Database::Migration[2.1]
  def change
    add_column :db_guides, :active, :boolean, default: true, null: false
  end
end

기존 열에 NOT NULL 제약 조건 추가

기존 데이터베이스 열에 NOT NULL을 추가하는 것은 보통 적어도 두 개의 다른 릴리즈로 나뉘어 여러 단계로 진행됩니다. 테이블이 충분히 작아 백그라운드 마이그레이션을 사용할 필요가 없는 경우, 이러한 모든 작업을 동일한 머지 리퀘스트에 포함시킬 수 있습니다. 트랜잭션 기간을 줄이기 위해 별도의 마이그레이션을 사용하는 것이 좋습니다.

필요한 단계는 다음과 같습니다:

  1. 릴리즈 N.M (현재 릴리즈)

    1. $속성 값이 응용 프로그램 수준에서 설정되고 있는지 확인합니다.
      1. 속성에 기본값이 있는 경우, 새 레코드에 기본값이 설정되도록 모델에 기본값을 추가합니다.
      2. 새로운 레코드와 기존 레코드에 nil로 설정되는 속성이 있는 경우 해당 코딩을 업데이트합니다. 모든 콜백을 건너뛰는 일부 프로세스가 있기 때문에 before_savebefore_validation과 같은 ActiveRecord 콜백을 사용하는 것만으로 충분하지 않을 수 있습니다. update_column, update_columns, insert_all, update_all 등의 대량 작업이 어떤 메서드인지 주의 깊게 살펴봐야 합니다.
    2. 기존 레코드를 수정하는 사후 배포 마이그레이션을 추가합니다.
    note
    테이블 크기에 따라 다음 릴리즈에서 클린업 배경 마이그레이션이 필요할 수 있습니다. 자세한 정보는 대규모 테이블의 NOT NULL 제약 조건 섹션을 참조하세요.
  2. 릴리즈 N.M+1 (다음 릴리즈)

    1. GitLab.com의 모든 기존 레코드가 속성이 설정되었는지 확인합니다. 그렇지 않으면 릴리즈 N.M 단계 1부터 다시 시작합니다.
    2. 단계 1이 정상적으로 보이고 배치된 백그라운드 마이그레이션에서 배포 마이그레이션을 완료하는 경우.
    3. 모든 기존 및 새 레코드가 유효해야 하므로 모델에서 속성에 대한 유효성을 추가합니다.
    4. NOT NULL 제약을 추가하는 사후 배포 마이그레이션을 추가합니다.

예제

13.0과 같은 특정 릴리스 마일스톤을 고려합니다.

프로덕션 데이터베이스를 확인한 후, NULL 설명이있는 epics가 있는 것으로 알고 있으므로 제약을 한 번에 추가하고 유효성을 검사할 수 없습니다.

note
NULL 설명이 없더라도 GitLab의 다른 인스턴스에는 해당 레코드가 있을 수 있으므로 어떤 경우에도 동일한 프로세스를 따를 것입니다.

새로운 유효하지 않은 레코드 방지 (현재 릴리즈)

속성이 NULL로 설정되는 경우 새로운 및 기존 레코드에 대해 속성을 nil 값이 아닌 값으로 설정하는 코드 경로를 모두 업데이트합니다.

새로운 레코드에 기본값이 설정되도록 레일 애트리뷰트 API를 사용하여 epic.rb에 속성이 추가되었습니다:

class Epic < ApplicationRecord
  attribute :description, default: 'No description'
end

현재 릴리스를 위한 데이터 마이그레이션으로 기존 레코드 수정

여기서의 접근 방식은 데이터 양과 정리 전략에 따라 다릅니다. GitLab.com에서 수정해야 하는 레코드 수는 후속 마이그레이션 또는 백그라운드 데이터 마이그레이션을 사용할지 결정하는 데 도움이 되는 좋은 지표입니다.

  • 데이터 양이 1000 레코드보다 적으면 데이터 마이그레이션을 후속 마이그레이션 내에서 실행할 수 있습니다.
  • 데이터 양이 1000 레코드보다 많은 경우 백그라운드 마이그레이션을 생성하는 것이 좋습니다.

어떤 옵션을 사용할지 확신이 들 때는 데이터베이스 팀에게 조언을 구하세요.

우리 예제로 돌아와서, epics 테이블은 상당히 크지 않으며 빈번하게 접근되지 않기 때문에 현재 13.0 마일스톤에 대한 후속 배포 마이그레이션을 추가합니다. db/post_migrate/20200501000002_cleanup_epics_with_null_description.rb:

class CleanupEpicsWithNullDescription < Gitlab::Database::Migration[2.1]
  # BATCH_SIZE=1000 및 GitLab.com의 epics.count=29500를 통해
  # - 30번의 반복 실행
  # - 각각 평균 ~150ms 소요
  # 예상 총 실행 시간: ~5초
  BATCH_SIZE = 1000

  disable_ddl_transaction!

  class Epic < ActiveRecord::Base
    include EachBatch

    self.table_name = 'epics'
  end

  def up
    Epic.each_batch(of: BATCH_SIZE) do |relation|
      relation.
        where('description IS NULL').
        update_all(description: 'No description')
    end
  end

  def down
    # no-op : `NULL`로 다시 돌아가려면 먼저 `NOT NULL` 제약을 삭제해야 합니다.
  end
end

모든 레코드가 수정되었는지 확인 (다음 릴리스)

프로덕션 데이터베이스의 subclone을 만들고 postgres.ai를 사용하여 GitLab.com의 모든 레코드가 설정된 속성을 가졌는지 확인합니다. 그렇지 않으면 새로운 유효하지 않은 레코드를 방지 단계로 돌아가 코드 경로에서 속성이 명시적으로 nil로 설정된 위치를 찾은 다음 코드 경로를 수정한 후 현재 레코드를 수정할 마이그레이션을 재스케줄하고 다음 릴리스를 위해 다음 단계를 수행하세요.

백그라운드 마이그레이션 완료 (다음 릴리스)

마이그레이션을 백그라운드 마이그레이션을 사용하여 수행한 경우 마이그레이션을 완료합니다.

모델에 유효성 추가 (다음 릴리스)

현재 및 새로운 레코드가 유효해야 하므로 해당 속성에 대한 모델에 유효성을 추가하세요.

class Epic < ApplicationRecord
  validates :description, presence: true
end

NOT NULL 제약 추가 (다음 릴리스)

NOT NULL 제약을 추가하여 전체 테이블을 스캔하고 각 레코드가 올바른지 확인합니다.

역시 우리의 예제에서, 다음 13.1 마일스톤에 대해 add_not_null_constraint 마이그레이션 도우미를 실행하여 최종 후속 배포 마이그레이션을 수행합니다:

class AddNotNullConstraintToEpicsDescription < Gitlab::Database::Migration[2.1]
  disable_ddl_transaction!

  def up
    # 이것은 `NOT NULL` 제약을 추가하고 유효성을 검사합니다.
    add_not_null_constraint :epics, :description
  end

  def down
    # `add_not_null_constraint`는 반환할 수 없으므로 필요하지 않습니다.
    remove_not_null_constraint :epics, :description
  end
end

대규모 테이블의 NOT NULL 제약

고트래픽 테이블(예: ci_buildsartifacts)의 NULL 가능한 열을 정리해야 하는 경우 백그라운드 마이그레이션을 위해 데이터 마이그레이션을 추가한 후에 여러 번의 릴리스가 필요하며 정리는 백그라운드 마이그레이션 후에 예약됩니다.

  1. 릴리스 N.M:
    • 기존 레코드를 수정하기 위해 백그라운드 마이그레이션을 추가합니다:

      # db/post_migrate/
      class QueueBackfillMergeRequestDiffsProjectId < Gitlab::Database::Migration[2.2]
        milestone '16.7'
        restrict_gitlab_migration gitlab_schema: :gitlab_main
      
        MIGRATION = 'BackfillMergeRequestDiffsProjectId'
        DELAY_INTERVAL = 2.minutes
      
        def up
          queue_batched_background_migration(
            MIGRATION,
            :merge_request_diffs,
            :id,
            job_interval: DELAY_INTERVAL
          )
        end
      
        def down
          delete_batched_background_migration(MIGRATION, :merge_request_diffs, :id, [])
        end
      end
      
  2. 릴리스 N.M+X, 여기서 X는 마이그레이션이 실행된 릴리스 수입니다:
    • 모든 기존 레코드가 수정되었는지 확인.

    • 백그라운드 마이그레이션 정리:

      # db/post_migrate/
      class FinalizeMergeRequestDiffsProjectIdBackfill < Gitlab::Database::Migration[2.2]
        disable_ddl_transaction!
        milestone '16.10'
        restrict_gitlab_migration gitlab_schema: :gitlab_main
      
        MIGRATION = 'BackfillMergeRequestDiffsProjectId'
      
        def up
          ensure_batched_background_migration_is_finished(
            job_class_name: MIGRATION,
            table_name: :merge_request_diffs,
            column_name: :id,
            job_arguments: [],
            finalize: true
          )
        end
      
        def down
          # no-op
        end
      end
      
    • NOT NULL 제약 추가:

      # db/post_migrate/
      class AddMergeRequestDiffsProjectIdNotNullConstraint < Gitlab::Database::Migration[2.2]
        disable_ddl_transaction!
        milestone '16.7'
      
        def up
          add_not_null_constraint :merge_request_diffs, :project_id
        end
      
        def down
          remove_not_null_constraint :merge_request_diffs, :project_id
        end
      end
      
    • 옵션. 매우 큰 테이블의 경우 유효하지 않은 NOT NULL 제약을 추가하고 비동기적으로 유효성 검사를 예약합니다:

      # db/post_migrate/
      class AddMergeRequestDiffsProjectIdNotNullConstraint < Gitlab::Database::Migration[2.2]
        disable_ddl_transaction!
        milestone '16.7'
      
        def up
          add_not_null_constraint :merge_request_diffs, :project_id, validate: false
        end
      
        def down
          remove_not_null_constraint :merge_request_diffs, :project_id
        end
      end
      
      # db/post_migrate/
      class PrepareMergeRequestDiffsProjectIdNotNullValidation < Gitlab::Database::Migration[2.2]
        milestone '16.10'
      
        CONSTRAINT_NAME = 'check_11c5f029ad'
      
        def up
          prepare_async_check_constraint_validation :merge_request_diffs, name: CONSTRAINT_NAME
        end
      
        def down
          unprepare_async_check_constraint_validation :merge_request_diffs, name: CONSTRAINT_NAME
        end
      end
      
  3. 옵션. 제약이 비동기적으로 검증되었을 경우, 유효성 검사가 완료된 후 NOT NULL 제약을 검증합니다:

    # db/post_migrate/
    class ValidateMergeRequestDiffsProjectIdNullConstraint < Gitlab::Database::Migration[2.2]
      milestone '16.10'
    
      def up
        validate_not_null_constraint :merge_request_diffs, :project_id
      end
    
      def down
        # no-op
      end
    

이러한 경우에는 업데이트 주기 초반에 데이터베이스 팀과 상의하세요. NOT NULL 제약이 필요하지 않을 수 있거나, 정말 큰 또는 빈번하게 접근되는 테이블에 영향을 주지 않는 다른 옵션이 있을 수 있습니다.