NOT NULL 제약 조건
GitLab v19.1NULL 값을 가져서는 안 되는 모든 속성은 데이터베이스에서 NOT NULL 칼럼으로 정의해야 합니다. 애플리케이션 로직에 따라, NOT NULL 칼럼에는 모델에 presence 유효성 검사를 정의하거나, 데이터베이스 정의의 일부로 기본값을 지정해야 합니다.
NULL 값을 가져서는 안 되는 모든 속성은 데이터베이스에서 NOT NULL 칼럼으로 정의해야 합니다.
애플리케이션 로직에 따라, NOT NULL 칼럼에는 모델에 presence 유효성 검사를 정의하거나, 데이터베이스 정의의 일부로 기본값을 지정해야 합니다.
예를 들어, 후자는 항상 non-NULL 값을 가져야 하지만 애플리케이션이 매번 강제할 필요 없는 잘 정의된 기본값이 있는 boolean 속성에 해당할 수 있습니다 (예: active=true).
belongs_to 연관 관계의 일부인 외래 키 칼럼의 경우, 별도의 presence: true 유효성 검사 대신 연관 관계에서 optional: false를 사용하는 것이 좋습니다. 이 방법이 의미적으로 더 올바르며 Rails의 내장 연관 관계 유효성 검사를 활용합니다. GitLab은 config/application.rb에 config.active_record.belongs_to_required_by_default = false가 설정되어 있으므로, belongs_to 연관 관계는 기본적으로 선택 사항이며 명시적으로 필수로 표시해야 합니다.
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에서 PostgreSQL 11이 최소 버전이 된 이후로, NULL 및/또는 기본값이 있는 칼럼 추가가 훨씬 쉬워졌으며, 모든 경우에 표준 add_column 헬퍼를 사용해야 합니다.
예를 들어, db_guides 테이블에 새로운 NOT NULL 칼럼 active를 추가하는 마이그레이션
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을 추가하려면 일반적으로 최소 두 개의 다른 릴리즈에 걸쳐 여러 단계가 필요합니다. 백그라운드 마이그레이션을 사용할 필요가 없을 만큼 테이블이 충분히 작다면, 이 모든 단계를 동일한 머지 리퀘스트에 포함할 수 있습니다. 트랜잭션 지속 시간을 줄이기 위해 별도의 마이그레이션을 사용하는 것을 권장합니다.
필요한 단계는 다음과 같습니다:
-
릴리즈
N.M(현재 릴리즈)애플리케이션 레벨에서 $ATTRIBUTE 값이 설정되고 있는지 확인합니다.
속성에 기본값이 있는 경우, 새 레코드에 기본값이 설정되도록 모델에 기본값을 추가합니다.
- 새 레코드와 기존 레코드 모두에 대해 속성이
nil로 설정되는 코드의 모든 위치를 업데이트합니다 (해당하는 경우).before_save및before_validation과 같은 ActiveRecord 콜백을 사용하는 경우에도, 일부 프로세스는 이러한 콜백을 건너뛸 수 있으므로 충분하지 않을 수 있습니다.update_column,update_columns, 그리고insert_all및update_all과 같은 대량 작업은 주의해야 할 메서드의 몇 가지 예시입니다.
- 새 레코드와 기존 레코드 모두에 대해 속성이
-
기존 레코드를 수정하기 위한 배포 후 마이그레이션을 추가합니다.
테이블 크기에 따라, 다음 릴리즈에서 정리를 위한 백그라운드 마이그레이션이 필요할 수 있습니다. 자세한 내용은 대규모 테이블의
NOT NULL제약 조건 섹션을 참조하세요. -
릴리즈
N.M+1(다음 릴리즈)GitLab.com의 모든 기존 레코드에 속성이 설정되어 있는지 확인합니다. 그렇지 않다면, 릴리즈
N.M의 1단계로 돌아갑니다.-
1단계가 정상적이고 릴리즈
N.M의 백필이 배치 백그라운드 마이그레이션을 통해 완료된 경우, 백그라운드 마이그레이션을 완료하는 배포 후 마이그레이션을 추가합니다. -
이제 모든 기존 및 새 레코드가 유효해야 하므로,
nil속성을 가진 레코드를 방지하기 위해 모델에 속성에 대한 유효성 검사를 추가합니다. -
NOT NULL제약 조건을 추가하는 배포 후 마이그레이션을 추가합니다.
-
예시#
특정 릴리즈 마일스톤(예: 13.0)을 고려해 보세요.
프로덕션 데이터베이스를 확인한 결과, NULL 설명을 가진 epics가 있으므로 하나의 단계에서 제약 조건을 추가하고 유효성 검사를 할 수 없습니다.
NULL 설명을 가진 에픽이 없더라도, 다른 GitLab 인스턴스에는 그러한 레코드가 있을 수 있으므로 어느 경우에나 동일한 프로세스를 따릅니다.
새로운 유효하지 않은 레코드 방지 (현재 릴리즈)#
속성이 nil로 설정되는 코드 경로가 있다면 모두 업데이트하여 새 레코드와 기존 레코드 모두에 대해 속성을 non-nil 값으로 설정합니다.
Rails attributes 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]
# With BATCH_SIZE=1000 and epics.count=29500 on GitLab.com
# - 30 iterations will be run
# - each requires on average ~150ms
# Expected total run time: ~5 seconds
BATCH_SIZE = 1000
disable_ddl_transaction!
class Epic < MigrationRecord
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 : can't go back to `NULL` without first dropping the `NOT NULL` constraint
end
end
모든 레코드가 수정되었는지 확인 (다음 릴리즈)#
postgres.ai를 사용하여 프로덕션 데이터베이스의 씬 클론을 생성하고 GitLab.com의 모든 레코드에 속성이 설정되어 있는지 확인합니다.
그렇지 않다면 새로운 유효하지 않은 레코드 방지 단계로 돌아가서 코드에서 속성이 명시적으로 nil로 설정되는 위치를 파악합니다. 코드 경로를 수정한 후 기존 레코드를 수정하기 위한 마이그레이션을 다시 예약하고 다음 단계를 수행할 다음 릴리즈를 기다립니다.
백그라운드 마이그레이션 완료 (다음 릴리즈)#
백그라운드 마이그레이션을 사용하여 마이그레이션이 완료된 경우 마이그레이션을 완료합니다.
모델에 유효성 검사 추가 (다음 릴리즈)#
이제 모든 기존 및 새 레코드가 유효해야 하므로, nil 속성을 가진 레코드를 방지하기 위해 모델에 속성에 대한 유효성 검사를 추가합니다.
belongs_to 연관 관계의 일부인 외래 키 칼럼의 경우, optional: false를 사용하는 것이 좋습니다:
class Epic < ApplicationRecord
belongs_to :group, optional: false
end
다음보다 선호됩니다:
class Epic < ApplicationRecord
belongs_to :group
validates :group, presence: true
end
일반 속성의 경우:
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
# This will add the `NOT NULL` constraint and validate it
add_not_null_constraint :epics, :description
end
def down
# Down is required as `add_not_null_constraint` is not reversible
remove_not_null_constraint :epics, :description
end
end
대규모 테이블의 NOT NULL 제약 조건#
트래픽이 많은 테이블 (예: ci_builds의 artifacts)에 대해 nullable 칼럼을 정리해야 하는 경우, 백그라운드 마이그레이션이 상당 시간 동안 실행될 수 있으며 데이터 마이그레이션 추가 후 다음 릴리즈에서 추가적인 배치 백그라운드 마이그레이션 정리가 필요합니다.
이 경우 릴리즈 수는 기존 레코드를 마이그레이션하는 데 필요한 시간에 따라 다릅니다. 정리는 백그라운드 마이그레이션이 완료된 후 예약되며, 이는 제약 조건이 추가된 후 여러 릴리즈가 지난 후일 수 있습니다.
-
릴리즈
N.M:기존 레코드를 수정하기 위한 백그라운드 마이그레이션을 추가합니다:
# db/post_migrate/
class QueueBackfillMergeRequestDiffsProjectId < Gitlab::Database::Migration[2.2]
milestone '16.7'
restrict_gitlab_migration gitlab_schema: :gitlab_main_org
MIGRATION = 'BackfillMergeRequestDiffsProjectId'
DELAY_INTERVAL = 2.minutes
def up
queue_batched_background_migration(
MIGRATION,
:merge_request_diffs,
:id
)
end
def down
delete_batched_background_migration(MIGRATION, :merge_request_diffs, :id, [])
end
end
-
릴리즈
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_org
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.10'
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.10'
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
- 선택 사항. 파티션 테이블의 경우 다음을 사용합니다:
# db/post_migrate/
PARTITIONED_TABLE_NAME = :p_ci_builds
CONSTRAINT_NAME = 'check_9aa9432137'
# Partitioned check constraint to be validated in https://gitlab.com/gitlab-org/gitlab/-/issues/XXXXX
def up
prepare_partitioned_async_check_constraint_validation PARTITIONED_TABLE_NAME, name: CONSTRAINT_NAME
end
def down
unprepare_partitioned_async_check_constraint_validation PARTITIONED_TABLE_NAME, name: CONSTRAINT_NAME
end
prepare_partitioned_async_check_constraint_validation은 모든 파티션에 대해 기존의 NOT VALID 체크 제약 조건만 비동기적으로 유효성 검사합니다.
파티션 테이블에 대한 체크 제약 조건을 생성하거나 유효성 검사하지 않습니다.
-
선택 사항. 제약 조건이 비동기적으로 유효성 검사된 경우, 유효성 검사가 완료되면
NOT NULL제약 조건을 유효성 검사합니다:Database Lab을 사용하여 유효성 검사가 성공했는지 확인합니다.
\d+ table_name명령을 실행하고 체크 제약 조건 정의에서NOT VALID가 제거되었는지 확인합니다. -
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
end
이러한 경우, 업데이트 주기 초반에 데이터베이스 팀과 상의하세요. NOT NULL 제약 조건이 필요하지 않을 수도 있고, 정말 크거나 자주 액세스되는 테이블에 영향을 주지 않는 다른 옵션이 있을 수도 있습니다.
여러 칼럼에 대한 NOT NULL 제약 조건#
때로는 특정 수의 NOT NULL 값을 포함하는 칼럼 집합을 보장하고 싶을 때가 있습니다. 일반적인 예시는 프로젝트나 그룹 중 하나에 속할 수 있는 테이블로, 이 경우 project_id 또는 group_id가 반드시 존재해야 합니다. 이를 적용하려면 위의 사용 사례에 맞는 단계를 따르되, add_multi_column_not_null_constraint 헬퍼를 사용합니다.
이 예시에서, labels는 프로젝트나 그룹 중 하나에 속해야 하지만 둘 다에 속할 수는 없습니다. 이를 적용하기 위해 체크 제약 조건을 추가할 수 있습니다:
class AddLabelsNullConstraint < Gitlab::Database::Migration[2.2]
disable_ddl_transaction!
milestone '16.10'
def up
add_multi_column_not_null_constraint(:labels, :group_id, :project_id)
end
def down
remove_multi_column_not_null_constraint(:labels, :group_id, :project_id)
end
end
이렇게 하면 labels에 다음 제약 조건이 추가됩니다:
CREATE TABLE labels (
...
CONSTRAINT check_45e873b2a8 CHECK ((num_nonnulls(group_id, project_id) = 1))
);
num_nonnulls는 제공된 인수 중 non-null인 수를 반환합니다. 제약 조건에서 이 값이 1과 같은지 확인하면 행에서 group_id와 project_id 중 하나만 non-null 값을 포함해야 하고 둘 다 포함해서는 안 된다는 의미입니다.
커스텀 한도 및 연산자#
필요한 non-null 수를 사용자 정의하려면 다른 limit 및/또는 operator를 사용할 수 있습니다:
class AddLabelsNullConstraint < Gitlab::Database::Migration[2.2]
disable_ddl_transaction!
milestone '16.10'
def up
add_multi_column_not_null_constraint(:labels, :group_id, :project_id, limit: 0, operator: '>')
end
def down
remove_multi_column_not_null_constraint(:labels, :group_id, :project_id)
end
end
이는 project_id와 group_id 모두 존재할 수 있도록 허용하는 제약 조건에 반영됩니다:
CREATE TABLE labels (
...
CONSTRAINT check_45e873b2a8 CHECK ((num_nonnulls(group_id, project_id) > 0))
);
기존 테이블의 칼럼에서 NOT NULL 제약 조건 제거#
기존 데이터베이스 칼럼에서 NOT NULL 제약 조건을 제거하려면 다단계 마이그레이션 프로세스가 필요합니다:
-
NOT NULL제약 조건을 삭제하는 스키마 마이그레이션. -
잠재적 롤백 후 데이터 무결성을 보장하기 위한 별도의 데이터 마이그레이션. 이 마이그레이션은 다음 중 하나를 수행할 수 있습니다:
유효하지 않은 레코드를 삭제합니다.
- 유효하지 않은 레코드를 기본값으로 업데이트합니다.
데이터 수정(DML)과 스키마 변경(DDL)을 하나의 마이그레이션에 결합하는 것은 허용되지 않으므로 여러 마이그레이션이 필요합니다.
칼럼에 체크 제약 조건이 있는 경우 NOT NULL 제약 조건 제거#
먼저, 칼럼에 제약 조건이 있는지 확인합니다. 여러 가지 방법으로 확인할 수 있습니다:
-
Rails 콘솔에서
Gitlab::Database::PostgresConstraint뷰 쿼리 -
psql을 사용하여 테이블 자체 확인:\d+ table_name -
structure.sql확인:
CREATE TABLE labels (
...
CONSTRAINT check_061f6f1c91 CHECK ((project_view IS NOT NULL))
);
예시#
마일스톤 번호는 예시일 뿐입니다. 올바른 버전을 사용하세요.
# frozen_string_literal: true
class DropNotNullConstraintFromLabelsProjectView< Gitlab::Database::Migration[2.2]
disable_ddl_transaction!
milestone '16.7'
def up
remove_not_null_constraint :labels, :project_view
end
def down
add_not_null_constraint :labels, :project_view
end
end
# frozen_string_literal: true
class CleanupRecordsWithNullProjectViewValuesFromLabels < Gitlab::Database::Migration[2.2]
disable_ddl_transaction!
milestone '16.7'
BATCH_SIZE = 1000
class Label < MigrationRecord
include EachBatch
self.table_name = 'labels'
end
def up
# no-op - this migration is required to allow a rollback of `DropNotNullConstraintFromLabelsProjectView`
end
def down
Label.each_batch(of: BATCH_SIZE) do |relation|
relation.
where('project_view IS NULL').
delete_all
end
end
end
칼럼에 체크 제약 조건이 없는 경우 NOT NULL 제약 조건 제거#
NOT NULL이 체크 제약 조건 없이 칼럼에만 정의되어 있다면 change_column_null을 사용할 수 있습니다.
structure.sql의 예시:
CREATE TABLE labels (
...
projects_limit integer NOT NULL
);
예시#
마일스톤 번호는 예시일 뿐입니다. 올바른 버전을 사용하세요.
# frozen_string_literal: true
class DropNotNullConstraintFromLabelsProjectsLimit < Gitlab::Database::Migration[2.2]
milestone '16.7'
def up
change_column_null :labels, :projects_limit, true
end
def down
change_column_null :labels, :projects_limit, false
end
end
# frozen_string_literal: true
class CleanupRecordsWithNullProjectsLimitValuesFromLabels < Gitlab::Database::Migration[2.2]
disable_ddl_transaction!
milestone '16.7'
BATCH_SIZE = 1000
class Label < MigrationRecord
include EachBatch
self.table_name = 'labels'
end
def up
# no-op - this migration is required to allow a rollback of `DropNotNullConstraintFromLabelsProjectsLimit`
end
def down
Label.each_batch(of: BATCH_SIZE) do |relation|
relation.
where('projects_limit IS NULL').
delete_all
end
end
end
파티션 테이블에서 NOT NULL 제약 조건 제거#
중요 참고 사항: 모든 파티션이 부모 테이블로부터 제약 조건을 상속받으므로, 부모 테이블에 제약 조건이 존재하는 경우 개별 파티션에서 NOT NULL 제약 조건을 삭제할 수 없습니다. 이러한 이유로, 모든 자식 파티션으로 연쇄되는 부모 테이블에서 제약 조건을 삭제해야 합니다.