e1be30f8ca
Build & Deploy to K8s / build-and-deploy (push) Failing after 6m54s
Flyway V017 은 메타 DB 만 갱신하지만 MENU_INFO 의 슈퍼관리자 메뉴 (COMPANY_CODE='*') 는 회사 프로비저닝 때 각 테넌트 DB 로 복사되어 박혀있다. StartupSchemaMigrator 에 동일 UPDATE 를 넣어 부팅 시 메타 + 모든 활성 테넌트에 자동 동기화. SEQ 만 갱신하므로 멱등.
123 lines
4.9 KiB
Java
123 lines
4.9 KiB
Java
package com.erp.migration;
|
|
|
|
import com.erp.tenant.DbContextHolder;
|
|
import com.erp.tenant.TenantDbSettings;
|
|
import lombok.RequiredArgsConstructor;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import org.apache.ibatis.session.SqlSession;
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
|
import org.springframework.context.event.EventListener;
|
|
import org.springframework.stereotype.Component;
|
|
|
|
import java.sql.Connection;
|
|
import java.sql.DriverManager;
|
|
import java.sql.Statement;
|
|
import java.util.List;
|
|
|
|
/**
|
|
* 앱 부팅 시 idempotent 스키마 마이그레이션 실행.
|
|
*
|
|
* 왜 필요:
|
|
* DB-per-tenant 구조 → 메타 DB 1개 + 활성 테넌트 DB N개 에 동일 ALTER 필요.
|
|
* runbook 문서 (`db/migrations/RUN_*.md`) 로만 남기면 배포 때 사람이 까먹으면 장애.
|
|
* 부팅 때 `IF NOT EXISTS` 로 안전하게 돌려두면 배포 순서/인적 실수와 무관.
|
|
*
|
|
* 원칙:
|
|
* - 각 ALTER 는 반드시 idempotent (`IF NOT EXISTS` / 재실행 안전)
|
|
* - 테넌트 DB 하나 실패해도 다른 DB 는 계속 진행 (ERROR 로그만)
|
|
* - 메타 DB 가 실패하면 앱 시작은 계속되지만 WARN 크게 남김
|
|
*/
|
|
@Component
|
|
@RequiredArgsConstructor
|
|
@Slf4j
|
|
public class StartupSchemaMigrator {
|
|
|
|
private final TenantDbSettings tenantDbSettings;
|
|
private final SqlSession sqlSession;
|
|
|
|
@Value("${spring.datasource.url}")
|
|
private String metaJdbcUrl;
|
|
|
|
private static final List<String> MIGRATIONS = List.of(
|
|
// RUN_082: 첫 로그인 비밀번호 강제 변경 플래그
|
|
"ALTER TABLE USER_INFO ADD COLUMN IF NOT EXISTS FORCE_PASSWORD_CHANGE BOOLEAN DEFAULT FALSE",
|
|
|
|
// V017: 회사 관리 그룹 하위 관리자 메뉴 순서 재배열
|
|
// 조직 계층(회사→부서→사용자) + 권한 체계(메뉴→권한→권한그룹)
|
|
// 메타 DB 는 Flyway V017 로도 적용되지만 프로비저닝된 테넌트 DB 는
|
|
// 회사 생성 시점 스냅샷이 박혀있으므로 부팅 때 모든 활성 DB 에 동기화.
|
|
// SEQ 만 갱신 → 멱등.
|
|
"""
|
|
UPDATE MENU_INFO
|
|
SET SEQ = CASE MENU_NAME_KOR
|
|
WHEN '회사관리' THEN 100
|
|
WHEN '부서관리' THEN 200
|
|
WHEN '사용자관리' THEN 300
|
|
WHEN '메뉴관리' THEN 400
|
|
WHEN '권한관리' THEN 500
|
|
WHEN '권한 그룹관리' THEN 600
|
|
ELSE SEQ
|
|
END
|
|
WHERE MENU_TYPE = '0'
|
|
AND COMPANY_CODE = '*'
|
|
AND PARENT_OBJ_ID IS NOT NULL
|
|
AND PARENT_OBJ_ID <> '0'
|
|
AND MENU_NAME_KOR IN (
|
|
'회사관리', '부서관리', '사용자관리',
|
|
'메뉴관리', '권한관리', '권한 그룹관리'
|
|
)
|
|
"""
|
|
);
|
|
|
|
@EventListener(ApplicationReadyEvent.class)
|
|
public void run() {
|
|
log.info("[SchemaMigrator] start — {} migration(s)", MIGRATIONS.size());
|
|
|
|
String metaDb = parseMetaDbName(metaJdbcUrl);
|
|
applyTo(metaDb, "meta");
|
|
|
|
// 테넌트 목록 조회는 반드시 메타 라우팅으로
|
|
DbContextHolder.setMeta();
|
|
List<String> tenantDbs;
|
|
try {
|
|
tenantDbs = sqlSession.selectList("provisioning.listActiveDbNames");
|
|
} catch (Exception e) {
|
|
log.warn("[SchemaMigrator] tenant list query failed — skipping tenants: {}", e.getMessage());
|
|
tenantDbs = List.of();
|
|
} finally {
|
|
DbContextHolder.clear();
|
|
}
|
|
|
|
int ok = 0, fail = 0;
|
|
for (String db : tenantDbs) {
|
|
if (db == null || db.isBlank() || db.equalsIgnoreCase(metaDb)) continue;
|
|
if (applyTo(db, "tenant")) ok++; else fail++;
|
|
}
|
|
log.info("[SchemaMigrator] done — meta=done, tenants ok={}, fail={}", ok, fail);
|
|
}
|
|
|
|
private boolean applyTo(String dbName, String kind) {
|
|
String url = tenantDbSettings.buildJdbcUrl(dbName);
|
|
try (Connection c = DriverManager.getConnection(url, tenantDbSettings.username(), tenantDbSettings.password());
|
|
Statement s = c.createStatement()) {
|
|
for (String ddl : MIGRATIONS) {
|
|
s.execute(ddl);
|
|
}
|
|
log.info("[SchemaMigrator] {} db='{}' OK", kind, dbName);
|
|
return true;
|
|
} catch (Exception e) {
|
|
log.error("[SchemaMigrator] {} db='{}' FAILED: {}", kind, dbName, e.getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static String parseMetaDbName(String jdbcUrl) {
|
|
int slash = jdbcUrl.lastIndexOf('/');
|
|
if (slash < 0) return "invyone";
|
|
String tail = jdbcUrl.substring(slash + 1);
|
|
int q = tail.indexOf('?');
|
|
return q < 0 ? tail : tail.substring(0, q);
|
|
}
|
|
}
|