docs(aicharacter): 크리에이터 연결 DDL을 보강한다
This commit is contained in:
@@ -20,6 +20,13 @@ PREPARE add_member_kind_stmt FROM @add_member_kind_sql;
|
|||||||
EXECUTE add_member_kind_stmt;
|
EXECUTE add_member_kind_stmt;
|
||||||
DEALLOCATE PREPARE add_member_kind_stmt;
|
DEALLOCATE PREPARE add_member_kind_stmt;
|
||||||
|
|
||||||
|
-- 1번 결과 확인: varchar(30), NOT NULL, DEFAULT 'HUMAN'
|
||||||
|
SELECT column_name, column_type, is_nullable, column_default, column_comment
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = DATABASE()
|
||||||
|
AND table_name = 'member'
|
||||||
|
AND column_name = 'member_kind';
|
||||||
|
|
||||||
-- 2. chat_character.creator_member_id nullable 컬럼 추가
|
-- 2. chat_character.creator_member_id nullable 컬럼 추가
|
||||||
SET @creator_member_column_exists := (
|
SET @creator_member_column_exists := (
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
@@ -31,7 +38,7 @@ SET @creator_member_column_exists := (
|
|||||||
|
|
||||||
SET @add_creator_member_sql := IF(
|
SET @add_creator_member_sql := IF(
|
||||||
@creator_member_column_exists = 0,
|
@creator_member_column_exists = 0,
|
||||||
'ALTER TABLE chat_character ADD COLUMN creator_member_id BIGINT NULL COMMENT ''크리에이터 기능 주체 Member ID''',
|
'ALTER TABLE chat_character ADD COLUMN creator_member_id BIGINT NULL COMMENT ''크리에이터 기능 주체 Member ID'' AFTER character_type',
|
||||||
'SELECT ''chat_character.creator_member_id already exists'' AS message'
|
'SELECT ''chat_character.creator_member_id already exists'' AS message'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -39,49 +46,30 @@ PREPARE add_creator_member_stmt FROM @add_creator_member_sql;
|
|||||||
EXECUTE add_creator_member_stmt;
|
EXECUTE add_creator_member_stmt;
|
||||||
DEALLOCATE PREPARE add_creator_member_stmt;
|
DEALLOCATE PREPARE add_creator_member_stmt;
|
||||||
|
|
||||||
|
-- 2번 결과 확인: bigint, NULL 허용
|
||||||
|
SELECT column_name, column_type, is_nullable, column_comment
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = DATABASE()
|
||||||
|
AND table_name = 'chat_character'
|
||||||
|
AND column_name = 'creator_member_id';
|
||||||
|
|
||||||
-- 3. 기존 chat_character별 AI 캐릭터용 Member 생성 및 매핑
|
-- 3. 기존 chat_character별 AI 캐릭터용 Member 생성 및 매핑
|
||||||
DROP TEMPORARY TABLE IF EXISTS tmp_chat_character_creator_member;
|
DROP TEMPORARY TABLE IF EXISTS tmp_chat_character_creator_member;
|
||||||
CREATE TEMPORARY TABLE tmp_chat_character_creator_member (
|
CREATE TEMPORARY TABLE tmp_chat_character_creator_member (
|
||||||
chat_character_id BIGINT NOT NULL PRIMARY KEY,
|
chat_character_id BIGINT NOT NULL PRIMARY KEY,
|
||||||
|
migration_email VARCHAR(255) NOT NULL UNIQUE,
|
||||||
creator_member_id BIGINT NULL
|
creator_member_id BIGINT NULL
|
||||||
) COMMENT 'chat_character와 backfill member.id 임시 매핑';
|
) COMMENT 'chat_character와 backfill member.id 임시 매핑';
|
||||||
|
|
||||||
INSERT INTO tmp_chat_character_creator_member (chat_character_id)
|
INSERT INTO tmp_chat_character_creator_member (chat_character_id, migration_email)
|
||||||
SELECT c.id
|
SELECT
|
||||||
|
c.id,
|
||||||
|
CONCAT('__ai_character_creator_', c.id, '@migration.local')
|
||||||
FROM chat_character c
|
FROM chat_character c
|
||||||
WHERE c.creator_member_id IS NULL;
|
WHERE c.creator_member_id IS NULL;
|
||||||
|
|
||||||
DROP PROCEDURE IF EXISTS backfill_chat_character_creator_member;
|
-- member.email은 nullable이므로 backfill 중에만 임시 식별자로 사용하고, 매핑 후 NULL로 되돌린다.
|
||||||
|
INSERT INTO member (
|
||||||
DELIMITER //
|
|
||||||
CREATE PROCEDURE backfill_chat_character_creator_member()
|
|
||||||
BEGIN
|
|
||||||
DECLARE done BOOLEAN DEFAULT FALSE;
|
|
||||||
DECLARE v_chat_character_id BIGINT;
|
|
||||||
DECLARE v_name VARCHAR(255);
|
|
||||||
DECLARE v_description TEXT;
|
|
||||||
DECLARE v_image_path VARCHAR(255);
|
|
||||||
|
|
||||||
DECLARE character_cursor CURSOR FOR
|
|
||||||
SELECT c.id, c.name, c.description, c.image_path
|
|
||||||
FROM chat_character c
|
|
||||||
INNER JOIN tmp_chat_character_creator_member m
|
|
||||||
ON m.chat_character_id = c.id
|
|
||||||
WHERE m.creator_member_id IS NULL
|
|
||||||
ORDER BY c.id;
|
|
||||||
|
|
||||||
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
|
|
||||||
|
|
||||||
OPEN character_cursor;
|
|
||||||
|
|
||||||
read_loop: LOOP
|
|
||||||
FETCH character_cursor INTO v_chat_character_id, v_name, v_description, v_image_path;
|
|
||||||
|
|
||||||
IF done THEN
|
|
||||||
LEAVE read_loop;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
INSERT INTO member (
|
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
nickname,
|
nickname,
|
||||||
@@ -109,20 +97,21 @@ BEGIN
|
|||||||
apple_reward_can,
|
apple_reward_can,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
) VALUES (
|
)
|
||||||
NULL,
|
SELECT
|
||||||
|
m.migration_email,
|
||||||
'',
|
'',
|
||||||
v_name,
|
c.name,
|
||||||
v_image_path,
|
c.image_path,
|
||||||
'EMAIL',
|
'EMAIL',
|
||||||
'NONE',
|
'NONE',
|
||||||
'CREATOR',
|
'CREATOR',
|
||||||
'AI_CHARACTER',
|
'AI_CHARACTER',
|
||||||
TRUE,
|
TRUE,
|
||||||
'CUMULATIVE',
|
'CUMULATIVE',
|
||||||
TRUE,
|
c.is_active,
|
||||||
'web',
|
'web',
|
||||||
COALESCE(v_description, ''),
|
COALESCE(c.description, ''),
|
||||||
'',
|
'',
|
||||||
'',
|
'',
|
||||||
'',
|
'',
|
||||||
@@ -137,19 +126,19 @@ BEGIN
|
|||||||
0,
|
0,
|
||||||
CURRENT_TIMESTAMP,
|
CURRENT_TIMESTAMP,
|
||||||
CURRENT_TIMESTAMP
|
CURRENT_TIMESTAMP
|
||||||
);
|
FROM tmp_chat_character_creator_member m
|
||||||
|
INNER JOIN chat_character c
|
||||||
|
ON c.id = m.chat_character_id
|
||||||
|
LEFT JOIN member existing_member
|
||||||
|
ON existing_member.email = m.migration_email
|
||||||
|
WHERE existing_member.id IS NULL;
|
||||||
|
|
||||||
UPDATE tmp_chat_character_creator_member
|
UPDATE tmp_chat_character_creator_member m
|
||||||
SET creator_member_id = LAST_INSERT_ID()
|
INNER JOIN member mb
|
||||||
WHERE chat_character_id = v_chat_character_id;
|
ON mb.email = m.migration_email
|
||||||
END LOOP;
|
SET m.creator_member_id = mb.id
|
||||||
|
WHERE m.creator_member_id IS NULL
|
||||||
CLOSE character_cursor;
|
AND m.chat_character_id IS NOT NULL;
|
||||||
END //
|
|
||||||
DELIMITER ;
|
|
||||||
|
|
||||||
CALL backfill_chat_character_creator_member();
|
|
||||||
DROP PROCEDURE IF EXISTS backfill_chat_character_creator_member;
|
|
||||||
|
|
||||||
UPDATE chat_character c
|
UPDATE chat_character c
|
||||||
INNER JOIN tmp_chat_character_creator_member m
|
INNER JOIN tmp_chat_character_creator_member m
|
||||||
@@ -158,6 +147,12 @@ SET c.creator_member_id = m.creator_member_id
|
|||||||
WHERE c.creator_member_id IS NULL
|
WHERE c.creator_member_id IS NULL
|
||||||
AND m.creator_member_id IS NOT NULL;
|
AND m.creator_member_id IS NOT NULL;
|
||||||
|
|
||||||
|
UPDATE member mb
|
||||||
|
INNER JOIN tmp_chat_character_creator_member m
|
||||||
|
ON m.creator_member_id = mb.id
|
||||||
|
SET mb.email = NULL
|
||||||
|
WHERE mb.email = m.migration_email;
|
||||||
|
|
||||||
-- 4. unique index 추가
|
-- 4. unique index 추가
|
||||||
SET @creator_member_unique_exists := (
|
SET @creator_member_unique_exists := (
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
@@ -207,6 +202,10 @@ SELECT COUNT(*) AS missing_creator_member_count
|
|||||||
FROM chat_character
|
FROM chat_character
|
||||||
WHERE creator_member_id IS NULL;
|
WHERE creator_member_id IS NULL;
|
||||||
|
|
||||||
|
SELECT COUNT(*) AS remaining_migration_email_count
|
||||||
|
FROM member
|
||||||
|
WHERE email LIKE '__ai_character_creator_%@migration.local';
|
||||||
|
|
||||||
-- 7. 검증 완료 후 creator_member_id NOT NULL 전환
|
-- 7. 검증 완료 후 creator_member_id NOT NULL 전환
|
||||||
SET @missing_creator_member_count := (
|
SET @missing_creator_member_count := (
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
@@ -233,3 +232,38 @@ EXECUTE modify_creator_member_not_null_stmt;
|
|||||||
DEALLOCATE PREPARE modify_creator_member_not_null_stmt;
|
DEALLOCATE PREPARE modify_creator_member_not_null_stmt;
|
||||||
|
|
||||||
DROP TEMPORARY TABLE IF EXISTS tmp_chat_character_creator_member;
|
DROP TEMPORARY TABLE IF EXISTS tmp_chat_character_creator_member;
|
||||||
|
|
||||||
|
-- Rollback 참고용. 운영 반영 후 문제가 있으면 백업 복구를 우선 검토한다.
|
||||||
|
-- 아래 SQL은 이 마이그레이션으로 연결된 AI_CHARACTER Member와 제약/컬럼을 되돌리는 전체 롤백 예시다.
|
||||||
|
-- 신규 기능을 이미 운영에서 사용한 뒤에는 후속 데이터 의존성이 생길 수 있으므로 실행 전 영향 범위를 재확인한다.
|
||||||
|
-- 1) FK 제거
|
||||||
|
-- ALTER TABLE chat_character DROP FOREIGN KEY fk_chat_character_creator_member;
|
||||||
|
-- 2) unique index 제거
|
||||||
|
-- ALTER TABLE chat_character DROP INDEX uk_chat_character_creator_member;
|
||||||
|
-- 3) creator_member_id를 NULL 허용으로 복구
|
||||||
|
-- ALTER TABLE chat_character MODIFY COLUMN creator_member_id BIGINT NULL COMMENT '크리에이터 기능 주체 Member ID';
|
||||||
|
-- 4) backfill로 연결된 AI 캐릭터용 Member 삭제 준비
|
||||||
|
-- DROP TEMPORARY TABLE IF EXISTS tmp_rollback_ai_character_member;
|
||||||
|
-- CREATE TEMPORARY TABLE tmp_rollback_ai_character_member (
|
||||||
|
-- member_id BIGINT NOT NULL PRIMARY KEY
|
||||||
|
-- ) COMMENT 'AI 캐릭터 크리에이터 backfill 롤백 대상 Member';
|
||||||
|
-- INSERT INTO tmp_rollback_ai_character_member (member_id)
|
||||||
|
-- SELECT DISTINCT mb.id
|
||||||
|
-- FROM member mb
|
||||||
|
-- INNER JOIN chat_character c
|
||||||
|
-- ON c.creator_member_id = mb.id
|
||||||
|
-- WHERE mb.member_kind = 'AI_CHARACTER'
|
||||||
|
-- AND mb.role = 'CREATOR';
|
||||||
|
-- 5) chat_character 연결 해제 후 Member 삭제
|
||||||
|
-- UPDATE chat_character c
|
||||||
|
-- INNER JOIN tmp_rollback_ai_character_member r
|
||||||
|
-- ON r.member_id = c.creator_member_id
|
||||||
|
-- SET c.creator_member_id = NULL;
|
||||||
|
-- DELETE mb
|
||||||
|
-- FROM member mb
|
||||||
|
-- INNER JOIN tmp_rollback_ai_character_member r
|
||||||
|
-- ON r.member_id = mb.id;
|
||||||
|
-- DROP TEMPORARY TABLE IF EXISTS tmp_rollback_ai_character_member;
|
||||||
|
-- 6) 컬럼 제거가 필요한 전체 스키마 롤백인 경우에만 실행
|
||||||
|
-- ALTER TABLE chat_character DROP COLUMN creator_member_id;
|
||||||
|
-- ALTER TABLE member DROP COLUMN member_kind;
|
||||||
|
|||||||
@@ -74,8 +74,9 @@
|
|||||||
- 기존 모든 `chat_character`별 AI 캐릭터용 Member 생성
|
- 기존 모든 `chat_character`별 AI 캐릭터용 Member 생성
|
||||||
- 생성된 Member를 `chat_character.creator_member_id`에 연결
|
- 생성된 Member를 `chat_character.creator_member_id`에 연결
|
||||||
- 검증 후 `chat_character.creator_member_id not null` 전환
|
- 검증 후 `chat_character.creator_member_id not null` 전환
|
||||||
- SQL backfill은 `email = null`, `password = ''`, `role = 'CREATOR'`, `member_kind = 'AI_CHARACTER'`로 생성한다.
|
- SQL backfill은 최종적으로 `email = null`, `password = ''`, `role = 'CREATOR'`, `member_kind = 'AI_CHARACTER'` 상태가 되도록 생성한다.
|
||||||
- MySQL에서 insert된 Member ID를 안전하게 매핑하기 위해 저장 프로시저 또는 임시 매핑 테이블을 사용한다. `email`을 임시 식별자로 사용하지 않는다.
|
- `member.email`은 nullable이므로 저장 프로시저 대신 backfill 중 임시 식별자로 사용할 수 있다. 단, `chat_character.creator_member_id` 매핑 후 임시 email 값은 반드시 `NULL`로 되돌린다.
|
||||||
|
- 운영 반영 후 문제에 대비해 FK/index/연결 데이터/컬럼 제거 순서의 롤백 방법을 SQL 문서에 함께 기록한다.
|
||||||
- Verify:
|
- Verify:
|
||||||
- SQL 내 검증 쿼리 포함:
|
- SQL 내 검증 쿼리 포함:
|
||||||
```sql
|
```sql
|
||||||
@@ -308,3 +309,9 @@
|
|||||||
- `./gradlew test`
|
- `./gradlew test`
|
||||||
- 목적: 리뷰 보완 후 전체 회귀 테스트 확인.
|
- 목적: 리뷰 보완 후 전체 회귀 테스트 확인.
|
||||||
- 결과: `BUILD SUCCESSFUL in 1m 15s`.
|
- 결과: `BUILD SUCCESSFUL in 1m 15s`.
|
||||||
|
- `rg -n "CREATE PROCEDURE|CURSOR|CALL backfill_chat_character_creator_member|LAST_INSERT_ID|Rollback|임시 식별자" docs/20260611_AI캐릭터_크리에이터기능_최소연결`
|
||||||
|
- 목적: 운영 DB 반영 SQL을 저장 프로시저 없는 단순 SQL로 변경했고, 임시 email 식별자 기준과 롤백 절차가 문서에 남았는지 확인.
|
||||||
|
- 결과: 저장 프로시저/커서/CALL/LAST_INSERT_ID 패턴은 미검출. `alter-existing-tables.sql`에 임시 식별자 정리와 Rollback 절차가 존재함을 확인.
|
||||||
|
- `./gradlew tasks --all`
|
||||||
|
- 목적: 문서 변경 후 Gradle 명령 유효성 확인.
|
||||||
|
- 결과: `BUILD SUCCESSFUL in 942ms`.
|
||||||
|
|||||||
Reference in New Issue
Block a user