기존 열에 외래 키 제약 조건 추가하기

외래 키는 관련된 데이터베이스 테이블 간의 일관성을 보장합니다. 현재 데이터베이스 검토 과정은 다른 테이블에서 레코드를 참조하는 테이블을 만들 때 항상 외래 키를 추가하도록 권장합니다.

Rails 버전 4부터는 Rails에 데이터베이스 테이블에 외래 키 제약 조건을 추가하기 위한 마이그레이션 도우미가 포함되어 있습니다. Rails 4 이전에는 연관 정의에서 dependent 옵션을 사용하는 것이 일관성을 보장하는 유일한 방법이었습니다. 응용 프로그램 수준에서 데이터 일관성을 보장하는 것이 경우에 따라 실패할 수 있으므로 테이블에 일관성을 보장하기 위한 프레임워크 지원이 없었던 경우에는 테이블의 데이터 불일치가 발생할 수 있습니다. 이는 주로 오래된 테이블에 영향을 미칩니다.

기존 데이터베이스 열에 외래 키를 추가하려면 데이터베이스 구조 변경과 잠재적 데이터 변경이 필요합니다. 테이블이 사용 중인 경우 일관성이 없는 데이터가 있다고 가정해야 합니다.

기존 열에 외래 키 제약 조건을 추가하려면:

  1. GitLab 버전 N.M: NOT VALID 외래 키 제약 조건을 해당 열에 추가하여 일관성이 없는 레코드가 GitLab에서 생성되지 않도록 합니다.
  2. GitLab 버전 N.M: 기존 레코드를 수정하거나 정리하기 위한 데이터 마이그레이션을 추가합니다.
  3. GitLab 버전 N.M+1: 외래 키를 VALID로 만들어 전체 테이블을 확인합니다.

예제

다음과 같은 테이블 구조를 고려해보세요.

users 테이블:

  • id (정수, 기본 키)
  • name (문자열)

emails 테이블:

  • id (정수, 기본 키)
  • user_id (정수)
  • email (문자열)

ActiveRecord에서 관계 표현:

class User < ActiveRecord::Base
  has_many :emails
end

class Email < ActiveRecord::Base
  belongs_to :user
end

문제: 사용자가 삭제되면 삭제된 사용자와 관련된 이메일 레코드가 emails 테이블에 남아 있습니다.

user = User.find(1)
user.destroy

emails = Email.where(user_id: 1) # 삭제된 사용자에 대한 이메일을 반환합니다

잘못된 레코드 방지

레코드 변경에 대한 일관성을 강제하는 NOT VALID 외래 키 제약 조건을 테이블에 추가합니다.

위의 예에서 emails 테이블에서 레코드를 업데이트할 수는 있지만, user_id를 존재하지 않는 값으로 업데이트하려고하면 제약 조건으로 인해 데이터베이스 오류가 발생합니다.

NOT VALID 외래 키를 추가하는 마이그레이션 파일:

class AddNotValidForeignKeyToEmailsUser < Gitlab::Database::Migration[2.1]
  def up
    add_concurrent_foreign_key :emails, :users, column: :user_id, on_delete: :cascade, validate: false
  end
  
  def down
    remove_foreign_key_if_exists :emails, column: :user_id
  end
end

검증 없이 외래 키를 추가하는 것은 빠른 작업입니다. 새 데이터에 제약 조건을 적용하기 위해 테이블을 잠시 잠그기만 하면 됩니다. 고트랙 및 대용량 테이블에 대한 잠금 재시도를 활성화해야 합니다. add_concurrent_foreign_key가 이를 처리하고 외래 키가 이미 있는지 확인합니다.

caution
소스 및 대상 테이블이 동일하지 않은 경우, 마이그레이션 파일 당 add_foreign_key 또는 add_concurrent_foreign_key 제약 조건을 두 번 이상 사용하면 안됩니다.

기존 레코드 정리 데이터 마이그레이션

이 접근 방식은 데이터 양과 정리 전략에 따라 다릅니다. 데이터베이스 쿼리를 사용하여 “잘못된” 레코드를 찾을 수 있고 레코드 수가 많지 않다면, 데이터 마이그레이션을 Rails 마이그레이션에서 실행할 수 있습니다.

데이터 양이 많은 경우(> 1000 레코드), 백그라운드 마이그레이션을 만드는 것이 좋습니다. 확실하지 않은 경우 데이터베이스 팀에 조언을 구하세요.

emails 테이블에서 레코드를 정리하기 위한 데이터베이스 마이그레이션 예제:

class RemoveRecordsWithoutUserFromEmailsTable < Gitlab::Database::Migration[2.1]
  disable_ddl_transaction!
  
  class Email < ActiveRecord::Base
    include EachBatch
  end
  
  def up
    Email.where('user_id NOT IN (SELECT id FROM users)').each_batch do |relation|
      relation.delete_all
    end
  end
  
  def down
    # 데이터 일관성이 응용 프로그램의 배포 전 및 후 버전에 영향을 미치지 않을 경우 no-op일 수 있습니다.
    # 이 경우, `emails` 테이블에 레코드는 더 이상 `users` 테이블의 연결된 레코드가 없을 수 있습니다.
  end
end

외래 키 검증

외래 키 검증은 전체 테이블을 스캔하고 각 관계가 올바른지 확인합니다. 다행히도 실행 중에 소스 테이블인 users는 잠그지 않습니다.

note
일괄 배경 마이그레이션을 사용할 때 외래 키 검증은 다음 GitLab 릴리즈에서 수행되어야 합니다.

외래 키를 검증하기 위한 마이그레이션 파일:

# frozen_string_literal: true

class ValidateForeignKeyOnEmailUsers < Gitlab::Database::Migration[2.1]
  def up
    validate_foreign_key :emails, :user_id
  end
  
  def down
    # 불일치하는 데이터를 롤백하지 않는 경우 안전한 no-op일 수 있습니다.
  end
end

비동기로 외래 키 검증

매우 큰 테이블의 경우 외래 키 검증은 여러 시간에 걸쳐서 실행하는 것이 도전적일 수 있습니다. autovacuum과 같은 필요한 데이터베이스 작업을 실행할 수 없으며, GitLab.com에서는 마이그레이션이 완료될 때까지 배포 프로세스가 차단됩니다.

GitLab.com에 영향을 최소화하기 위해 주말 시간에 외래 키 검증을 비동기로 실행하는 프로세스가 있습니다. 일반적으로 트래픽이 적고 배포 횟수도 적기 때문에 이때 외래 키 검증을 수행할 수 있습니다.

저영향 시간에 외래 키 검증 예약

  1. 검증할 외래 키 예약.
  2. MR이 배포되었고 운영 환경에서 외래 키가 유효함을 확인.
  3. 외래 키를 동기적으로 검증하는 마이그레이션 추가.

검증할 외래 키 예약

  1. 비동기적으로 외래 키를 준비하는 사후 배포 마이그레이션을 포함하는 MR을 생성하여 외래 키를 준비합니다.
  2. 외래 키를 동기적으로 검증하는 마이그레이션을 추가하는 후속 이슈를 생성합니다.
  3. 비동기 외래 키를 준비하는 MR에 후속 이슈를 언급하는 코멘트를 추가합니다.

비동기 도우미를 사용하여 외래 키를 검증하는 예제:

# in db/post_migrate/

FK_NAME = :fk_be5624bf37

# TODO: 이슈나 MR에서 외래 키를 동기적으로 검증할 것
def up
  # `some_column`는 열의 배열일 수 있으며, `name`이 제공된 경우 필수가 아닙니다.
  # `name`이 다른 인수보다 우선합니다.
  prepare_async_foreign_key_validation :ci_builds, :some_column, name: FK_NAME
  
  # 파티셔닝된 테이블의 경우:
  # prepare_partitioned_async_foreign_key_validation :p_ci_builds, :some_column, name: FK_NAME
end

def down
  unprepare_async_foreign_key_validation :ci_builds, :some_column, name: FK_NAME
  
  # 파티셔닝된 테이블의 경우:
  # unprepare_partitioned_async_foreign_key_validation :p_ci_builds, :some_column, name: FK_NAME
end

MR 배포가 확인되고 FK가 프로덕션에서 유효한지 확인합니다.

  1. /chatops run auto_deploy status <merge_sha>를 사용하여 GitLab.com에서 챗옵스를 이용하여 포스트-디플로이 마이그레이션이 실행되었는지 확인합니다. 출력 결과가 db/gprd로 나오면, 포스트-디플로이 마이그레이션이 프로덕션 데이터베이스에서 실행된 것입니다. 자세한 정보는 GitLab.com에서 포스트-디플로이 마이그레이션이 실행되었는지 확인하는 방법을 참조하세요.
  2. FK가 주말에 유효화되도록 다음 주까지 기다립니다.
  3. 검증이 성공적으로 이루어졌는지 확인하기 위해 Database Lab를 사용하세요. 출력 결과가 외래 키가 NOT VALID로 표시되지 않도록 확인합니다.

FK를 동기적으로 검증하는 마이그레이션 추가

프로덕션 데이터베이스에서 외래 키가 유효해진 후, 외래 키를 동기적으로 유효화하는 두 번째 Merge Request을 생성하세요. 스키마 변경 내용은 이 두 번째 Merge Request의 structure.sql에 업데이트되고 커밋되어야 합니다. 동기적 마이그레이션은 GitLab.com에서 무효 조치가 되지만, 여전히 다른 설치에 대한 기대에 따라 마이그레이션을 추가해야 합니다. 아래 블록에서는 이전 비동기적 예제에 대한 두 번째 마이그레이션을 생성하는 방법을 보여줍니다.

caution
두 번째 마이그레이션을 validate_foreign_key로 Merge하기 전에 프로덕션에서 외래 키가 유효한지 확인하세요. 두 번째 마이그레이션이 실행되기 전에 검증이 실행되지 않은 경우 두 번째 마이그레이션이 실행될 때 외래 키가 동기적으로 유효화됩니다.
# in db/post_migrate/
  
  FK_NAME = :fk_be5624bf37
  
  def up
    validate_foreign_key :ci_builds, :some_column, name: FK_NAME
  end
  
  def down
    # 일관성 없는 데이터를 롤백하지 않는 경우 안전한 노-옵(no-op)입니다.
  end
end

지역 데이터베이스 FK 변경 테스트

Merge Request을 생성하기 전에 지역 환경에서 데이터베이스 외래 키 변경을 테스트해야 합니다.

비동기적으로 검증된 외래 키 확인

외래 키를 검증하는 비동기적 헬퍼를 로컬 환경에서 사용하여 변경 내용을 테스트하세요:

  1. Rails 콘솔에서 Feature.enable(:database_async_foreign_key_validation)을 실행하여 피처 플래그를 활성화합니다.
  2. bundle exec rails db:migrate를 실행하여 비동기 검증 테이블에 항목을 만듭니다.
  3. bundle exec rails gitlab:db:validate_async_constraints:all을 실행하여 모든 데이터베이스에서 외래 키가 비동기적으로 유효화되도록합니다.
  4. 외래 키를 확인하기 위해 GDK 명령어인 gdk psql을 사용하여 PostgreSQL 콘솔을 열고 명령어 \d+ table_name을 실행하여 외래 키가 유효한지 확인합니다. 성공적인 유효화는 외래 키 정의에서 NOT VALID를 제거합니다.