From 3f9383fa5f1f13e19687da0a7dc9e776bbd657c5 Mon Sep 17 00:00:00 2001 From: cornerblue Date: Mon, 20 Jul 2026 02:52:19 -0700 Subject: [PATCH] perf(#2): aggregate storage queries instead of loading all documents - sumSizeBytesByOwnerId for quota checks (store path) - countByOwner_Id + sum for admin user list (was N+1 load-all-docs) - existsByAdminTrue / countByAdminTrue instead of findAll().stream() Closes #2 --- .../crystalpdf/backend/config/AdminDataInitializer.java | 2 +- .../crystalpdf/backend/repository/DocumentRepository.java | 8 ++++++++ .../com/crystalpdf/backend/repository/UserRepository.java | 2 ++ .../java/com/crystalpdf/backend/service/AdminService.java | 8 ++++---- .../com/crystalpdf/backend/service/StorageService.java | 5 ++--- 5 files changed, 17 insertions(+), 8 deletions(-) diff --git a/backend/src/main/java/com/crystalpdf/backend/config/AdminDataInitializer.java b/backend/src/main/java/com/crystalpdf/backend/config/AdminDataInitializer.java index c3eb4a5..a9691db 100644 --- a/backend/src/main/java/com/crystalpdf/backend/config/AdminDataInitializer.java +++ b/backend/src/main/java/com/crystalpdf/backend/config/AdminDataInitializer.java @@ -25,7 +25,7 @@ ApplicationRunner seedAdmin( // Credentials are read from application.yml (environment-configurable). // Once created, admin password is not auto-reset (prevent privilege escalation). // To recover a lost admin password, manually set a new one via database or admin console. - boolean adminExists = userRepo.findAll().stream().anyMatch(User::isAdmin); + boolean adminExists = userRepo.existsByAdminTrue(); if (!adminExists) { User admin = new User(); admin.setEmail(adminEmail); diff --git a/backend/src/main/java/com/crystalpdf/backend/repository/DocumentRepository.java b/backend/src/main/java/com/crystalpdf/backend/repository/DocumentRepository.java index 0b74d20..4f0fb6e 100644 --- a/backend/src/main/java/com/crystalpdf/backend/repository/DocumentRepository.java +++ b/backend/src/main/java/com/crystalpdf/backend/repository/DocumentRepository.java @@ -2,6 +2,8 @@ import com.crystalpdf.backend.entity.Document; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.List; import java.util.Optional; @@ -9,4 +11,10 @@ public interface DocumentRepository extends JpaRepository { List findByOwnerIdOrderByCreatedAtDesc(Long userId); Optional findByIdAndOwnerId(Long id, Long userId); + + /** Sum storage without loading every Document row (list endpoint / quota checks). */ + @Query("select coalesce(sum(d.sizeBytes), 0) from Document d where d.owner.id = :ownerId") + long sumSizeBytesByOwnerId(@Param("ownerId") Long ownerId); + + long countByOwner_Id(Long ownerId); } diff --git a/backend/src/main/java/com/crystalpdf/backend/repository/UserRepository.java b/backend/src/main/java/com/crystalpdf/backend/repository/UserRepository.java index 591cf61..168a8c4 100644 --- a/backend/src/main/java/com/crystalpdf/backend/repository/UserRepository.java +++ b/backend/src/main/java/com/crystalpdf/backend/repository/UserRepository.java @@ -9,5 +9,7 @@ public interface UserRepository extends JpaRepository { Optional findByEmail(String email); boolean existsByEmail(String email); Optional findByUsername(String username); + boolean existsByAdminTrue(); + long countByAdminTrue(); boolean existsByUsername(String username); } diff --git a/backend/src/main/java/com/crystalpdf/backend/service/AdminService.java b/backend/src/main/java/com/crystalpdf/backend/service/AdminService.java index 8234b91..6b38f7c 100644 --- a/backend/src/main/java/com/crystalpdf/backend/service/AdminService.java +++ b/backend/src/main/java/com/crystalpdf/backend/service/AdminService.java @@ -68,15 +68,15 @@ public Page getAllUsers(int page, int pageSize, String search // Convert to responses List responses = allUsers.stream().map(user -> { - List docs = documentRepository.findByOwnerIdOrderByCreatedAtDesc(user.getId()); - long storageUsed = docs.stream().mapToLong(Document::getSizeBytes).sum(); + long storageUsed = documentRepository.sumSizeBytesByOwnerId(user.getId()); + int documentCount = (int) documentRepository.countByOwner_Id(user.getId()); AppSettings settings = getSettingsEntity(); long limitBytes = user.getStorageLimitBytes() != null ? user.getStorageLimitBytes() : settings.getDefaultStorageLimitMb() * 1024L * 1024L; return new AdminUserResponse( user.getId(), user.getEmail(), user.getDisplayUsername(), user.isAdmin(), user.isPasswordChangeRequired(), - limitBytes, storageUsed, docs.size(), + limitBytes, storageUsed, documentCount, user.getCreatedAt() != null ? user.getCreatedAt().toString() : "" ); }).toList(); @@ -167,7 +167,7 @@ public Map getSystemInfo() { // Platform stats info.put("totalUsers", userRepository.count()); info.put("totalFiles", documentRepository.count()); - info.put("totalAdmins", userRepository.findAll().stream().filter(User::isAdmin).count()); + info.put("totalAdmins", userRepository.countByAdminTrue()); info.put("javaVersion", System.getProperty("java.version")); info.put("osName", System.getProperty("os.name")); diff --git a/backend/src/main/java/com/crystalpdf/backend/service/StorageService.java b/backend/src/main/java/com/crystalpdf/backend/service/StorageService.java index 76efe7b..c4e296f 100644 --- a/backend/src/main/java/com/crystalpdf/backend/service/StorageService.java +++ b/backend/src/main/java/com/crystalpdf/backend/service/StorageService.java @@ -70,9 +70,8 @@ public Document store(MultipartFile file, User owner) throws IOException { settings.getMaxUploadSizeMb() + " MB."); } - // Check storage limit - List existingDocs = documentRepository.findByOwnerIdOrderByCreatedAtDesc(owner.getId()); - long usedBytes = existingDocs.stream().mapToLong(Document::getSizeBytes).sum(); + // Check storage limit (aggregate query — avoid loading all document rows) + long usedBytes = documentRepository.sumSizeBytesByOwnerId(owner.getId()); long limitBytes = owner.getStorageLimitBytes() != null ? owner.getStorageLimitBytes() : settings.getDefaultStorageLimitMb() * 1024L * 1024L; if (usedBytes + fileSizeBytes > limitBytes) {