-
Notifications
You must be signed in to change notification settings - Fork 1
fix: 중복되는 채팅 방 메시지 병합 #511
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+271
−0
Merged
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
cffde9b
fix: DIRECT 채팅방 중복 병합 처리 SQL 추가
dh2906 c768385
fix: DIRECT 채팅방 중복 병합 처리 SQL 추가
dh2906 d205b43
fix: V69 마이그레이션에서 하드코딩된 schema 선택 제거
dh2906 294d8d6
fix: DIRECT 채팅방 병합 시 메시지 존재하는 방 우선 선택 및 메타데이터 갱신
dh2906 2d7b1ca
fix: 채팅 메시지 이동 시 updated_at 타임스탬프 보존
dh2906 9f43ba8
fix: 채팅방 병합 시 멤버십 상태 보존 로직 추가
dh2906 0655a90
fix: 채팅방 병합 시 누락된 사용자 상태 병합 추가
dh2906 e8575d3
fix: last_message_content 재계산 시 타임스탬프 동일 문제 해결
dh2906 db4ece6
fix: V69 마이그레이션 재시도 가능하도록 개선
dh2906 a8c253b
fix: 다중 loser 방 매핑 시 멤버십 상태 병합 충돌 해결
dh2906 d003be6
fix: V69 마이그레이션 재시도 시 매핑 테이블 보호
dh2906 9828c01
fix: V69 마이그레이션 뮤트 설정 UNIQUE 충돌 해결
dh2906 a6aad82
fix: V69 마이그레이션 모든 엣지케이스 처리
dh2906 90a66a9
fix: V69 마이그레이션 다중 loser 방 PK/UNIQUE 충돌 해결
dh2906 0f6dcbb
fix: V69 마이그레이션 LEAST/GREATEST NULL 처리 버그 수정
dh2906 508b4fa
fix: V69 마이그레이션 최신 메시지 선택 기준 개선
dh2906 dcf65eb
fix: V69 마이그레이션 0명 방 처리 개선
dh2906 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
271 changes: 271 additions & 0 deletions
271
src/main/resources/db/migration/V69__merge_duplicate_direct_chat_rooms.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,271 @@ | ||
| -- DIRECT 타입 채팅방 중 같은 두 유저 간 중복된 방 병합 | ||
| -- 이유: findByTwoUsers 쿼리가 유니크 결과를 기대하지만 중복 방으로 인해 2개 이상 반환됨 | ||
| -- 해결: 메시지를 하나의 방으로 합치고 중복 방 제거 | ||
| -- | ||
| -- [엣지케이스 처리] | ||
| -- 1. 재시도 시 매핑 테이블 보호: NOT EXISTS로 기존 매핑 유지 | ||
| -- 2. 연산자 우선순위: WHERE 조건에 괄호로 묶어 AND/OR 우선순위 명확화 | ||
| -- 3. chat_room_member 병합: visible_message_from, left_at, last_read_at, custom_room_name, is_owner 모두 처리 | ||
| -- - 기존 멤버 UPDATE 후 GROUP BY로 집계하여 orphan INSERT (다중 loser 시 PK 충돌 방지) | ||
| -- 4. notification_mute_setting 충돌: 여러 loser 방의 설정을 GROUP BY로 집계 후 INSERT | ||
| -- - 동일 사용자가 여러 loser 방에 뮤트 설정 시 MAX(is_muted)로 병합 | ||
| -- 5. 메시지 중복 방지: from_room_id는 PK라 자연스럽게 중복 없음 | ||
| -- 6. 롤백 지원: 매핑 테이블 DROP/CREATE 대신 재사용 패턴 | ||
|
|
||
| -- 임시 테이블 대신 실제 테이블 사용 (MySQL 임시 테이블 재참조 제한 회피) | ||
| -- 재시도 가능하도록: 기존 매핑 테이블이 있으면 스킵, 없으면 생성 | ||
| DROP TABLE IF EXISTS temp_direct_room_pairs; | ||
|
|
||
| -- 이전 실행에서 남은 매핑 테이블이 있으면 재사용 (재시도 시) | ||
| -- 없으면 새로 생성 | ||
| CREATE TABLE IF NOT EXISTS temp_duplicate_room_map ( | ||
| from_room_id INT PRIMARY KEY, | ||
| keep_room_id INT NOT NULL, | ||
| user1_id INT NOT NULL, | ||
| user2_id INT NOT NULL | ||
| ); | ||
|
|
||
| -- 1) DIRECT 방 중 "정확히 2명"인 방만 유저쌍 단위로 펼침 | ||
| -- 또는 이미 매핑 테이블에 있는 방도 포함 (재시도 시 0명이 된 방 처리) | ||
| -- 주의: AND/OR 우선순위 때문에 조건을 명확히 괄호로 묶음 | ||
| -- LEFT JOIN 사용: 0명 방도 EXISTS 조건으로 포함되도록 보존 | ||
| CREATE TABLE temp_direct_room_pairs AS | ||
| SELECT | ||
| cr.id AS room_id, | ||
| LEAST(c1.user_id, c2.user_id) AS user1_id, | ||
| GREATEST(c1.user_id, c2.user_id) AS user2_id, | ||
| cr.created_at, | ||
| ( | ||
| SELECT MAX(cm.created_at) | ||
| FROM chat_message cm | ||
| WHERE cm.chat_room_id = cr.id | ||
| ) AS last_message_at | ||
| FROM chat_room cr | ||
| LEFT JOIN chat_room_member c1 | ||
| ON c1.chat_room_id = cr.id | ||
| LEFT JOIN chat_room_member c2 | ||
| ON c2.chat_room_id = cr.id | ||
| AND c1.user_id < c2.user_id | ||
| WHERE cr.room_type = 'DIRECT' | ||
| AND ( | ||
| ( | ||
| SELECT COUNT(*) | ||
| FROM chat_room_member m | ||
| WHERE m.chat_room_id = cr.id | ||
| ) = 2 | ||
| OR EXISTS ( | ||
| SELECT 1 FROM temp_duplicate_room_map existing | ||
| WHERE existing.from_room_id = cr.id OR existing.keep_room_id = cr.id | ||
| ) | ||
| ); | ||
|
|
||
| -- 2) 중복 방 중 어떤 방을 남기고 어떤 방을 지울지 매핑 테이블 생성 | ||
| -- 매핑 테이블이 비어있는 경우에만 채움 (재시도 시 기존 매핑 유지) | ||
| -- ON DUPLICATE KEY UPDATE 제거: 재시도 시 매핑 방향이 뒤바뀌지 않도록 보호 | ||
| INSERT INTO temp_duplicate_room_map (from_room_id, keep_room_id, user1_id, user2_id) | ||
| SELECT | ||
| loser.room_id AS from_room_id, | ||
| winner.room_id AS keep_room_id, | ||
| loser.user1_id, | ||
| loser.user2_id | ||
| FROM ( | ||
| SELECT | ||
| room_id, | ||
| user1_id, | ||
| user2_id, | ||
| ROW_NUMBER() OVER ( | ||
| PARTITION BY user1_id, user2_id | ||
| ORDER BY | ||
| (last_message_at IS NOT NULL) DESC, | ||
| last_message_at DESC, | ||
| created_at DESC, | ||
| room_id DESC | ||
| ) AS rn, | ||
| COUNT(*) OVER ( | ||
| PARTITION BY user1_id, user2_id | ||
| ) AS room_count | ||
| FROM temp_direct_room_pairs | ||
| ) loser | ||
| JOIN ( | ||
| SELECT | ||
| room_id, | ||
| user1_id, | ||
| user2_id, | ||
| ROW_NUMBER() OVER ( | ||
| PARTITION BY user1_id, user2_id | ||
| ORDER BY | ||
| (last_message_at IS NOT NULL) DESC, | ||
| last_message_at DESC, | ||
| created_at DESC, | ||
| room_id DESC | ||
| ) AS rn | ||
| FROM temp_direct_room_pairs | ||
| ) winner | ||
| ON winner.user1_id = loser.user1_id | ||
| AND winner.user2_id = loser.user2_id | ||
| AND winner.rn = 1 | ||
| WHERE loser.room_count > 1 | ||
| AND loser.rn > 1 | ||
| AND NOT EXISTS (SELECT 1 FROM temp_duplicate_room_map); | ||
|
|
||
| -- 3) 삭제 대상 방의 메시지를 keep 방으로 이동 | ||
| -- 메시지는 PK(id)로 관리되므로 중복 키 충돌 없음 | ||
| UPDATE chat_message cm | ||
| JOIN temp_duplicate_room_map m | ||
| ON cm.chat_room_id = m.from_room_id | ||
| SET cm.chat_room_id = m.keep_room_id, | ||
| cm.updated_at = cm.updated_at; | ||
|
|
||
| -- 4) 삭제 대상 방의 멤버십 상태를 keep 방으로 병합 | ||
| -- visible_message_from: 더 이른 값(더 많은 메시지 조회 가능) 선택 | ||
| -- left_at: 둘 중 하나라도 나간 경우 나간 것으로 처리 (더 이른 값 선택) | ||
| -- last_read_at: 더 나중에 읽은 값(최신 읽음 시점) 선택 | ||
| -- custom_room_name: 사용자가 설정한 방 이름이 있으면 보존 | ||
| -- is_owner: 하나라도 owner면 owner 유지 (OR 조건) | ||
| -- 여러 loser 방이 같은 keep 방으로 매핑될 수 있으므로 먼저 집계하여 중복 업데이트 방지 | ||
|
|
||
| -- 4a) 기존 keep_room 멤버 업데이트 | ||
| -- LEAST/GREATEST는 인자 중 NULL이 있으면 결과도 NULL이 되므로 CASE로 NULL 처리 | ||
| UPDATE chat_room_member t | ||
| JOIN ( | ||
| SELECT | ||
| m.keep_room_id, | ||
| crm.user_id, | ||
| MIN(crm.visible_message_from) AS min_visible_from, | ||
| MIN(crm.left_at) AS min_left_at, | ||
| MAX(crm.last_read_at) AS max_last_read_at, | ||
| MAX(crm.custom_room_name) AS max_custom_room_name, | ||
| MAX(CASE WHEN crm.is_owner THEN 1 ELSE 0 END) AS max_is_owner | ||
| FROM temp_duplicate_room_map m | ||
| JOIN chat_room_member crm ON crm.chat_room_id = m.from_room_id | ||
| GROUP BY m.keep_room_id, crm.user_id | ||
| ) la ON t.chat_room_id = la.keep_room_id AND t.user_id = la.user_id | ||
| SET t.visible_message_from = CASE | ||
| WHEN t.visible_message_from IS NULL THEN la.min_visible_from | ||
| WHEN la.min_visible_from IS NULL THEN t.visible_message_from | ||
| ELSE LEAST(t.visible_message_from, la.min_visible_from) | ||
| END, | ||
| t.left_at = CASE | ||
| WHEN t.left_at IS NULL THEN la.min_left_at | ||
| WHEN la.min_left_at IS NULL THEN t.left_at | ||
| ELSE LEAST(t.left_at, la.min_left_at) | ||
| END, | ||
| t.last_read_at = CASE | ||
| WHEN t.last_read_at IS NULL THEN la.max_last_read_at | ||
| WHEN la.max_last_read_at IS NULL THEN t.last_read_at | ||
| ELSE GREATEST(t.last_read_at, la.max_last_read_at) | ||
| END, | ||
| t.custom_room_name = COALESCE(t.custom_room_name, la.max_custom_room_name), | ||
| t.is_owner = (t.is_owner OR la.max_is_owner > 0), | ||
| t.updated_at = t.updated_at; | ||
dh2906 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| -- 4b) keep_room에 없는 loser 멤버 INSERT (orphan member 처리) | ||
| -- 여러 loser 방이 같은 keep 방으로 매핑될 수 있으므로 GROUP BY로 집계하여 PK 충돌 방지 | ||
| INSERT INTO chat_room_member ( | ||
| chat_room_id, user_id, last_read_at, created_at, updated_at, | ||
| visible_message_from, left_at, custom_room_name, is_owner | ||
| ) | ||
| SELECT | ||
| m.keep_room_id, | ||
| crm.user_id, | ||
| MAX(crm.last_read_at) AS last_read_at, | ||
| MIN(crm.created_at) AS created_at, | ||
| MAX(crm.updated_at) AS updated_at, | ||
| MIN(crm.visible_message_from) AS visible_message_from, | ||
| MIN(crm.left_at) AS left_at, | ||
| MAX(crm.custom_room_name) AS custom_room_name, | ||
| MAX(CASE WHEN crm.is_owner THEN 1 ELSE 0 END) AS is_owner | ||
| FROM temp_duplicate_room_map m | ||
| JOIN chat_room_member crm ON crm.chat_room_id = m.from_room_id | ||
| LEFT JOIN chat_room_member existing | ||
| ON existing.chat_room_id = m.keep_room_id | ||
| AND existing.user_id = crm.user_id | ||
| WHERE existing.chat_room_id IS NULL | ||
| GROUP BY m.keep_room_id, crm.user_id; | ||
|
|
||
| -- 5) 삭제 대상 방의 알림 뮤트 설정을 keep 방으로 이동 | ||
| -- 여러 loser 방이 같은 keep 방으로 매핑될 수 있으므로 먼저 집계하여 UNIQUE 충돌 방지 | ||
|
|
||
| -- 5a) loser 방들의 뮤트 설정을 keep_room_id 기준으로 집계 | ||
| -- 동일 사용자가 여러 loser 방에 뮤트 설정을 가진 경우 MAX(is_muted)로 병합 | ||
| CREATE TEMPORARY TABLE temp_mute_setting_agg AS | ||
| SELECT | ||
| m.keep_room_id, | ||
| nms.user_id, | ||
| MAX(nms.is_muted) AS is_muted | ||
| FROM temp_duplicate_room_map m | ||
| JOIN notification_mute_setting nms | ||
| ON nms.target_id = m.from_room_id | ||
| AND nms.target_type = 'CHAT_ROOM' | ||
| GROUP BY m.keep_room_id, nms.user_id; | ||
|
|
||
| -- 5b) 집계된 설정과 충돌하는 기존 keep 방 뮤트 설정 삭제 | ||
| DELETE nms | ||
| FROM notification_mute_setting nms | ||
| JOIN temp_mute_setting_agg agg | ||
| ON nms.target_id = agg.keep_room_id | ||
| AND nms.user_id = agg.user_id | ||
| WHERE nms.target_type = 'CHAT_ROOM'; | ||
|
|
||
| -- 5c) 집계된 뮤트 설정을 keep 방에 INSERT | ||
| INSERT INTO notification_mute_setting (user_id, target_type, target_id, is_muted, created_at, updated_at) | ||
| SELECT | ||
| agg.user_id, | ||
| 'CHAT_ROOM', | ||
| agg.keep_room_id, | ||
| agg.is_muted, | ||
| NOW(), | ||
| NOW() | ||
| FROM temp_mute_setting_agg agg; | ||
|
|
||
| -- 5d) loser 방의 뮤트 설정 삭제 (이미 집계되어 이동 완료) | ||
| DELETE nms | ||
| FROM notification_mute_setting nms | ||
| JOIN temp_duplicate_room_map m | ||
| ON nms.target_id = m.from_room_id | ||
| WHERE nms.target_type = 'CHAT_ROOM'; | ||
|
|
||
| -- 임시 테이블 정리 | ||
| DROP TEMPORARY TABLE IF EXISTS temp_mute_setting_agg; | ||
|
|
||
| -- 6) 삭제 대상 방의 멤버십 삭제 | ||
| DELETE crm | ||
| FROM chat_room_member crm | ||
| JOIN temp_duplicate_room_map m | ||
| ON crm.chat_room_id = m.from_room_id; | ||
dh2906 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| -- 7) 삭제 대상 방 삭제 | ||
| DELETE cr | ||
| FROM chat_room cr | ||
| JOIN temp_duplicate_room_map m | ||
| ON cr.id = m.from_room_id; | ||
|
|
||
| -- 8) 남은 방의 last_message_content와 last_message_sent_at 갱신 | ||
| -- MAX(created_at)으로 최신 메시지 선택, 동일 타임스탬프 시 id로 타임브레이커 | ||
| UPDATE chat_room cr | ||
| JOIN temp_duplicate_room_map m | ||
| ON cr.id = m.keep_room_id | ||
| LEFT JOIN ( | ||
| SELECT | ||
| cm1.chat_room_id, | ||
| cm1.content, | ||
| cm1.created_at | ||
| FROM chat_message cm1 | ||
| JOIN ( | ||
| SELECT chat_room_id, MAX(created_at) AS max_created_at | ||
| FROM chat_message | ||
| GROUP BY chat_room_id | ||
| ) cm2 ON cm2.chat_room_id = cm1.chat_room_id AND cm2.max_created_at = cm1.created_at | ||
| WHERE cm1.id = ( | ||
| SELECT MAX(id) | ||
| FROM chat_message | ||
| WHERE chat_room_id = cm1.chat_room_id | ||
| AND created_at = cm2.max_created_at | ||
| ) | ||
| ) latest_msg ON latest_msg.chat_room_id = cr.id | ||
| SET cr.last_message_content = latest_msg.content, | ||
| cr.last_message_sent_at = latest_msg.created_at; | ||
dh2906 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| -- 9) 임시 테이블 정리 | ||
| DROP TABLE IF EXISTS temp_duplicate_room_map; | ||
| DROP TABLE IF EXISTS temp_direct_room_pairs; | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.