refs #703: route schema metadata through MyBatis
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
4. Data Redaction 동기화는 `SGMP_POC`의 실제 게임 사용자·판매 데이터 컬럼만 관리 대상으로 삼는다.
|
||||
5. 내부 호환용 `CB_*` 뷰와 과거 SQL 이력은 실행 경로에서 제외한다. Smilegate 공개 화면·MCP 설정은 이력의 고객 데이터나 endpoint를 참조하지 않는다.
|
||||
6. Streamlit 외피는 Smilegate 프로필·게임 데이터 시나리오·`oracle.select_ai.smilegate_game_text2sql` MCP 하나만 노출한다. 이전 고객용 토큰 프리셋 및 감사·보안관리 탭은 기본 실행 경로에서 제외한다.
|
||||
7. `/schema-metadata`의 테이블 comment·컬럼 comment·annotation 조회와 저장 DDL은 모두 `SchemaMetadataMapper`로 수행한다. 메타데이터 조회는 `SGMP_POC` owner와 허용된 테이블 목록으로 한정한다.
|
||||
|
||||
## 설계 결정
|
||||
|
||||
@@ -51,6 +52,10 @@ MCP tool은 `SGMP_POC_HAIKU45` 프로파일을 기준으로 게임 데이터의
|
||||
|
||||
마스킹 동기화 대상 owner는 `SGMP_POC`다. 관리 정책은 실제 컬럼 존재 여부를 검증한 뒤 사용자 식별자와 거래 사용자 식별자에만 적용한다. 대상에 없는 규칙은 DBMS_REDACT 호출 전에 화면 설정 오류로 처리한다.
|
||||
|
||||
### 5. 스키마 메타데이터 접근은 MyBatis로 통일한다
|
||||
|
||||
`/schema-metadata`는 화면 카드 목록을 정적 허용 목록에서 만들고, 선택된 테이블의 comment·컬럼·annotation만 조회한다. 서비스 계층에는 JDBC 직접 실행을 두지 않는다. MyBatis mapper의 모든 사전 조회는 `owner = 'SGMP_POC'` 조건을 갖고, DDL에 쓰이는 테이블·컬럼·annotation 이름은 호출 전에 대문자 식별자 규칙과 허용 테이블 목록으로 검증한다.
|
||||
|
||||
## 변경 파일과 책임
|
||||
|
||||
| 영역 | 파일 | 변경 |
|
||||
@@ -61,6 +66,7 @@ MCP tool은 `SGMP_POC_HAIKU45` 프로파일을 기준으로 게임 데이터의
|
||||
| MCP 화면 | `templates/mcp-sse.html`, `McpSseService.java`, `SmilegateSelectAiService.java` | 게임 데이터 Select AI 도구, 토큰 검증 및 SHOWSQL 생성 |
|
||||
| 보안 스크립트 화면 | `SecuritySqlScriptService.java` | UI에 노출되는 KB 설명을 게임 데이터 설명으로 교체 |
|
||||
| Streamlit 외피 | `poc4_active_source_20260714/config/`, `apps/poc4/mcp_discovery_ui.py` | Smilegate 로그인/헤더/시나리오와 단일 게임 Text2SQL MCP 계약 적용 |
|
||||
| 스키마 메타데이터 | `SchemaMetadataService.java`, `SchemaMetadataMapper.java`, `SchemaMetadataMapper.xml` | 직접 JDBC 제거, MyBatis 조회·DDL 통일, `SGMP_POC` owner 조건 강제 |
|
||||
|
||||
## 완료 기준
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.schemametadata;
|
||||
|
||||
/**
|
||||
* Database row from ALL_ANNOTATIONS_USAGE for one table or column annotation.
|
||||
*/
|
||||
public record SchemaMetadataAnnotationRow(
|
||||
String columnName,
|
||||
String annotationName,
|
||||
String annotationValue
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.schemametadata;
|
||||
|
||||
/**
|
||||
* Database row used only while assembling the schema metadata screen.
|
||||
*/
|
||||
public record SchemaMetadataColumnRow(
|
||||
String columnName,
|
||||
String dataType,
|
||||
String nullable,
|
||||
String comment
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.cloudhandson.vpdbackoffice.mapper;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.schemametadata.SchemaMetadataAnnotationRow;
|
||||
import com.cloudhandson.vpdbackoffice.domain.schemametadata.SchemaMetadataColumnRow;
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
/**
|
||||
* All database access for the schema metadata screen. Identifier parameters
|
||||
* are validated against the screen's closed table list before mapper calls.
|
||||
*/
|
||||
@Mapper
|
||||
public interface SchemaMetadataMapper {
|
||||
|
||||
String findTableComment(@Param("owner") String owner, @Param("tableName") String tableName);
|
||||
|
||||
List<SchemaMetadataColumnRow> findColumns(
|
||||
@Param("owner") String owner,
|
||||
@Param("tableName") String tableName
|
||||
);
|
||||
|
||||
List<SchemaMetadataAnnotationRow> findAnnotations(
|
||||
@Param("owner") String owner,
|
||||
@Param("tableName") String tableName
|
||||
);
|
||||
|
||||
int countColumn(
|
||||
@Param("owner") String owner,
|
||||
@Param("tableName") String tableName,
|
||||
@Param("columnName") String columnName
|
||||
);
|
||||
|
||||
int countTableAnnotation(
|
||||
@Param("owner") String owner,
|
||||
@Param("tableName") String tableName,
|
||||
@Param("annotationName") String annotationName
|
||||
);
|
||||
|
||||
int countColumnAnnotation(
|
||||
@Param("owner") String owner,
|
||||
@Param("tableName") String tableName,
|
||||
@Param("columnName") String columnName,
|
||||
@Param("annotationName") String annotationName
|
||||
);
|
||||
|
||||
void updateTableComment(
|
||||
@Param("owner") String owner,
|
||||
@Param("tableName") String tableName,
|
||||
@Param("commentLiteral") String commentLiteral
|
||||
);
|
||||
|
||||
void updateColumnComment(
|
||||
@Param("owner") String owner,
|
||||
@Param("tableName") String tableName,
|
||||
@Param("columnName") String columnName,
|
||||
@Param("commentLiteral") String commentLiteral
|
||||
);
|
||||
|
||||
void dropTableAnnotation(
|
||||
@Param("owner") String owner,
|
||||
@Param("tableName") String tableName,
|
||||
@Param("annotationName") String annotationName
|
||||
);
|
||||
|
||||
void addTableAnnotation(
|
||||
@Param("owner") String owner,
|
||||
@Param("tableName") String tableName,
|
||||
@Param("annotationName") String annotationName,
|
||||
@Param("annotationValueLiteral") String annotationValueLiteral
|
||||
);
|
||||
|
||||
void dropColumnAnnotation(
|
||||
@Param("owner") String owner,
|
||||
@Param("tableName") String tableName,
|
||||
@Param("columnName") String columnName,
|
||||
@Param("annotationName") String annotationName
|
||||
);
|
||||
|
||||
void addColumnAnnotation(
|
||||
@Param("owner") String owner,
|
||||
@Param("tableName") String tableName,
|
||||
@Param("columnName") String columnName,
|
||||
@Param("annotationName") String annotationName,
|
||||
@Param("annotationValueLiteral") String annotationValueLiteral
|
||||
);
|
||||
}
|
||||
@@ -1,20 +1,25 @@
|
||||
package com.cloudhandson.vpdbackoffice.service;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.schemametadata.SchemaAnnotation;
|
||||
import com.cloudhandson.vpdbackoffice.domain.schemametadata.SchemaMetadataAnnotationRow;
|
||||
import com.cloudhandson.vpdbackoffice.domain.schemametadata.SchemaMetadataColumn;
|
||||
import com.cloudhandson.vpdbackoffice.domain.schemametadata.SchemaMetadataColumnRow;
|
||||
import com.cloudhandson.vpdbackoffice.domain.schemametadata.SchemaMetadataView;
|
||||
import com.cloudhandson.vpdbackoffice.domain.structured.StructuredDataTable;
|
||||
import com.cloudhandson.vpdbackoffice.mapper.SchemaMetadataMapper;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* Screen-level orchestration for schema metadata. Database reads and DDL are
|
||||
* intentionally delegated only to {@link SchemaMetadataMapper}.
|
||||
*/
|
||||
@Service
|
||||
public class SchemaMetadataService {
|
||||
|
||||
@@ -23,11 +28,14 @@ public class SchemaMetadataService {
|
||||
private static final int MAX_ANNOTATION_VALUE_LENGTH = 4000;
|
||||
private static final Pattern ORACLE_SIMPLE_NAME = Pattern.compile("[A-Z][A-Z0-9_$#]{0,127}");
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final SchemaMetadataMapper mapper;
|
||||
private final StructuredDataService structuredDataService;
|
||||
|
||||
public SchemaMetadataService(JdbcTemplate jdbcTemplate, StructuredDataService structuredDataService) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
public SchemaMetadataService(
|
||||
SchemaMetadataMapper mapper,
|
||||
StructuredDataService structuredDataService
|
||||
) {
|
||||
this.mapper = mapper;
|
||||
this.structuredDataService = structuredDataService;
|
||||
}
|
||||
|
||||
@@ -41,13 +49,12 @@ public class SchemaMetadataService {
|
||||
|
||||
public SchemaMetadataView find(String tableKey) {
|
||||
StructuredDataTable table = structuredDataService.requireTable(tableKey);
|
||||
String tableName = table.tableName();
|
||||
String tableComment = tableComment(tableName);
|
||||
String tableName = requireSimpleName(table.tableName(), "table name");
|
||||
Map<String, List<SchemaAnnotation>> annotations = annotationsByTarget(tableName);
|
||||
List<SchemaMetadataColumn> columns = columns(tableName, annotations);
|
||||
return new SchemaMetadataView(
|
||||
table,
|
||||
nullToEmpty(tableComment),
|
||||
nullToEmpty(mapper.findTableComment(OWNER, tableName)),
|
||||
annotations.getOrDefault(tableTargetKey(), List.of()),
|
||||
columns
|
||||
);
|
||||
@@ -56,24 +63,24 @@ public class SchemaMetadataService {
|
||||
@Transactional
|
||||
public void updateTableComment(String tableKey, String comment) {
|
||||
StructuredDataTable table = structuredDataService.requireTable(tableKey);
|
||||
String tableName = requireSimpleName(table.tableName(), "table name");
|
||||
String normalizedComment = normalizeText(comment, MAX_COMMENT_LENGTH, "테이블 comment");
|
||||
jdbcTemplate.execute("COMMENT ON TABLE " + qualifiedTable(table.tableName())
|
||||
+ " IS " + quoteLiteral(normalizedComment));
|
||||
mapper.updateTableComment(OWNER, tableName, quoteLiteral(normalizedComment));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void updateColumnComment(String tableKey, String columnName, String comment) {
|
||||
StructuredDataTable table = structuredDataService.requireTable(tableKey);
|
||||
String column = requireColumn(table.tableName(), columnName);
|
||||
String tableName = requireSimpleName(table.tableName(), "table name");
|
||||
String column = requireColumn(tableName, columnName);
|
||||
String normalizedComment = normalizeText(comment, MAX_COMMENT_LENGTH, "컬럼 comment");
|
||||
jdbcTemplate.execute("COMMENT ON COLUMN " + qualifiedTable(table.tableName()) + "."
|
||||
+ quoteName(column) + " IS " + quoteLiteral(normalizedComment));
|
||||
mapper.updateColumnComment(OWNER, tableName, column, quoteLiteral(normalizedComment));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void updateTableAnnotation(String tableKey, String annotationName, String annotationValue) {
|
||||
StructuredDataTable table = structuredDataService.requireTable(tableKey);
|
||||
updateAnnotation(table.tableName(), null, annotationName, annotationValue);
|
||||
updateAnnotation(requireSimpleName(table.tableName(), "table name"), null, annotationName, annotationValue);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -84,8 +91,9 @@ public class SchemaMetadataService {
|
||||
String annotationValue
|
||||
) {
|
||||
StructuredDataTable table = structuredDataService.requireTable(tableKey);
|
||||
String column = requireColumn(table.tableName(), columnName);
|
||||
updateAnnotation(table.tableName(), column, annotationName, annotationValue);
|
||||
String tableName = requireSimpleName(table.tableName(), "table name");
|
||||
String column = requireColumn(tableName, columnName);
|
||||
updateAnnotation(tableName, column, annotationName, annotationValue);
|
||||
}
|
||||
|
||||
private void updateAnnotation(
|
||||
@@ -97,75 +105,55 @@ public class SchemaMetadataService {
|
||||
String key = requireSimpleName(annotationName, "annotation name");
|
||||
String value = normalizeText(annotationValue, MAX_ANNOTATION_VALUE_LENGTH, "annotation value");
|
||||
if (annotationExists(tableName, columnName, key)) {
|
||||
jdbcTemplate.execute(annotationSql(tableName, columnName, "DROP " + quoteName(key)));
|
||||
if (columnName == null) {
|
||||
mapper.dropTableAnnotation(OWNER, tableName, key);
|
||||
} else {
|
||||
mapper.dropColumnAnnotation(OWNER, tableName, columnName, key);
|
||||
}
|
||||
}
|
||||
if (!value.isBlank()) {
|
||||
jdbcTemplate.execute(annotationSql(tableName, columnName,
|
||||
"ADD " + quoteName(key) + " " + quoteLiteral(value)));
|
||||
if (columnName == null) {
|
||||
mapper.addTableAnnotation(OWNER, tableName, key, quoteLiteral(value));
|
||||
} else {
|
||||
mapper.addColumnAnnotation(OWNER, tableName, columnName, key, quoteLiteral(value));
|
||||
}
|
||||
}
|
||||
|
||||
private String tableComment(String tableName) {
|
||||
List<String> values = jdbcTemplate.query("""
|
||||
SELECT comments
|
||||
FROM all_tab_comments
|
||||
WHERE owner = ?
|
||||
AND table_name = ?
|
||||
""", (rs, rowNum) -> rs.getString(1), OWNER, tableName);
|
||||
return values.isEmpty() ? "" : values.getFirst();
|
||||
}
|
||||
|
||||
private List<SchemaMetadataColumn> columns(
|
||||
String tableName,
|
||||
Map<String, List<SchemaAnnotation>> annotations
|
||||
) {
|
||||
return jdbcTemplate.query("""
|
||||
SELECT c.column_name,
|
||||
CASE
|
||||
WHEN c.data_type IN ('VARCHAR2', 'CHAR', 'NVARCHAR2', 'NCHAR')
|
||||
THEN c.data_type || '(' || c.char_length || ')'
|
||||
WHEN c.data_type = 'NUMBER' AND c.data_precision IS NOT NULL AND c.data_scale IS NOT NULL
|
||||
THEN c.data_type || '(' || c.data_precision || ',' || c.data_scale || ')'
|
||||
WHEN c.data_type = 'NUMBER' AND c.data_precision IS NOT NULL
|
||||
THEN c.data_type || '(' || c.data_precision || ')'
|
||||
ELSE c.data_type
|
||||
END AS display_type,
|
||||
c.nullable,
|
||||
cc.comments
|
||||
FROM all_tab_columns c
|
||||
LEFT JOIN all_col_comments cc
|
||||
ON cc.owner = c.owner
|
||||
AND cc.table_name = c.table_name
|
||||
AND cc.column_name = c.column_name
|
||||
WHERE c.owner = ?
|
||||
AND c.table_name = ?
|
||||
ORDER BY c.column_id
|
||||
""", (rs, rowNum) -> new SchemaMetadataColumn(
|
||||
rs.getString("column_name"),
|
||||
rs.getString("display_type"),
|
||||
"Y".equalsIgnoreCase(rs.getString("nullable")),
|
||||
nullToEmpty(rs.getString("comments")),
|
||||
annotations.getOrDefault(columnTargetKey(rs.getString("column_name")), List.of())
|
||||
), OWNER, tableName);
|
||||
return mapper.findColumns(OWNER, tableName).stream()
|
||||
.map(row -> toColumn(row, annotations))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private SchemaMetadataColumn toColumn(
|
||||
SchemaMetadataColumnRow row,
|
||||
Map<String, List<SchemaAnnotation>> annotations
|
||||
) {
|
||||
String columnName = requireSimpleName(row.columnName(), "column name");
|
||||
return new SchemaMetadataColumn(
|
||||
columnName,
|
||||
row.dataType(),
|
||||
"Y".equalsIgnoreCase(row.nullable()),
|
||||
nullToEmpty(row.comment()),
|
||||
annotations.getOrDefault(columnTargetKey(columnName), List.of())
|
||||
);
|
||||
}
|
||||
|
||||
private Map<String, List<SchemaAnnotation>> annotationsByTarget(String tableName) {
|
||||
Map<String, LinkedHashMap<String, List<String>>> grouped = new LinkedHashMap<>();
|
||||
jdbcTemplate.query("""
|
||||
SELECT column_name, annotation_name, annotation_value
|
||||
FROM all_annotations_usage
|
||||
WHERE object_name = ?
|
||||
AND object_type = 'TABLE'
|
||||
ORDER BY column_name NULLS FIRST, annotation_name, annotation_value
|
||||
""", rs -> {
|
||||
String target = rs.getString("column_name") == null
|
||||
for (SchemaMetadataAnnotationRow row : mapper.findAnnotations(OWNER, tableName)) {
|
||||
String target = row.columnName() == null
|
||||
? tableTargetKey()
|
||||
: columnTargetKey(rs.getString("column_name"));
|
||||
: columnTargetKey(row.columnName());
|
||||
grouped
|
||||
.computeIfAbsent(target, ignored -> new LinkedHashMap<>())
|
||||
.computeIfAbsent(rs.getString("annotation_name"), ignored -> new ArrayList<>())
|
||||
.add(nullToEmpty(rs.getString("annotation_value")));
|
||||
}, tableName);
|
||||
.computeIfAbsent(row.annotationName(), ignored -> new ArrayList<>())
|
||||
.add(nullToEmpty(row.annotationValue()));
|
||||
}
|
||||
|
||||
Map<String, List<SchemaAnnotation>> result = new LinkedHashMap<>();
|
||||
grouped.forEach((target, valuesByName) -> {
|
||||
@@ -180,44 +168,14 @@ public class SchemaMetadataService {
|
||||
}
|
||||
|
||||
private boolean annotationExists(String tableName, String columnName, String annotationName) {
|
||||
Integer count = columnName == null
|
||||
? jdbcTemplate.queryForObject("""
|
||||
SELECT COUNT(*)
|
||||
FROM all_annotations_usage
|
||||
WHERE object_name = ?
|
||||
AND object_type = 'TABLE'
|
||||
AND annotation_name = ?
|
||||
AND column_name IS NULL
|
||||
""", Integer.class, tableName, annotationName)
|
||||
: jdbcTemplate.queryForObject("""
|
||||
SELECT COUNT(*)
|
||||
FROM all_annotations_usage
|
||||
WHERE object_name = ?
|
||||
AND object_type = 'TABLE'
|
||||
AND annotation_name = ?
|
||||
AND column_name = ?
|
||||
""", Integer.class, tableName, annotationName, columnName);
|
||||
return count != null && count > 0;
|
||||
}
|
||||
|
||||
private String annotationSql(String tableName, String columnName, String operation) {
|
||||
if (columnName == null) {
|
||||
return "ALTER TABLE " + qualifiedTable(tableName) + " ANNOTATIONS (" + operation + ")";
|
||||
}
|
||||
return "ALTER TABLE " + qualifiedTable(tableName) + " MODIFY " + quoteName(columnName)
|
||||
+ " ANNOTATIONS (" + operation + ")";
|
||||
return columnName == null
|
||||
? mapper.countTableAnnotation(OWNER, tableName, annotationName) > 0
|
||||
: mapper.countColumnAnnotation(OWNER, tableName, columnName, annotationName) > 0;
|
||||
}
|
||||
|
||||
private String requireColumn(String tableName, String columnName) {
|
||||
String column = requireSimpleName(columnName, "column name");
|
||||
Integer count = jdbcTemplate.queryForObject("""
|
||||
SELECT COUNT(*)
|
||||
FROM all_tab_columns
|
||||
WHERE owner = ?
|
||||
AND table_name = ?
|
||||
AND column_name = ?
|
||||
""", Integer.class, OWNER, tableName, column);
|
||||
if (count == null || count == 0) {
|
||||
if (mapper.countColumn(OWNER, tableName, column) == 0) {
|
||||
throw new AppException("선택한 테이블에 존재하지 않는 컬럼입니다.");
|
||||
}
|
||||
return column;
|
||||
@@ -242,14 +200,6 @@ public class SchemaMetadataService {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private String qualifiedTable(String tableName) {
|
||||
return quoteName(OWNER) + "." + quoteName(requireSimpleName(tableName, "table name"));
|
||||
}
|
||||
|
||||
private String quoteName(String value) {
|
||||
return "\"" + value.replace("\"", "\"\"") + "\"";
|
||||
}
|
||||
|
||||
private String quoteLiteral(String value) {
|
||||
return "'" + value.replace("'", "''") + "'";
|
||||
}
|
||||
|
||||
103
src/main/resources/mapper/SchemaMetadataMapper.xml
Normal file
103
src/main/resources/mapper/SchemaMetadataMapper.xml
Normal file
@@ -0,0 +1,103 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.cloudhandson.vpdbackoffice.mapper.SchemaMetadataMapper">
|
||||
<select id="findTableComment" resultType="string">
|
||||
SELECT comments
|
||||
FROM all_tab_comments
|
||||
WHERE owner = #{owner,jdbcType=VARCHAR}
|
||||
AND table_name = #{tableName,jdbcType=VARCHAR}
|
||||
</select>
|
||||
|
||||
<select id="findColumns"
|
||||
resultType="com.cloudhandson.vpdbackoffice.domain.schemametadata.SchemaMetadataColumnRow">
|
||||
SELECT c.column_name AS column_name,
|
||||
CASE
|
||||
WHEN c.data_type IN ('VARCHAR2', 'CHAR', 'NVARCHAR2', 'NCHAR')
|
||||
THEN c.data_type || '(' || c.char_length || ')'
|
||||
WHEN c.data_type = 'NUMBER' AND c.data_precision IS NOT NULL AND c.data_scale IS NOT NULL
|
||||
THEN c.data_type || '(' || c.data_precision || ',' || c.data_scale || ')'
|
||||
WHEN c.data_type = 'NUMBER' AND c.data_precision IS NOT NULL
|
||||
THEN c.data_type || '(' || c.data_precision || ')'
|
||||
ELSE c.data_type
|
||||
END AS data_type,
|
||||
c.nullable,
|
||||
cc.comments
|
||||
FROM all_tab_columns c
|
||||
LEFT JOIN all_col_comments cc
|
||||
ON cc.owner = c.owner
|
||||
AND cc.table_name = c.table_name
|
||||
AND cc.column_name = c.column_name
|
||||
WHERE c.owner = #{owner,jdbcType=VARCHAR}
|
||||
AND c.table_name = #{tableName,jdbcType=VARCHAR}
|
||||
ORDER BY c.column_id
|
||||
</select>
|
||||
|
||||
<select id="findAnnotations"
|
||||
resultType="com.cloudhandson.vpdbackoffice.domain.schemametadata.SchemaMetadataAnnotationRow">
|
||||
SELECT column_name,
|
||||
annotation_name,
|
||||
annotation_value
|
||||
FROM all_annotations_usage
|
||||
WHERE object_owner = #{owner,jdbcType=VARCHAR}
|
||||
AND object_name = #{tableName,jdbcType=VARCHAR}
|
||||
AND object_type = 'TABLE'
|
||||
ORDER BY column_name NULLS FIRST, annotation_name, annotation_value
|
||||
</select>
|
||||
|
||||
<select id="countColumn" resultType="int">
|
||||
SELECT COUNT(*)
|
||||
FROM all_tab_columns
|
||||
WHERE owner = #{owner,jdbcType=VARCHAR}
|
||||
AND table_name = #{tableName,jdbcType=VARCHAR}
|
||||
AND column_name = #{columnName,jdbcType=VARCHAR}
|
||||
</select>
|
||||
|
||||
<select id="countTableAnnotation" resultType="int">
|
||||
SELECT COUNT(*)
|
||||
FROM all_annotations_usage
|
||||
WHERE object_owner = #{owner,jdbcType=VARCHAR}
|
||||
AND object_name = #{tableName,jdbcType=VARCHAR}
|
||||
AND object_type = 'TABLE'
|
||||
AND annotation_name = #{annotationName,jdbcType=VARCHAR}
|
||||
AND column_name IS NULL
|
||||
</select>
|
||||
|
||||
<select id="countColumnAnnotation" resultType="int">
|
||||
SELECT COUNT(*)
|
||||
FROM all_annotations_usage
|
||||
WHERE object_owner = #{owner,jdbcType=VARCHAR}
|
||||
AND object_name = #{tableName,jdbcType=VARCHAR}
|
||||
AND object_type = 'TABLE'
|
||||
AND annotation_name = #{annotationName,jdbcType=VARCHAR}
|
||||
AND column_name = #{columnName,jdbcType=VARCHAR}
|
||||
</select>
|
||||
|
||||
<!-- Identifiers are server-side validated names. Oracle DDL comments require a literal. -->
|
||||
<update id="updateTableComment">
|
||||
COMMENT ON TABLE "${owner}"."${tableName}" IS ${commentLiteral}
|
||||
</update>
|
||||
|
||||
<update id="updateColumnComment">
|
||||
COMMENT ON COLUMN "${owner}"."${tableName}"."${columnName}" IS ${commentLiteral}
|
||||
</update>
|
||||
|
||||
<update id="dropTableAnnotation">
|
||||
ALTER TABLE "${owner}"."${tableName}" ANNOTATIONS (DROP "${annotationName}")
|
||||
</update>
|
||||
|
||||
<update id="addTableAnnotation">
|
||||
ALTER TABLE "${owner}"."${tableName}"
|
||||
ANNOTATIONS (ADD "${annotationName}" ${annotationValueLiteral})
|
||||
</update>
|
||||
|
||||
<update id="dropColumnAnnotation">
|
||||
ALTER TABLE "${owner}"."${tableName}" MODIFY "${columnName}"
|
||||
ANNOTATIONS (DROP "${annotationName}")
|
||||
</update>
|
||||
|
||||
<update id="addColumnAnnotation">
|
||||
ALTER TABLE "${owner}"."${tableName}" MODIFY "${columnName}"
|
||||
ANNOTATIONS (ADD "${annotationName}" ${annotationValueLiteral})
|
||||
</update>
|
||||
</mapper>
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.cloudhandson.vpdbackoffice.service;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.schemametadata.SchemaMetadataAnnotationRow;
|
||||
import com.cloudhandson.vpdbackoffice.domain.schemametadata.SchemaMetadataColumnRow;
|
||||
import com.cloudhandson.vpdbackoffice.domain.schemametadata.SchemaMetadataView;
|
||||
import com.cloudhandson.vpdbackoffice.domain.structured.StructuredDataTable;
|
||||
import com.cloudhandson.vpdbackoffice.mapper.SchemaMetadataMapper;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class SchemaMetadataServiceTest {
|
||||
|
||||
private static final StructuredDataTable GAME_USERS = new StructuredDataTable(
|
||||
"game-users", "CZN_COMN_USER_MST", "게임 사용자", "카제나 게임 사용자 마스터");
|
||||
|
||||
private SchemaMetadataMapper mapper;
|
||||
private SchemaMetadataService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mapper = mock(SchemaMetadataMapper.class);
|
||||
StructuredDataService structuredDataService = mock(StructuredDataService.class);
|
||||
when(structuredDataService.requireTable("game-users")).thenReturn(GAME_USERS);
|
||||
when(structuredDataService.tables()).thenReturn(List.of(GAME_USERS));
|
||||
when(structuredDataService.defaultKey()).thenReturn("game-users");
|
||||
service = new SchemaMetadataService(mapper, structuredDataService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void readsOnlyTheSelectedSmilegateTableThroughMapper() {
|
||||
when(mapper.findTableComment("SGMP_POC", "CZN_COMN_USER_MST"))
|
||||
.thenReturn("게임별 사용자 기준 정보");
|
||||
when(mapper.findAnnotations("SGMP_POC", "CZN_COMN_USER_MST")).thenReturn(List.of(
|
||||
new SchemaMetadataAnnotationRow(null, "BUSINESS_TERM", "게임 사용자"),
|
||||
new SchemaMetadataAnnotationRow("GUID", "BUSINESS_TERM", "사용자 고유 식별자"),
|
||||
new SchemaMetadataAnnotationRow("GUID", "BUSINESS_TERM", "고객 계정 식별자")
|
||||
));
|
||||
when(mapper.findColumns("SGMP_POC", "CZN_COMN_USER_MST")).thenReturn(List.of(
|
||||
new SchemaMetadataColumnRow("GUID", "VARCHAR2(64)", "N", "사용자 GUID")
|
||||
));
|
||||
|
||||
SchemaMetadataView view = service.find("game-users");
|
||||
|
||||
assertThat(view.tableComment()).isEqualTo("게임별 사용자 기준 정보");
|
||||
assertThat(view.tableAnnotations()).singleElement()
|
||||
.extracting(annotation -> annotation.name(), annotation -> annotation.value())
|
||||
.containsExactly("BUSINESS_TERM", "게임 사용자");
|
||||
assertThat(view.columns()).singleElement()
|
||||
.satisfies(column -> {
|
||||
assertThat(column.columnName()).isEqualTo("GUID");
|
||||
assertThat(column.nullable()).isFalse();
|
||||
assertThat(column.annotations()).singleElement()
|
||||
.extracting(annotation -> annotation.value())
|
||||
.isEqualTo("사용자 고유 식별자\n--- duplicate annotation value ---\n고객 계정 식별자");
|
||||
});
|
||||
verify(mapper).findAnnotations("SGMP_POC", "CZN_COMN_USER_MST");
|
||||
verify(mapper).findColumns("SGMP_POC", "CZN_COMN_USER_MST");
|
||||
}
|
||||
|
||||
@Test
|
||||
void replacesAnExistingColumnAnnotationUsingMapperDdl() {
|
||||
when(mapper.countColumn("SGMP_POC", "CZN_COMN_USER_MST", "GUID")).thenReturn(1);
|
||||
when(mapper.countColumnAnnotation("SGMP_POC", "CZN_COMN_USER_MST", "GUID", "BUSINESS_TERM"))
|
||||
.thenReturn(1);
|
||||
|
||||
service.updateColumnAnnotation("game-users", "guid", "business_term", "사용자 고유 식별자");
|
||||
|
||||
verify(mapper).dropColumnAnnotation("SGMP_POC", "CZN_COMN_USER_MST", "GUID", "BUSINESS_TERM");
|
||||
verify(mapper).addColumnAnnotation(
|
||||
"SGMP_POC", "CZN_COMN_USER_MST", "GUID", "BUSINESS_TERM", "'사용자 고유 식별자'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void clearsAnExistingTableAnnotationWithoutAddingAnEmptyValue() {
|
||||
when(mapper.countTableAnnotation("SGMP_POC", "CZN_COMN_USER_MST", "BUSINESS_TERM"))
|
||||
.thenReturn(1);
|
||||
|
||||
service.updateTableAnnotation("game-users", "business_term", " ");
|
||||
|
||||
verify(mapper).dropTableAnnotation("SGMP_POC", "CZN_COMN_USER_MST", "BUSINESS_TERM");
|
||||
verify(mapper, never()).addTableAnnotation(anyString(), anyString(), anyString(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsAColumnThatIsNotPresentInTheSelectedTable() {
|
||||
when(mapper.countColumn("SGMP_POC", "CZN_COMN_USER_MST", "UNKNOWN_COL")).thenReturn(0);
|
||||
|
||||
assertThatThrownBy(() -> service.updateColumnComment("game-users", "unknown_col", "설명"))
|
||||
.isInstanceOf(AppException.class)
|
||||
.hasMessage("선택한 테이블에 존재하지 않는 컬럼입니다.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user