Batch API key usage queries to avoid N+1
This commit is contained in:
+16
@@ -1,5 +1,6 @@
|
||||
package stirling.software.proprietary.security.repository;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
@@ -33,6 +34,21 @@ public interface ApiKeyDailyUsageRepository
|
||||
+ "WHERE u.apiKeyId = :apiKeyId AND u.epochDay = :epochDay")
|
||||
Long countForDay(@Param("apiKeyId") Long apiKeyId, @Param("epochDay") long epochDay);
|
||||
|
||||
/** Batched today-count for many keys in one query (avoids N+1 when listing keys). */
|
||||
@Query(
|
||||
"SELECT u.apiKeyId AS apiKeyId, u.count AS total FROM ApiKeyDailyUsage u "
|
||||
+ "WHERE u.apiKeyId IN :ids AND u.epochDay = :epochDay")
|
||||
List<ApiKeyUsageSum> countForDayByIds(
|
||||
@Param("ids") Collection<Long> ids, @Param("epochDay") long epochDay);
|
||||
|
||||
/** Batched trailing-window sum for many keys in one query. */
|
||||
@Query(
|
||||
"SELECT u.apiKeyId AS apiKeyId, SUM(u.count) AS total FROM ApiKeyDailyUsage u "
|
||||
+ "WHERE u.apiKeyId IN :ids AND u.epochDay >= :fromDayInclusive "
|
||||
+ "GROUP BY u.apiKeyId")
|
||||
List<ApiKeyUsageSum> sumSinceByIds(
|
||||
@Param("ids") Collection<Long> ids, @Param("fromDayInclusive") long fromDayInclusive);
|
||||
|
||||
void deleteByApiKeyId(Long apiKeyId);
|
||||
|
||||
List<ApiKeyDailyUsage> findByApiKeyId(Long apiKeyId);
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package stirling.software.proprietary.security.repository;
|
||||
|
||||
/** Projection: a key id and a usage total, for batching per-key usage into one query. */
|
||||
public interface ApiKeyUsageSum {
|
||||
Long getApiKeyId();
|
||||
|
||||
Long getTotal();
|
||||
}
|
||||
+34
-7
@@ -4,8 +4,10 @@ import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@@ -86,8 +88,31 @@ public class ApiKeyManagementService {
|
||||
.forEach(visible::add);
|
||||
}
|
||||
|
||||
// Batch usage for all visible keys into two queries rather than two-per-key (avoids N+1).
|
||||
long today = Instant.now().atZone(ZoneOffset.UTC).toLocalDate().toEpochDay();
|
||||
List<Long> ids = visible.stream().map(ApiKey::getId).toList();
|
||||
Map<Long, Long> todayById = new HashMap<>();
|
||||
Map<Long, Long> monthById = new HashMap<>();
|
||||
if (!ids.isEmpty()) {
|
||||
usageRepository
|
||||
.countForDayByIds(ids, today)
|
||||
.forEach(r -> todayById.put(r.getApiKeyId(), r.getTotal()));
|
||||
usageRepository
|
||||
.sumSinceByIds(ids, today - (MONTH_WINDOW_DAYS - 1))
|
||||
.forEach(r -> monthById.put(r.getApiKeyId(), r.getTotal()));
|
||||
}
|
||||
|
||||
List<PortalApiKeyDto> keys =
|
||||
visible.stream().map(k -> toDto(caller, k, isManager, teamName)).toList();
|
||||
visible.stream()
|
||||
.map(
|
||||
k ->
|
||||
toDto(
|
||||
caller,
|
||||
k,
|
||||
teamName,
|
||||
zeroIfNull(todayById.get(k.getId())),
|
||||
zeroIfNull(monthById.get(k.getId()))))
|
||||
.toList();
|
||||
return PortalApiKeysResponse.builder()
|
||||
.keys(keys)
|
||||
.canCreateTeamKeys(isManager && teamId != null)
|
||||
@@ -95,6 +120,10 @@ public class ApiKeyManagementService {
|
||||
.build();
|
||||
}
|
||||
|
||||
private static long zeroIfNull(Long value) {
|
||||
return value == null ? 0L : value;
|
||||
}
|
||||
|
||||
/** Create a key and return its one-time secret. */
|
||||
@Transactional
|
||||
public CreatedApiKeyDto createKey(CreateApiKeyRequest request) {
|
||||
@@ -151,7 +180,7 @@ public class ApiKeyManagementService {
|
||||
.build());
|
||||
|
||||
return CreatedApiKeyDto.builder()
|
||||
.key(toDto(caller, saved, policyAuthority.canEditPolicies(), teamName))
|
||||
.key(toDto(caller, saved, teamName, 0L, 0L))
|
||||
.secret(rawKey)
|
||||
.build();
|
||||
}
|
||||
@@ -230,10 +259,8 @@ public class ApiKeyManagementService {
|
||||
});
|
||||
}
|
||||
|
||||
private PortalApiKeyDto toDto(User caller, ApiKey key, boolean isManager, String teamName) {
|
||||
long today = Instant.now().atZone(ZoneOffset.UTC).toLocalDate().toEpochDay();
|
||||
Long todayCount = usageRepository.countForDay(key.getId(), today);
|
||||
long usageMonth = usageRepository.sumSince(key.getId(), today - (MONTH_WINDOW_DAYS - 1));
|
||||
private PortalApiKeyDto toDto(
|
||||
User caller, ApiKey key, String teamName, long usageToday, long usageMonth) {
|
||||
return PortalApiKeyDto.builder()
|
||||
.id(String.valueOf(key.getId()))
|
||||
.name(key.getName())
|
||||
@@ -247,7 +274,7 @@ public class ApiKeyManagementService {
|
||||
? "Never"
|
||||
: LAST_USED_FORMAT.format(key.getLastUsedAt()))
|
||||
.status(key.isActive() ? "active" : "revoked")
|
||||
.usageToday(todayCount == null ? 0 : todayCount)
|
||||
.usageToday(usageToday)
|
||||
.usageMonth(usageMonth)
|
||||
.canManage(canManage(caller, key))
|
||||
.build();
|
||||
|
||||
Reference in New Issue
Block a user