[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 enableObject(@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.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -19,6 +21,10 @@ public class ProtectedObjectService {
|
||||
|
||||
private final ProtectedObjectMapper mapper;
|
||||
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) {
|
||||
this.mapper = mapper;
|
||||
@@ -30,11 +36,24 @@ public class ProtectedObjectService {
|
||||
}
|
||||
|
||||
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) {
|
||||
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) {
|
||||
@@ -46,7 +65,13 @@ public class ProtectedObjectService {
|
||||
}
|
||||
|
||||
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
|
||||
@@ -58,18 +83,50 @@ public class ProtectedObjectService {
|
||||
for (String column : splitCsv(normalized.columns())) {
|
||||
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,
|
||||
normalized.objectName()));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
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.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;
|
||||
}
|
||||
throw new AppException("테이블/뷰 관리에서 실제 ORDS Path를 먼저 등록하세요: "
|
||||
+ owner.toUpperCase(Locale.ROOT) + "." + objectName.toUpperCase(Locale.ROOT));
|
||||
List<String> columns = findDatabaseColumns(normalizedOwner, normalizedObjectName);
|
||||
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) {
|
||||
@@ -120,4 +177,11 @@ public class ProtectedObjectService {
|
||||
.forEach(result::add);
|
||||
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.List;
|
||||
import java.util.stream.Collectors;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -18,6 +20,7 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
@Controller
|
||||
public class PermissionController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(PermissionController.class);
|
||||
private final PermissionService permissionService;
|
||||
private final ProtectedObjectService protectedObjectService;
|
||||
|
||||
@@ -59,21 +62,42 @@ public class PermissionController {
|
||||
|
||||
@GetMapping("/permissions")
|
||||
public String permissions(Model model) {
|
||||
long started = System.nanoTime();
|
||||
var objects = protectedObjectService.findEnabled();
|
||||
model.addAttribute("roles", permissionService.findRoles());
|
||||
model.addAttribute("objects", objects);
|
||||
model.addAttribute("columnsByObject", objects.stream()
|
||||
long objectsAt = System.nanoTime();
|
||||
var roles = permissionService.findRoles();
|
||||
long rolesAt = System.nanoTime();
|
||||
var columnsByObject = objects.stream()
|
||||
.collect(Collectors.toMap(
|
||||
object -> object.objectId(),
|
||||
object -> protectedObjectService.findColumns(object.objectId()).stream()
|
||||
.map(column -> column.columnName())
|
||||
.toList()
|
||||
)));
|
||||
model.addAttribute("dbObjects", protectedObjectService.findDatabaseObjects());
|
||||
model.addAttribute("permissions", permissionService.findPermissionViews());
|
||||
));
|
||||
long columnsAt = System.nanoTime();
|
||||
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";
|
||||
}
|
||||
|
||||
private long elapsedMillis(long from, long to) {
|
||||
return (to - from) / 1_000_000;
|
||||
}
|
||||
|
||||
@PostMapping("/permissions")
|
||||
public String save(
|
||||
@RequestParam long roleId,
|
||||
|
||||
@@ -8,9 +8,11 @@ spring:
|
||||
driver-class-name: oracle.jdbc.OracleDriver
|
||||
hikari:
|
||||
pool-name: vpd-backoffice-pool
|
||||
maximum-pool-size: ${BACKOFFICE_DB_POOL_MAX:5}
|
||||
minimum-idle: ${BACKOFFICE_DB_POOL_MIN:1}
|
||||
maximum-pool-size: ${BACKOFFICE_DB_POOL_MAX:10}
|
||||
minimum-idle: ${BACKOFFICE_DB_POOL_MIN:2}
|
||||
connection-timeout: 10000
|
||||
idle-timeout: ${BACKOFFICE_DB_POOL_IDLE_TIMEOUT_MS:300000}
|
||||
keepalive-time: ${BACKOFFICE_DB_POOL_KEEPALIVE_MS:120000}
|
||||
thymeleaf:
|
||||
cache: false
|
||||
|
||||
|
||||
@@ -28,6 +28,13 @@
|
||||
WHERE object_type IN ('TABLE', 'VIEW')
|
||||
AND owner NOT IN ('SYS', 'SYSTEM', 'ORDS_METADATA', 'ORDS_PUBLIC_USER')
|
||||
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
|
||||
FETCH FIRST 500 ROWS ONLY
|
||||
</select>
|
||||
@@ -77,6 +84,12 @@
|
||||
WHERE object_id = #{objectId,jdbcType=NUMERIC}
|
||||
</update>
|
||||
|
||||
<update id="enableObject">
|
||||
UPDATE cb_protected_object
|
||||
SET enabled_yn = 'Y'
|
||||
WHERE object_id = #{objectId}
|
||||
</update>
|
||||
|
||||
<update id="disableObject">
|
||||
UPDATE cb_protected_object
|
||||
SET enabled_yn = 'N'
|
||||
|
||||
Reference in New Issue
Block a user