MySQL에서 PostgreSQL로의 마이그레이션 가이드

plans-img 모든 플랜 에서 사용 가능

deployment-img self-hosted 배포판

매터모스트 v8.0부터는 매터모스트의 성능 및 기능을 향상시키기 위해 PostgreSQL을 사용하는 것이 우리의 선택 사항입니다. MySQL 데이터베이스에서 마이그레이션에 관심이 있는 커뮤니티 회원을 지원하는 것의 중요성을 인지하여, 우리는 선제적인 조치를 취하여 마이그레이션에 대한 지침과 모베스트 프랙티스를 제공하고 있습니다.

마이그레이션 프로세스를 간소화하고 잠재적인 어려움을 완화하기 위해, 원활한 전환을 촉진하기 위해 포괄적인 가이드 세트를 준비했습니다. 추가로, 마이그레이션 노력을 단순화하는 데 효과적으로 입증된 다양한 도구에 대한 권장 사항을 제공하고자 합니다.

Note

이러한 지침은 개발 중에 있으며, 마이그레이션 프로세스를 간소화하기 위해 노력하고 있습니다.이 가이드를 시작점으로 사용하고 마이그레이션을 시작하기 전에 데이터베이스를 항상 백업하십시오.

필요한 도구

Note

  • MySQL v8을 사용하는 경우: pgLoader 컴파일된 이진 파일에 알려진 버그 가 있는 경우, 소스에서 pgLoader를 컴파일해야 합니다. 소스에서 빌드하는 단계는 여기 를 참조하십시오.

  • pgloader Docker 이미지가 메모리 리소스에 제한을 받을 수 있음을 보고받았습니다. Docker 컨테이너 대신 pgloader를 직접 사용하십시오.

  • 다음 명령을 실행하여 morph CLI를 설치하십시오:

    • go install github.com/mattermost/morph/cmd/morph@v1

  • 마이그레이션 후 데이터 비교를 위해선 dbcmp 를 선택적으로 설치하십시오:

    • go install github.com/mattermost/dbcmp/cmd/dbcmp@latest

마이그레이션 전

Note

이 가이드는 v6.4 또는 이후의 스키마를 요구합니다. 따라서, 이전 버전을 사용하고 있다면 마이그레이션을 계획 중이라면, 매터모스트 서버를 최소한 v6.4로 업데이트하십시오.

  • MySQL 데이터를 백업하십시오.

  • 매터모스트 버전을 확인하십시오. 자세한 내용은 About 모달을 참조하십시오.

  • 필요한 마이그레이션 윈도우를 결정하십시오. 이 프로세스는 마이그레이션 중에 매터모스트 서버를 중지해야 합니다.

  • 스키마 간 데이터 호환성을 보장하기 위해 스키마 차이점 섹션을 참조하십시오.

  • 데이터베이스 준비 문서를 참조하여 PostgreSQL 환경을 준비하십시오.

  • 반복적인 마이그레이션(여러 번 pgLoader를 실행하는 것)을 실행하려는 경우, :ref: 반복적 마이그레이션 섹션을 참조하십시오.

대상 데이터베이스 준비

  • 특정 버전을 위해 mattermost 저장소를 복제하십시오:

    git clone -b <현재 버전 (예: release-7.8)> git@github.com:mattermost/mattermost.git --depth=1

  • mattermost 프로젝트로 이동하십시오.

  • 다음 명령을 사용하여 morph CLI를 통해 PostgreSQL 데이터베이스를 생성하십시오:

morph apply up --driver postgres --dsn "postgres://user:pass@localhost:5432/ <대상_db_이름>?sslmode=disable" --path ./db/migrations/postgres --number -1

* v8 이후 프로젝트 재구성으로, 마이그레이션 디렉터리는 ./server/channels/db/migrations/postgres/ 로 변경되었습니다. 따라서, mattermost/server/channels 로 이동하십시오.

스키마 차이점

마이그레이션 전에 두 스키마 간의 차이로 인해 오류 없는 마이그레이션을 위해 수동 단계가 필요할 수 있습니다.

텍스트에서 character varying로

매터모스트 MySQL 스키마가 PostgreSQL 스키마에 대한 varchar 표현 대신 다양한 테이블에서 text 열 유형을 사용하기 때문에 PostgreSQL 스키마의 크기가 제한되어 있는지 확인하는 것을 권장합니다.

테이블

데이터 유형 캐스팅

Audits

Action

text -> varchar(512)

Audits

ExtraInfo

text -> varchar(1024)

ClusterDiscovery

HostName

text -> varchar(512)

Commands

IconURL

text -> varchar(1024)

Commands

AutoCompleteDesc

text -> varchar(1024)

Commands

AutoCompleteHint

text -> varchar(1024)

Compliances

Keywords

text -> varchar(512)

Compliances

Emails

text -> varchar(1024)

FileInfo

Path

text -> varchar(512)

FileInfo

ThumbnailPath

text -> varchar(512)

FileInfo

PreviewPath

text -> varchar(512)

FileInfo

Name

text -> varchar(256)

FileInfo

MimeType

text -> varchar(256)

LinkMetadata

URL

text -> varchar(2048)

RemoteClusters

SiteURL

text -> varchar(512)

RemoteClusters

Topics

text -> varchar(512)

Sessions

DeviceId

text -> varchar(512)

Systems

Value

text -> varchar(1024)

UploadSessions

FileName

text -> varchar(256)

UploadSessions

Path

text -> varchar(512)

위와 같이, 스키마가 다를 수 있고 PostgreSQL 스키마의 데이터 크기 제약으로 인해 오류가 발생할 수 있습니다. 커뮤니티에서 LinkMetadataFileInfo 테이블에 일부 오버플로우가 있었다는 보고를 받았으므로, 이러한 특히 테이블을 확인하는 것을 권장합니다. MySQL 스키마에서 데이터가 이러한 제한을 초과하는지 확인하십시오. 필요한 삭제 항목이 있는지 확인하십시오. 예를 들어, Audits 테이블/ Action 열에서 이것을 확인하려면:

DELETE FROM mattermost.Audits where LENGTH(Action) > 512;

전체 텍스트 인덱스

maximun token length의 제한 을 초과할 수 있는 PostsFileInfo 테이블의 일부 단어가 있을 수 있습니다. 이러한 경우, PostgreSQL 스키마에서 idx_posts_message_txtidx_fileinfo_content_txt 인덱스를 삭제하고, 마이그레이션이 완료된 후 다음 쿼리를 실행하여 이러한 인덱스를 재작성하는 것을 권장합니다:

마이그레이션 전에 인덱스를 삭제하려면 다음 명령을 실행하십시오. 이 명령문은 스크립트에 포함되어 있지만 오류를 방지하기 위해 이를 수동으로 실행하는 것을 권장합니다:

DROP INDEX IF EXISTS idx_posts_message_txt;
DROP INDEX IF EXISTS idx_fileinfo_content_txt;

또한 마이그레이션이 완료된 후 이러한 인덱스가 다시 생성되었는지 확인하십시오. 이러한 인덱스를 다시 생성하려면 다음 쿼리를 간단히 실행하십시오:

CREATE INDEX IF NOT EXISTS idx_posts_message_txt ON {{ .source_schema }}.posts USING gin(to_tsvector('english', message));
CREATE INDEX IF NOT EXISTS idx_fileinfo_content_txt ON {{ .source_schema }}.fileinfo USING gin(to_tsvector('english', content));

Note

PostsFileInfo 테이블의 항목 중 일부가 제한을 초과하는 경우, pgloader 는 이러한 인덱스를 작성하려는 동안 ERROR:  string is too long for tsvector 오류로 실패할 수 있습니다. 이러한 명령문을 구성에서 제거해야 합니다.

Artifacts may remain from previous configurations/versions

이전에는 v6.4 전에, Mattermost가 스키마 이관을 처리하기 위해 golang-migrate 를 사용했습니다. 이제 더 이상 사용하지 않기 때문에 schema_migrations 테이블을 제외합니다. 만약 v6.4 이전에 Mattermost를 사용했다면, 이 테이블을 삭제하고 비교에서도 제외해야 합니다.

DROP TABLE mattermost.schema_migrations;

또한, 이전에 Mattermost 설정을 처리하기 위해 데이터베이스를 이용했다면, 해당 테이블들을 MySQL 데이터베이스에서 삭제해야 합니다. 테이블을 삭제하기 위해 다음의 DDL을 실행하는 것을 고려해보세요.

DROP TABLE ConfigurationFiles;
DROP TABLE Configurations;
DROP TABLE db_config_migrations;

커뮤니티 구성원 중 일부는 SharedChannelRemotes 테이블에 descriptionnextsyncat 열이 있다고보고했습니다. 이러한 열을 테이블에서 삭제해야 합니다. 다음의 DDL을 실행하여 열을 삭제하는 것을 고려해보세요. (이 마이그레이션은 Mattermost의 향후 버전에 추가될 것입니다).

ALTER TABLE SharedChannelRemotes DROP COLUMN description, DROP COLUMN nextsyncat;

데이터 이관

원하는 상태로 스키마를 설정한 후, pgLoader 를 실행하여 데이터 를 이관할 수 있습니다.

데이터 이관의 기준으로 다음 구성을 사용하십시오.

LOAD DATABASE
     FROM      mysql://{{ .mysql_user }}:{{ .mysql_password }}@mysql:3306/{{ .source_schema }}
     INTO      pgsql://{{ .pg_user }}:{{ .pg_password }}@postgres:5432/{{ .target_schema }}

데이터만,
     workers = 8, concurrency = 1,
     쓰레드 당 여러 리더, 범위 당 행 수 = 50000,
     테이블 생성 없음, 인덱스 생성 없음,
     인덱스 이름 유지

PostgreSQL 매개변수 설정
     maintenance_work_mem를 '128MB'로, work_mem을 '12MB'로

MySQL 매개변수 설정
      net_read_timeout를 '120'으로,
      net_write_timeout를 '120'으로

채널의 유형을 "channel_type"으로 변경, typemod 삭제,
     팀의 유형을 "team_type"으로 변경, typemod 삭제,
     업로드 세션의 유형을 "upload_session_type"으로 변경, typemod 삭제,
     드래프트의 우선 순위를 텍스트로 변경,
     precision이 11일 때 int 유형을 정수로 변경, typemod 삭제,
     precision이 20일 때 bigint 유형을 bigint로 변경, typemod 삭제,
     텍스트 유형을 varchar로 변경, typemod 삭제,
     precision이 4보다 작거나 같을 때 tinyint 유형을 boolean으로 변경, tinyint-to-boolean 사용,
     json 유형을 jsonb로 변경, typemod 삭제

~ <IR_>, ~ <focalboard>, schema_migrations 패턴과 일치하는 테이블 이름 제외

로드되기 전에 실행
     $$ ALTER SCHEMA public RENAME TO {{ .source_schema }}; $$,
     $$ IF EXISTS idx_posts_message_txt를 DROP INDEX; $$,
     $$ IF EXISTS idx_fileinfo_content_txt를 DROP INDEX; $$

로드된 후에 실행
     $$ {{ .source_schema }}.db_migrations의 버전이 92인 경우에 이름을 'add_createat_to_teamembers'로 변경하십시오; $$,
     $$ IF NOT EXISTS idx_posts_message_txt를 CREATE INDEX; $$,
     $$ IF NOT EXISTS idx_fileinfo_content_txt를 CREATE INDEX; $$,
     $$ {{ .source_schema }}의 스키마를 public으로 변경하십시오; $$,
     $$ pg_catalog.set_config('search_path', '"$user", public', false) 를 선택하십시오. $$,
     $$ {{ .pg_user }}의 SEARCH_PATH를 'public'로 변경하십시오. $$;

이 구성 파일을 저장한 후, 예를 들어 migration.load 로 명명하고 다음 명령을 사용하여 pgLoader 를 실행할 수 있습니다.

pgLoader migration.load > migration.log

이 마이그레이션에 대한 결과를 기고하거나 마이그레이션 과정에서 발견한 내용을 보고해 주시기 바랍니다.

데이터 비교

저희는 두 데이터베이스의 내용을 비교하는 프로세스를 간소화하기 위해 내부적으로 도구를 개발했습니다. dbcmp 도구는 모든 테이블을 비교하고 두 개의 스키마 사이에 어떤 이질성이 있는지 보고합니다.

비교를 실행하기 위한 몇 가지 플래그가 포함되어 있습니다.

사용법:
  dbcmp [flags]

플래그:
      --exclude strings   비교에서 테이블을 제외하고, 쉼표로 구분된 값이 필요합니다.
  -h, --help              dbcmp에 대한 도움말
      --source string     소스 데이터베이스 dsn
      --target string     대상 데이터베이스 dsn
  -v, --version           dbcmp의 버전

우리의 경우, 다음 명령을 간단히 실행할 수 있습니다:

dbcmp --source "${MYSQL_DSN}" --target "${POSTGRES_DSN}" --exclude="db_migrations,ir_,focalboard,systems"

이 마이그레이션 가이드는 Mattermost 제품을 위한 테이블만 다루고 있음을 유의하십시오.

또한, 이번 마이그레이션에서 작은 차이 (단일 마이그레이션 이름의 오타)가 발생하면 PostgreSQL 스키마를 만들고 공식 mattermost 출처를 통해 만들었기 때문에 안전하게 건너뛸 수 있는 db_migrations 테이블을 제외하고 있습니다. 반면, systems 테이블은 일부 마이그레이션 중에 추가 키가 추가되었을 경우 추가적인 차이를 나타낼 수 있습니다. 이러한 이유로 systems 테이블을 제외하고, 상대적으로 크기가 작은 systems 테이블의 데이터 비교를 수동으로 실행하십시오.

플러그인 마이그레이션

플러그인 측면에서는, 이전에 수행한 작업과는 다른 방식으로 접근할 것입니다. 이번에는 morph 도구를 사용하여 테이블 및 인덱스를 생성하지 않을 것입니다. BoardsPlaybooks 는 SQL 쿼리를 용이하게하기 위해 응용 프로그램 로직을 활용하고 있습니다. 그러나 이 시점에서 어떠한 수준의 응용 프로그램도 사용하고 싶지 않습니다.

Playbooks

pgloader 구성은 v1.38.1 기반으로 하며 플러그인은 최소한 v1.36.0 이어야 마이그레이션을 수행할 수 있습니다.

마이그레이션을 시작할 준비가 되었다면, pgLoader 를 실행하여 스키마데이터 를 마이그레이션할 수 있습니다.

데이터 마이그레이션의 기준으로 다음 구성을 사용하십시오.

LOAD DATABASE
     FROM      mysql://{{ .mysql_user }}:{{ .mysql_password }}@mysql:3306/{{ .source_schema }}
     INTO      pgsql://{{ .pg_user }}:{{ .pg_password }}@postgres:5432/{{ .target_schema }}

WITH include drop, create tables, create indexes, no foreign keys,
    workers = 8, concurrency = 1,
    multiple readers per thread, rows per range = 50000,
    preserve index names

SET PostgreSQL PARAMETERS
    maintenance_work_mem to '128MB',
    work_mem to '12MB'

SET MySQL PARAMETERS
    net_read_timeout  = '120',
    net_write_timeout = '120'

CAST column IR_ChannelAction.ActionType to text drop typemod,
     column IR_ChannelAction.TriggerType to text drop typemod,
     column IR_Incident.ChecklistsJSON to "json" drop typemod

INCLUDING ONLY TABLE NAMES MATCHING
    ~/IR_/

BEFORE LOAD DO
    $$ ALTER SCHEMA public RENAME TO {{ .source_schema }}; $$

AFTER LOAD DO
    $$ ALTER TABLE {{ .source_schema }}.IR_ChannelAction ALTER COLUMN ActionType TYPE varchar(65536); $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_ChannelAction ALTER COLUMN TriggerType TYPE varchar(65536); $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Incident ALTER COLUMN ReminderMessageTemplate TYPE varchar(65536); $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Incident ALTER COLUMN ReminderMessageTemplate SET DEFAULT ''::text;  $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Incident ALTER COLUMN ConcatenatedInvitedUserIDs TYPE varchar(65536); $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Incident ALTER COLUMN ConcatenatedInvitedUserIDs SET DEFAULT ''::text; $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Incident ALTER COLUMN ConcatenatedWebhookOnCreationURLs TYPE varchar(65536); $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Incident ALTER COLUMN ConcatenatedWebhookOnCreationURLs SET DEFAULT ''::text; $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Incident ALTER COLUMN ConcatenatedInvitedGroupIDs TYPE varchar(65536); $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Incident ALTER COLUMN ConcatenatedInvitedGroupIDs SET DEFAULT ''::text; $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Incident ALTER COLUMN Retrospective TYPE varchar(65536); $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Incident ALTER COLUMN Retrospective SET DEFAULT ''::text; $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Incident ALTER COLUMN MessageOnJoin TYPE varchar(65536); $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Incident ALTER COLUMN MessageOnJoin SET DEFAULT ''::text; $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Incident ALTER COLUMN ConcatenatedWebhookOnStatusUpdateURLs TYPE varchar(65536); $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Incident ALTER COLUMN ConcatenatedWebhookOnStatusUpdateURLs SET DEFAULT ''::text; $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Incident ALTER COLUMN CategoryName TYPE varchar(65536); $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Incident ALTER COLUMN CategoryName SET DEFAULT ''::text; $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Incident ALTER COLUMN ConcatenatedBroadcastChannelIds TYPE varchar(65536); $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Incident ALTER COLUMN ConcatenatedBroadcastChannelIds SET DEFAULT ''::text; $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Incident ALTER COLUMN ChannelIDToRootID TYPE varchar(65536); $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Incident ALTER COLUMN ChannelIDToRootID SET DEFAULT ''::text; $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Playbook ALTER COLUMN ReminderMessageTemplate TYPE varchar(65536); $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Playbook ALTER COLUMN ReminderMessageTemplate SET DEFAULT ''::text; $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Playbook ALTER COLUMN ConcatenatedInvitedUserIDs TYPE varchar(65536); $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Playbook ALTER COLUMN ConcatenatedInvitedUserIDs SET DEFAULT ''::text; $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Playbook ALTER COLUMN ConcatenatedWebhookOnCreationURLs TYPE varchar(65536); $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Playbook ALTER COLUMN ConcatenatedWebhookOnCreationURLs SET DEFAULT ''::text; $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Playbook ALTER COLUMN ConcatenatedInvitedGroupIDs TYPE varchar(65536); $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Playbook ALTER COLUMN ConcatenatedInvitedGroupIDs SET DEFAULT ''::text; $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Playbook ALTER COLUMN MessageOnJoin TYPE varchar(65536); $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Playbook ALTER COLUMN MessageOnJoin SET DEFAULT ''::text; $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Playbook ALTER COLUMN RetrospectiveTemplate TYPE varchar(65536); $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Playbook ALTER COLUMN RetrospectiveTemplate SET DEFAULT ''::text; $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Playbook ALTER COLUMN ConcatenatedWebhookOnStatusUpdateURLs TYPE varchar(65536); $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Playbook ALTER COLUMN ConcatenatedWebhookOnStatusUpdateURLs SET DEFAULT ''::text; $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Playbook ALTER COLUMN ConcatenatedSignalAnyKeywords TYPE varchar(65536); $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Playbook ALTER COLUMN ConcatenatedSignalAnyKeywords SET DEFAULT ''::text; $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Playbook ALTER COLUMN CategoryName TYPE varchar(65536); $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Playbook ALTER COLUMN CategoryName SET DEFAULT ''::text; $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Playbook ALTER COLUMN ChecklistsJSON TYPE JSON USING ChecklistsJSON::JSON; $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Playbook ALTER COLUMN ConcatenatedBroadcastChannelIds TYPE varchar(65536); $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Playbook ALTER COLUMN ConcatenatedBroadcastChannelIds SET DEFAULT ''::text; $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Playbook ALTER COLUMN RunSummaryTemplate TYPE varchar(65536); $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Playbook ALTER COLUMN RunSummaryTemplate SET DEFAULT ''::text; $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Playbook ALTER COLUMN ChannelNameTemplate TYPE varchar(65536); $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Playbook ALTER COLUMN ChannelNameTemplate SET DEFAULT ''::text; $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_PlaybookMember ALTER COLUMN Roles TYPE varchar(65536); $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Category_Item ADD CONSTRAINT ir_category_item_categoryid FOREIGN KEY (CategoryId) REFERENCES {{ .source_schema }}.IR_Category(Id); $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Metric ADD CONSTRAINT ir_metric_metricconfigid FOREIGN KEY (MetricConfigId) REFERENCES {{ .source_schema }}.IR_MetricConfig(Id); $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Metric ADD CONSTRAINT ir_metric_incidentid FOREIGN KEY (IncidentId) REFERENCES {{ .source_schema }}.IR_Incident(Id); $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_MetricConfig ADD CONSTRAINT ir_metricconfig_playbookid FOREIGN KEY (PlaybookId) REFERENCES {{ .source_schema }}.IR_Playbook(Id); $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_PlaybookAutoFollow ADD CONSTRAINT ir_playbookautofollow_playbookid FOREIGN KEY (PlaybookId) REFERENCES {{ .source_schema }}.IR_Playbook(Id); $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_PlaybookMember ADD CONSTRAINT ir_playbookmember_playbookid FOREIGN KEY (PlaybookId) REFERENCES {{ .source_schema }}.IR_Playbook(Id); $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_Run_Participants ADD CONSTRAINT ir_run_participants_incidentid FOREIGN KEY (IncidentId) REFERENCES {{ .source_schema }}.IR_Incident(Id); $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_StatusPosts ADD CONSTRAINT ir_statusposts_incidentid FOREIGN KEY (IncidentId) REFERENCES {{ .source_schema }}.IR_Incident(Id); $$,
    $$ ALTER TABLE {{ .source_schema }}.IR_TimelineEvent ADD CONSTRAINT ir_timelineevent_incidentid FOREIGN KEY (IncidentId) REFERENCES {{ .source_schema }}.IR_Incident(Id); $$,
    $$ CREATE UNIQUE INDEX IF NOT EXISTS ir_playbookmember_playbookid_memberid_key on {{ .source_schema }}.IR_PlaybookMember(PlaybookId,MemberId); $$,
    $$ CREATE INDEX IF NOT EXISTS ir_statusposts_incidentid_postid_key on {{ .source_schema }}.IR_StatusPosts(IncidentId,PostId); $$,
    $$ CREATE INDEX IF NOT EXISTS ir_playbookmember_playbookid on {{ .source_schema }}.IR_PlaybookMember(PlaybookId); $$,
    $$ ALTER SCHEMA {{ .source_schema }} RENAME TO public; $$,
    $$ SELECT pg_catalog.set_config('search_path', '"$user", public', false); $$,
    $$ ALTER USER {{ .pg_user }} SET SEARCH_PATH TO 'public'; $$;
pgLoader playbooks.load > playbooks_migration.log

Focalboard

v9.0 부터 보드는 완전히 커뮤니티에서 Focialboard 플러그인으로 지원되는 형태로 전환됩니다. 따라서 이 가이드는 스키마의 v7.10.x 버전만 다루고 있습니다. Official announcement .

이전을 통해 마이그레이션을 시작할 준비가 되면, 스키마데이터 를 다음과 같이 실행하여 마이그레이션을 시작할 수 있습니다:

LOAD DATABASE
     FROM      mysql://{{ .mysql_user }}:{{ .mysql_password }}@mysql:3306/{{ .source_schema }}
     INTO      pgsql://{{ .pg_user }}:{{ .pg_password }}@postgres:5432/{{ .target_schema }}

WITH include drop, create tables, create indexes, reset sequences,
    workers = 8, concurrency = 1,
    multiple readers per thread, rows per range = 50000,
    preserve index names

SET PostgreSQL PARAMETERS
    maintenance_work_mem to '128MB',
    work_mem to '12MB'

SET MySQL PARAMETERS
    net_read_timeout  = '120',
    net_write_timeout = '120'

CAST column focalboard_blocks.fields to "json" drop typemod,
     column focalboard_blocks_history.fields to "json" drop typemod,
     column focalboard_schema_migrations.name to "varchar" drop typemod,
     column focalboard_sessions.props to "json" drop typemod,
     column focalboard_teams.settings to "json" drop typemod,
     column focalboard_users.props to "json" drop typemod,
     type int when (= precision 11) to int4 drop typemod,
     type json to jsonb drop typemod

INCLUDING ONLY TABLE NAMES MATCHING
    ~/focalboard/

BEFORE LOAD DO
    $$ ALTER SCHEMA public RENAME TO {{ .source_schema }}; $$

AFTER LOAD DO
    $$ UPDATE {{ .source_schema }}.focalboard_blocks SET "fields" = '{}'::json WHERE "fields"::text = ''; $$,
    $$ UPDATE {{ .source_schema }}.focalboard_blocks_history SET "fields" = '{}'::json WHERE "fields"::text = ''; $$,
    $$ UPDATE {{ .source_schema }}.focalboard_sessions SET "props" = '{}'::json WHERE "props"::text = ''; $$,
    $$ UPDATE {{ .source_schema }}.focalboard_teams SET "settings" = '{}'::json WHERE "settings"::text = ''; $$,
    $$ UPDATE {{ .source_schema }}.focalboard_users SET "props" = '{}'::json WHERE "props"::text = ''; $$,
    $$ ALTER SCHEMA {{ .source_schema }} RENAME TO public; $$,
    $$ SELECT pg_catalog.set_config('search_path', '"$user", public', false); $$,
    $$ ALTER USER {{ .pg_user }} SET SEARCH_PATH TO 'public'; $$;
pgLoader focalboard.load > focalboard_migration.log

플러그인 데이터 비교

dbcmp --source "${MYSQL_DSN}" --target "${POSTGRES_DSN}" --exclude="db_migrations,systems"

반복적인 마이그레이션

pgloader 구성 파일에서 마이그레이션이 한 번에 이루어질 것으로 가정하는 경우 여러 단계가 있습니다. 마이그레이션을 여러 번 실행할 계획이라면 아래 정의된 변경 사항을 완료하십시오:

  • BEFORE LOAD DOAFTER LOAD DO 섹션에 정의된 모든 문을 버립니다.

  • 마이그레이션 전에 BEFORE LOAD DO 섹션에 정의된 문들을 한 번만 실행합니다.

  • 마이그레이션이 완료되면, AFTER LOAD DO 섹션에 정의된 문들을 수동으로 실행합니다.

문제 해결

MySQL의 지원되지 않는 인증

만약 MySQL v8의 인증으로 인한 오류가 발생한다면, pgLoader와 관련된 알려진 이슈 와 관련이 있을 수 있습니다. 이를 해결하기 위해서는 MySQL 구성에서 기본 인증 방법을 mysql_native_password 로 설정해야 합니다. 이를 위해 mysql.cnf 파일에 default-authentication-plugin=mysql_native_password 값을 추가하십시오. 또한 사용자를 이 인증 방법을 사용하도록 업데이트하는 것을 잊지 마십시오.

ALTER USER ' <mysql_user>'@'%' IDENTIFIED WITH mysql_native_password BY ' <mysql_password>';