[Developer] #424 speed up permission object selection
This commit is contained in:
@@ -0,0 +1,50 @@
|
|||||||
|
package com.cloudhandson.vpdbackoffice.config;
|
||||||
|
|
||||||
|
import com.cloudhandson.vpdbackoffice.service.PermissionService;
|
||||||
|
import com.cloudhandson.vpdbackoffice.service.ProtectedObjectService;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import javax.sql.DataSource;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class DbPoolWarmup {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(DbPoolWarmup.class);
|
||||||
|
private final DataSource dataSource;
|
||||||
|
private final ProtectedObjectService protectedObjectService;
|
||||||
|
private final PermissionService permissionService;
|
||||||
|
|
||||||
|
public DbPoolWarmup(
|
||||||
|
DataSource dataSource,
|
||||||
|
ProtectedObjectService protectedObjectService,
|
||||||
|
PermissionService permissionService
|
||||||
|
) {
|
||||||
|
this.dataSource = dataSource;
|
||||||
|
this.protectedObjectService = protectedObjectService;
|
||||||
|
this.permissionService = permissionService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventListener(ApplicationReadyEvent.class)
|
||||||
|
public void warmup() {
|
||||||
|
try (Connection ignored = dataSource.getConnection()) {
|
||||||
|
log.info("Backoffice DB pool warmed up");
|
||||||
|
warmupBackofficeCatalog();
|
||||||
|
} catch (Exception exception) {
|
||||||
|
log.warn("Backoffice DB pool warm-up failed: {}", exception.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void warmupBackofficeCatalog() {
|
||||||
|
long started = System.nanoTime();
|
||||||
|
var objects = protectedObjectService.findEnabled();
|
||||||
|
objects.forEach(object -> protectedObjectService.findColumns(object.objectId()));
|
||||||
|
protectedObjectService.findDatabaseObjects();
|
||||||
|
permissionService.findRoles();
|
||||||
|
permissionService.findPermissionViews();
|
||||||
|
log.info("Backoffice DB catalog cache warmed up in {}ms", (System.nanoTime() - started) / 1_000_000);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,5 +36,7 @@ public interface ProtectedObjectMapper {
|
|||||||
|
|
||||||
int updateOrdsPath(@Param("objectId") long objectId, @Param("ordsPath") String ordsPath);
|
int updateOrdsPath(@Param("objectId") long objectId, @Param("ordsPath") String ordsPath);
|
||||||
|
|
||||||
|
int enableObject(@Param("objectId") long objectId);
|
||||||
|
|
||||||
int disableObject(@Param("objectId") long objectId);
|
int disableObject(@Param("objectId") long objectId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ import java.util.Arrays;
|
|||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -19,6 +21,10 @@ public class ProtectedObjectService {
|
|||||||
|
|
||||||
private final ProtectedObjectMapper mapper;
|
private final ProtectedObjectMapper mapper;
|
||||||
private final AuditService auditService;
|
private final AuditService auditService;
|
||||||
|
private volatile CacheEntry<List<DatabaseObjectOption>> databaseObjectsCache;
|
||||||
|
private final Map<String, CacheEntry<List<String>>> databaseColumnsCache = new ConcurrentHashMap<>();
|
||||||
|
private final Map<Long, CacheEntry<List<ProtectedColumn>>> protectedColumnsCache = new ConcurrentHashMap<>();
|
||||||
|
private static final long CATALOG_CACHE_MILLIS = 60_000L;
|
||||||
|
|
||||||
public ProtectedObjectService(ProtectedObjectMapper mapper, AuditService auditService) {
|
public ProtectedObjectService(ProtectedObjectMapper mapper, AuditService auditService) {
|
||||||
this.mapper = mapper;
|
this.mapper = mapper;
|
||||||
@@ -30,11 +36,24 @@ public class ProtectedObjectService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<DatabaseObjectOption> findDatabaseObjects() {
|
public List<DatabaseObjectOption> findDatabaseObjects() {
|
||||||
return mapper.findDatabaseObjects();
|
CacheEntry<List<DatabaseObjectOption>> cached = databaseObjectsCache;
|
||||||
|
if (cached != null && !cached.expired()) {
|
||||||
|
return cached.value();
|
||||||
|
}
|
||||||
|
List<DatabaseObjectOption> objects = List.copyOf(mapper.findDatabaseObjects());
|
||||||
|
databaseObjectsCache = new CacheEntry<>(objects, System.currentTimeMillis() + CATALOG_CACHE_MILLIS);
|
||||||
|
return objects;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<String> findDatabaseColumns(String owner, String objectName) {
|
public List<String> findDatabaseColumns(String owner, String objectName) {
|
||||||
return mapper.findDatabaseColumns(owner, objectName);
|
String key = owner.trim().toUpperCase(Locale.ROOT) + "." + objectName.trim().toUpperCase(Locale.ROOT);
|
||||||
|
CacheEntry<List<String>> cached = databaseColumnsCache.get(key);
|
||||||
|
if (cached != null && !cached.expired()) {
|
||||||
|
return cached.value();
|
||||||
|
}
|
||||||
|
List<String> columns = List.copyOf(mapper.findDatabaseColumns(owner, objectName));
|
||||||
|
databaseColumnsCache.put(key, new CacheEntry<>(columns, System.currentTimeMillis() + CATALOG_CACHE_MILLIS));
|
||||||
|
return columns;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ProtectedObject assertEnabled(long objectId) {
|
public ProtectedObject assertEnabled(long objectId) {
|
||||||
@@ -46,7 +65,13 @@ public class ProtectedObjectService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<ProtectedColumn> findColumns(long objectId) {
|
public List<ProtectedColumn> findColumns(long objectId) {
|
||||||
return mapper.findColumns(objectId);
|
CacheEntry<List<ProtectedColumn>> cached = protectedColumnsCache.get(objectId);
|
||||||
|
if (cached != null && !cached.expired()) {
|
||||||
|
return cached.value();
|
||||||
|
}
|
||||||
|
List<ProtectedColumn> columns = List.copyOf(mapper.findColumns(objectId));
|
||||||
|
protectedColumnsCache.put(objectId, new CacheEntry<>(columns, System.currentTimeMillis() + CATALOG_CACHE_MILLIS));
|
||||||
|
return columns;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -58,18 +83,50 @@ public class ProtectedObjectService {
|
|||||||
for (String column : splitCsv(normalized.columns())) {
|
for (String column : splitCsv(normalized.columns())) {
|
||||||
mapper.insertColumn(mapper.nextColumnId(), objectId, column, sensitive.contains(column) ? "Y" : "N");
|
mapper.insertColumn(mapper.nextColumnId(), objectId, column, sensitive.contains(column) ? "Y" : "N");
|
||||||
}
|
}
|
||||||
|
protectedColumnsCache.remove(objectId);
|
||||||
auditService.record(new AuditEvent("PROTECTED_OBJECT_CREATED", null, objectId, "SUCCESS", null, null,
|
auditService.record(new AuditEvent("PROTECTED_OBJECT_CREATED", null, objectId, "SUCCESS", null, null,
|
||||||
normalized.objectName()));
|
normalized.objectName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public ProtectedObject ensureProtectedObject(String owner, String objectName) {
|
public ProtectedObject ensureProtectedObject(String owner, String objectName) {
|
||||||
ProtectedObject existing = mapper.findByOwnerAndName(owner, objectName);
|
String normalizedOwner = owner.trim().toUpperCase(Locale.ROOT);
|
||||||
|
String normalizedObjectName = objectName.trim().toUpperCase(Locale.ROOT);
|
||||||
|
ProtectedObject existing = mapper.findByOwnerAndName(normalizedOwner, normalizedObjectName);
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
|
if (!existing.enabled()) {
|
||||||
|
mapper.enableObject(existing.objectId());
|
||||||
|
auditService.record(new AuditEvent("PROTECTED_OBJECT_RE_ENABLED", null, existing.objectId(), "SUCCESS", null,
|
||||||
|
null, existing.displayName()));
|
||||||
|
return mapper.findById(existing.objectId());
|
||||||
|
}
|
||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
throw new AppException("테이블/뷰 관리에서 실제 ORDS Path를 먼저 등록하세요: "
|
List<String> columns = findDatabaseColumns(normalizedOwner, normalizedObjectName);
|
||||||
+ owner.toUpperCase(Locale.ROOT) + "." + objectName.toUpperCase(Locale.ROOT));
|
if (columns.isEmpty()) {
|
||||||
|
throw new AppException("DB에서 컬럼 정보를 찾을 수 없습니다. 객체명과 스키마를 확인하세요: "
|
||||||
|
+ normalizedOwner + "." + normalizedObjectName);
|
||||||
|
}
|
||||||
|
ProtectedObjectCreateCommand command = new ProtectedObjectCreateCommand(
|
||||||
|
normalizedOwner,
|
||||||
|
normalizedObjectName,
|
||||||
|
defaultOrdsPath(normalizedOwner, normalizedObjectName),
|
||||||
|
String.join(",", columns),
|
||||||
|
""
|
||||||
|
);
|
||||||
|
long objectId = mapper.nextObjectId();
|
||||||
|
mapper.insertObject(objectId, command);
|
||||||
|
for (String column : columns) {
|
||||||
|
mapper.insertColumn(mapper.nextColumnId(), objectId, column, "N");
|
||||||
|
}
|
||||||
|
protectedColumnsCache.remove(objectId);
|
||||||
|
auditService.record(new AuditEvent("PROTECTED_OBJECT_AUTO_CREATED", null, objectId, "SUCCESS", null, null,
|
||||||
|
command.objectName()));
|
||||||
|
return mapper.findById(objectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String defaultOrdsPath(String owner, String objectName) {
|
||||||
|
return owner.toLowerCase(Locale.ROOT) + "/" + objectName.toLowerCase(Locale.ROOT);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ProtectedObjectCreateCommand normalizeCreateCommand(ProtectedObjectCreateCommand command) {
|
private ProtectedObjectCreateCommand normalizeCreateCommand(ProtectedObjectCreateCommand command) {
|
||||||
@@ -120,4 +177,11 @@ public class ProtectedObjectService {
|
|||||||
.forEach(result::add);
|
.forEach(result::add);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record CacheEntry<T>(T value, long expiresAt) {
|
||||||
|
|
||||||
|
boolean expired() {
|
||||||
|
return System.currentTimeMillis() > expiresAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import com.cloudhandson.vpdbackoffice.service.ProtectedObjectService;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
@@ -18,6 +20,7 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
|||||||
@Controller
|
@Controller
|
||||||
public class PermissionController {
|
public class PermissionController {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(PermissionController.class);
|
||||||
private final PermissionService permissionService;
|
private final PermissionService permissionService;
|
||||||
private final ProtectedObjectService protectedObjectService;
|
private final ProtectedObjectService protectedObjectService;
|
||||||
|
|
||||||
@@ -59,21 +62,42 @@ public class PermissionController {
|
|||||||
|
|
||||||
@GetMapping("/permissions")
|
@GetMapping("/permissions")
|
||||||
public String permissions(Model model) {
|
public String permissions(Model model) {
|
||||||
|
long started = System.nanoTime();
|
||||||
var objects = protectedObjectService.findEnabled();
|
var objects = protectedObjectService.findEnabled();
|
||||||
model.addAttribute("roles", permissionService.findRoles());
|
long objectsAt = System.nanoTime();
|
||||||
model.addAttribute("objects", objects);
|
var roles = permissionService.findRoles();
|
||||||
model.addAttribute("columnsByObject", objects.stream()
|
long rolesAt = System.nanoTime();
|
||||||
|
var columnsByObject = objects.stream()
|
||||||
.collect(Collectors.toMap(
|
.collect(Collectors.toMap(
|
||||||
object -> object.objectId(),
|
object -> object.objectId(),
|
||||||
object -> protectedObjectService.findColumns(object.objectId()).stream()
|
object -> protectedObjectService.findColumns(object.objectId()).stream()
|
||||||
.map(column -> column.columnName())
|
.map(column -> column.columnName())
|
||||||
.toList()
|
.toList()
|
||||||
)));
|
));
|
||||||
model.addAttribute("dbObjects", protectedObjectService.findDatabaseObjects());
|
long columnsAt = System.nanoTime();
|
||||||
model.addAttribute("permissions", permissionService.findPermissionViews());
|
var dbObjects = protectedObjectService.findDatabaseObjects();
|
||||||
|
long dbObjectsAt = System.nanoTime();
|
||||||
|
var permissions = permissionService.findPermissionViews();
|
||||||
|
long permissionsAt = System.nanoTime();
|
||||||
|
log.info("permissions page timings: objects={}ms roles={}ms columns={}ms dbObjects={}ms permissions={}ms total={}ms",
|
||||||
|
elapsedMillis(started, objectsAt),
|
||||||
|
elapsedMillis(objectsAt, rolesAt),
|
||||||
|
elapsedMillis(rolesAt, columnsAt),
|
||||||
|
elapsedMillis(columnsAt, dbObjectsAt),
|
||||||
|
elapsedMillis(dbObjectsAt, permissionsAt),
|
||||||
|
elapsedMillis(started, permissionsAt));
|
||||||
|
model.addAttribute("roles", roles);
|
||||||
|
model.addAttribute("objects", objects);
|
||||||
|
model.addAttribute("columnsByObject", columnsByObject);
|
||||||
|
model.addAttribute("dbObjects", dbObjects);
|
||||||
|
model.addAttribute("permissions", permissions);
|
||||||
return "permissions";
|
return "permissions";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private long elapsedMillis(long from, long to) {
|
||||||
|
return (to - from) / 1_000_000;
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/permissions")
|
@PostMapping("/permissions")
|
||||||
public String save(
|
public String save(
|
||||||
@RequestParam long roleId,
|
@RequestParam long roleId,
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ spring:
|
|||||||
driver-class-name: oracle.jdbc.OracleDriver
|
driver-class-name: oracle.jdbc.OracleDriver
|
||||||
hikari:
|
hikari:
|
||||||
pool-name: vpd-backoffice-pool
|
pool-name: vpd-backoffice-pool
|
||||||
maximum-pool-size: ${BACKOFFICE_DB_POOL_MAX:5}
|
maximum-pool-size: ${BACKOFFICE_DB_POOL_MAX:10}
|
||||||
minimum-idle: ${BACKOFFICE_DB_POOL_MIN:1}
|
minimum-idle: ${BACKOFFICE_DB_POOL_MIN:2}
|
||||||
connection-timeout: 10000
|
connection-timeout: 10000
|
||||||
|
idle-timeout: ${BACKOFFICE_DB_POOL_IDLE_TIMEOUT_MS:300000}
|
||||||
|
keepalive-time: ${BACKOFFICE_DB_POOL_KEEPALIVE_MS:120000}
|
||||||
thymeleaf:
|
thymeleaf:
|
||||||
cache: false
|
cache: false
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,13 @@
|
|||||||
WHERE object_type IN ('TABLE', 'VIEW')
|
WHERE object_type IN ('TABLE', 'VIEW')
|
||||||
AND owner NOT IN ('SYS', 'SYSTEM', 'ORDS_METADATA', 'ORDS_PUBLIC_USER')
|
AND owner NOT IN ('SYS', 'SYSTEM', 'ORDS_METADATA', 'ORDS_PUBLIC_USER')
|
||||||
AND object_name NOT LIKE 'BIN$%'
|
AND object_name NOT LIKE 'BIN$%'
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM cb_protected_object po
|
||||||
|
WHERE po.owner = all_objects.owner
|
||||||
|
AND po.object_name = all_objects.object_name
|
||||||
|
AND po.enabled_yn = 'Y'
|
||||||
|
)
|
||||||
ORDER BY owner, object_type, object_name
|
ORDER BY owner, object_type, object_name
|
||||||
FETCH FIRST 500 ROWS ONLY
|
FETCH FIRST 500 ROWS ONLY
|
||||||
</select>
|
</select>
|
||||||
@@ -77,6 +84,12 @@
|
|||||||
WHERE object_id = #{objectId,jdbcType=NUMERIC}
|
WHERE object_id = #{objectId,jdbcType=NUMERIC}
|
||||||
</update>
|
</update>
|
||||||
|
|
||||||
|
<update id="enableObject">
|
||||||
|
UPDATE cb_protected_object
|
||||||
|
SET enabled_yn = 'Y'
|
||||||
|
WHERE object_id = #{objectId}
|
||||||
|
</update>
|
||||||
|
|
||||||
<update id="disableObject">
|
<update id="disableObject">
|
||||||
UPDATE cb_protected_object
|
UPDATE cb_protected_object
|
||||||
SET enabled_yn = 'N'
|
SET enabled_yn = 'N'
|
||||||
|
|||||||
Reference in New Issue
Block a user