wiki:AdvancedApplicationDevelopment

Version 2 (modified by 233062, 3 days ago) ( diff )

--

Напреден Апликативен Развој

1. Pooling - Hikari Configuration

Pooling претставува процес каде што не се отвара нова конекција за секој request, туку има константен број на отворени конекције кои се доделуваат на различни requests. Со ова значително се намалува времето по request бидејќи не се губи време за отварање на нова и затворање на стара конекција.

Во мојот проект ова го правам со Hikari Pool во Java Spring Boot. Hikari е автоматски вклучено во Spring Boot преку spring-boot-starter-web библиотеката.

Пред (конзервативна конфигурација за SSH tunnel):

spring.datasource.hikari.maximum-pool-size=${SPRING_DATASOURCE_HIKARI_MAX_POOL_SIZE:2}
spring.datasource.hikari.minimum-idle=${SPRING_DATASOURCE_HIKARI_MIN_IDLE:1}

След (поголем default pool за поголем број корисници):

# Максимален број на конекции (default 20, може да се override преку env)
spring.datasource.hikari.maximum-pool-size=${SPRING_DATASOURCE_HIKARI_MAX_POOL_SIZE:20}

# Минимум конекции кои секогаш ќе се спремни за да преземат нови requests
spring.datasource.hikari.minimum-idle=${SPRING_DATASOURCE_HIKARI_MIN_IDLE:5}

# Колку долго да се чека за конекција пред requestot да е одбиен
spring.datasource.hikari.connection-timeout=30000

# Колку долго може да седи една конекција слободна
spring.datasource.hikari.idle-timeout=600000

# Максимално времетраење на една конекција
spring.datasource.hikari.max-lifetime=1800000

2. Транзакции - Write vs Read Operations

Основна философија: Транзакциите се применуваат САМО каде што се необходни.

Write Operations - Се Задржуваат Транзакции

AuthService - Register (Транзакционо)
    @Transactional
    public AuthResponse register(RegisterRequest request) {
        if (userRepository.existsByEmail(request.getEmail())) {
            throw new RuntimeException("Email already exists");
        }
        if (userRepository.existsByUsername(request.getUsername())) {
            throw new RuntimeException("Username already exists");
        }

        User user = new User();
        user.setEmail(request.getEmail().trim().toLowerCase());
        user.setUsername(request.getUsername().trim());
        user.setPassword(passwordEncoder.encode(request.getPassword()));

        user = userRepository.save(user);

        String token = jwtService.generateToken(user.getUserId(), 
                user.getUsername(), user.getEmail());
        return new AuthResponse(token, "Bearer", user.getUserId(), 
                user.getUsername(), user.getEmail());
    }

Причина: Проверка дали email/username постои → unique constraint validation → single write. Транзакцијата гарантира consistency.

DisciplineService - Compute Daily Completion (Транзакционо + Batch Optimized)

Пред (неоптимално - една insert per task):

        for (Task t : finishedTasks) {
            TaskDailyCompletion link = new TaskDailyCompletion();
            link.setTask(t);
            link.setDailyCompletion(savedCompletion);
            link.setId(new TaskDailyCompletionId(t.getTaskId(), 
                    savedCompletion.getDailyCompletionId()));
            taskDailyCompletionRepository.save(link);  // Loop - неоптимално!
        }

След (оптимизирано - batch insert):

        List<TaskDailyCompletion> links = taskRepository
                .findByDisciplineUser_UserIdAndFinishedTrue(userId)
                .stream()
                .map(t -> {
                    TaskDailyCompletion link = new TaskDailyCompletion();
                    link.setTask(t);
                    link.setDailyCompletion(savedCompletion);
                    link.setId(new TaskDailyCompletionId(t.getTaskId(), 
                            savedCompletion.getDailyCompletionId()));
                    return link;
                })
                .toList();
        taskDailyCompletionRepository.saveAll(links);  // Batch!

        taskRepository.resetFinishedForUser(userId);

Read-Only Operations - @Transactional(readOnly = true)

AuthService - Login (Read-Only)
    @Transactional(readOnly = true)
    public AuthResponse login(LoginRequest request) {
        String usernameOrEmail = request.getUsernameOrEmail().trim();
        String emailLookup = usernameOrEmail.contains("@") ? 
                usernameOrEmail.toLowerCase() : usernameOrEmail;
        
        User user = userRepository.findByUsername(usernameOrEmail)
                .or(() -> userRepository.findByEmail(emailLookup))
                .orElseThrow(() -> new RuntimeException("Invalid credentials"));

        if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
            throw new RuntimeException("Invalid credentials");
        }

        String token = jwtService.generateToken(user.getUserId(), 
                user.getUsername(), user.getEmail());
        return new AuthResponse(token, "Bearer", user.getUserId(), 
                user.getUsername(), user.getEmail());
    }
WeightService - Get Daily Intakes (Read-Only + N+1 OPTIMIZACIJA)

КРИТИЧНО: N+1 Query Fix

Пред (ПРОБЛЕМ - N+1):

    public WeightDailyIntakesResponse getDailyIntakes(Long userId, int page, int size) {
        Page<DailyIntake> result = dailyIntakeRepository
                .findByWeightUser_UserIdOrderByDateDesc(userId, PageRequest.of(page, size));

        List<WeightDailyIntakeDto> intakes = result.getContent().stream()
                .map(d -> {
                    // ПРОБЛЕМ: За СЕКОЈ intake, една query за training sessions!
                    var trainingSessions = trainingSessionRepository
                            .findByTrainingUser_UserIdAndDate(userId, d.getDate());
                    // 20 intakes = 20 queries + 1 page query = 21 queries!
                })
                .toList();

        return new WeightDailyIntakesResponse(intakes, result.hasNext(), hasTodayIntake);
    }

След (РЕШЕНО - Bulk Query):

    @Transactional(readOnly = true)
    public WeightDailyIntakesResponse getDailyIntakes(Long userId, int page, int size) {
        Page<DailyIntake> result = dailyIntakeRepository
                .findByWeightUser_UserIdOrderByDateDesc(userId, PageRequest.of(page, size));

        List<LocalDate> dates = result.getContent().stream()
                .map(DailyIntake::getDate)
                .distinct()
                .toList();

        // РЕШЕНО: Една bulk query наместо N queries
        Map<LocalDate, Integer> trainingCountsByDate = new HashMap<>();
        Map<LocalDate, BigDecimal> burnedCaloriesByDate = new HashMap<>();
        
        if (!dates.isEmpty()) {
            trainingSessionRepository.findByTrainingUser_UserIdAndDateIn(userId, dates)
                    .forEach(session -> {
                        LocalDate sessionDate = session.getDate();
                        trainingCountsByDate.merge(sessionDate, 1, Integer::sum);
                        BigDecimal calories = session.getCalories() != null ? 
                                session.getCalories() : BigDecimal.ZERO;
                        burnedCaloriesByDate.merge(sessionDate, calories, BigDecimal::add);
                    });
        }

        // Map lookup е O(1), не O(N) queries
        List<WeightDailyIntakeDto> intakes = result.getContent().stream()
                .map(d -> {
                    boolean trainedThatDay = trainingCountsByDate
                            .getOrDefault(d.getDate(), 0) > 0;
                    BigDecimal burnedCalories = burnedCaloriesByDate
                            .getOrDefault(d.getDate(), BigDecimal.ZERO)
                            .setScale(2, java.math.RoundingMode.HALF_UP);

                    return new WeightDailyIntakeDto(
                            d.getDailyIntakeId(),
                            d.getDate(),
                            d.getCalories(),
                            trainedThatDay,
                            burnedCalories);
                })
                .toList();

        return new WeightDailyIntakesResponse(intakes, result.hasNext(), hasTodayIntake);
    }

Што се промени: Од per-row queries → една bulk query со IN clause. За страна од 20 items: 21 queries → 2 queries.

DisciplineService - Get Daily Completions (Read-Only)
    @Transactional(readOnly = true)
    public DailyCompletionsResponse getDailyCompletions(Long userId, int page, int size) {
        Page<DailyCompletion> result = dailyCompletionRepository
                .findByUser_UserIdOrderByDateDescDailyCompletionIdDesc(userId, 
                        PageRequest.of(page, size));

        List<DailyCompletionDto> items = result.getContent().stream()
                .map(dc -> new DailyCompletionDto(dc.getDailyCompletionId(), 
                        dc.getDate(), dc.getProcent()))
                .toList();

        return new DailyCompletionsResponse(items, result.hasNext());
    }
CustomTrackingService - Get Categories (Read-Only)
    @Transactional(readOnly = true)
    public CustomTrackingCategoriesResponse getCustomTrackingCategories(Long userId) {
        List<CustomTrackingCategoryDto> items = customTrackingCategoryRepository
                .findByUser_UserIdOrderByCustomTrackingIdDesc(userId)
                .stream()
                .map(c -> new CustomTrackingCategoryDto(c.getCustomTrackingId(), 
                        c.getName()))
                .toList();

        return new CustomTrackingCategoriesResponse(items);
    }
FinanceService & InvestingService - Get Operations (Read-Only)
    @Transactional(readOnly = true)
    public FinanceProfileResponse getProfile(Long userId) { ... }

    @Transactional(readOnly = true)
    public IncomesResponse getIncomes(Long userId, int page, int size) { ... }

    @Transactional(readOnly = true)
    public InvestingAssetsResponse getAssets(Long userId, int page, int size) { ... }

3. Repository Changes

Додена нова bulk lookup метода во TrainingSessionRepository:

    @SuppressWarnings("unused")
    java.util.List<TrainingSession> findByTrainingUser_UserIdAndDateIn(Long userId, 
            Collection<LocalDate> dates);
Note: See TracWiki for help on using the wiki.