Changes between Initial Version and Version 1 of AdvancedApplicationDevelopment


Ignore:
Timestamp:
07/07/26 23:20:04 (4 days ago)
Author:
231035
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • AdvancedApplicationDevelopment

    v1 v1  
     1= Advanced Application Development
     2== Transactional
     3=== Getting similar listings based on liked ones
     4{{{
     5@Transactional(readOnly = true)
     6    public List<RecommendedListingProjection> getRecommendedListings(Long userId) {
     7        return recommendationRepository.getRecommendedListings(userId);
     8    }
     9}}}
     10=== Getting the top 10 most active users
     11{{{
     12@Transactional(readOnly = true)
     13    public List<UserActivityRankingProjection> getTopActiveUsers(
     14            LocalDateTime startTs,
     15            LocalDateTime endTs
     16    ) {
     17        return analyticsRepository.getTopActiveUsers(startTs, endTs);
     18    }
     19}}}
     20=== Adding a listing as a favorite
     21{{{
     22@Transactional
     23    public void addFavorite(Long userId, Long listingId) {
     24        Client client = clientRepository.findByUserId(userId)
     25            .orElseThrow(() -> new RuntimeException("Client not found"));
     26
     27        Listing listing = listingRepository.findById(listingId)
     28            .orElseThrow(() -> new RuntimeException("Listing not found"));
     29
     30        FavoriteListing favorite = new FavoriteListing(client, listing);
     31        favoriteRepository.save(favorite);
     32        logger.info("Added favorite - User: {}, Listing: {}", userId, listingId);
     33    }
     34}}}
     35=== Removing a listing from favorites
     36{{{
     37@Transactional
     38    public void removeFavorite(Long userId, Long listingId) {
     39        Client client = clientRepository.findByUserId(userId)
     40            .orElseThrow(() -> new RuntimeException("Client not found"));
     41
     42        Listing listing = listingRepository.findById(listingId)
     43            .orElseThrow(() -> new RuntimeException("Listing not found"));
     44
     45        FavoriteListing favorite = favoriteRepository.findByClientAndListing(client, listing)
     46            .orElseThrow(() -> new RuntimeException("Favorite not found"));
     47
     48        favoriteRepository.delete(favorite);
     49        logger.info("Removed favorite - User: {}, Listing: {}", userId, listingId);
     50    }
     51}}}
     52=== Getting information from the favorite_listings table
     53{{{
     54    @Transactional(readOnly = true)
     55    public List<ListingDTO> getFavoritedListings(Long userId) {
     56        return favoriteRepository.findFavoritedListingDTOs(userId);
     57    }
     58
     59    @Transactional(readOnly = true)
     60    public boolean isFavorited(Long userId, Long listingId) {
     61        Client client = clientRepository.findByUserId(userId)
     62            .orElse(null);
     63
     64        if (client == null) return false;
     65
     66        Listing listing = listingRepository.findById(listingId)
     67            .orElse(null);
     68
     69        if (listing == null) return false;
     70
     71        return favoriteRepository.findByClientAndListing(client, listing).isPresent();
     72    }
     73}}}
     74=== Creating a review
     75{{{
     76@Transactional
     77    public ReviewDTO createReview(Long reviewerId, Long targetUserId, CreateReviewRequest request) {
     78        logger.info("createReview");
     79
     80        if (request.getRating() == null || request.getRating() < 1 || request.getRating() > 5) {
     81            logger.error(" VALIDATION FAILED: Invalid rating: {}", request.getRating());
     82            throw new RuntimeException("Rating must be between 1 and 5");
     83        }
     84     
     85
     86        // Check if reviewer exists
     87        User reviewer = userRepository.findById(reviewerId)
     88                .orElseThrow(() -> {
     89                    logger.error(" Reviewer not found with ID: {}", reviewerId);
     90                    return new RuntimeException("Reviewer not found");
     91                });
     92
     93        // Check if target user exists
     94        User targetUser = userRepository.findById(targetUserId)
     95                .orElseThrow(() -> {
     96                    logger.error(" Target user not found with ID: {}", targetUserId);
     97                    return new RuntimeException("Target user not found");
     98                });
     99
     100        // Check if review already exists and is not deleted
     101        logger.info("Checking if reviewer {} has already reviewed user {}", reviewerId, targetUserId);
     102        var existingReview = userReviewRepository.findTopByReviewReviewerUserIdAndTargetUserIdAndReviewIsDeletedFalseOrderByReviewCreatedAtDesc(reviewerId, targetUserId);
     103
     104        if (existingReview.isPresent()) {
     105            Review existingReviewEntity = existingReview.get().getReview();
     106            logger.info("Found existing review with ID: {}", existingReviewEntity.getReviewId());
     107
     108            if (!existingReviewEntity.getIsDeleted()) {
     109                logger.error(" User {} has already reviewed user {} and review is NOT deleted", reviewerId, targetUserId);
     110                throw new RuntimeException("You have already reviewed this user");
     111            } else {
     112                logger.info(" User {} has a deleted review for user {} - can create a new one", reviewerId, targetUserId);
     113            }
     114        } else {
     115            logger.info(" No existing review found - safe to create new review");
     116        }
     117
     118        // Create Review entity
     119        Review review = new Review(reviewer, request.getRating(), request.getComment());
     120
     121
     122        // Save Review to database with flush
     123        review = reviewRepository.saveAndFlush(review);
     124        logger.info("Review ID after save: {}", review.getReviewId());
     125
     126        if (review.getReviewId() == null) {
     127            logger.error(" CRITICAL: Review ID is NULL after save!");
     128            throw new RuntimeException("Failed to save review - ID is null");
     129        }
     130
     131        // Create UserReview entry
     132        UserReview userReview = new UserReview();
     133        userReview.setReview(review);
     134        logger.info("UserReview reviewId after setReview: {}", userReview.getReviewId());
     135
     136        userReview.setTargetUserId(targetUserId);
     137
     138        // Save UserReview to database with flush
     139        userReview = userReviewRepository.saveAndFlush(userReview);
     140        logger.info(" UserReview saved successfully");
     141
     142        // Create and return DTO
     143        ReviewDTO reviewDTO = new ReviewDTO(review);
     144        logger.info(" ReviewDTO created successfully");
     145
     146        return reviewDTO;
     147    }
     148}}}
     149=== Deleting a review
     150{{{
     151@Transactional
     152    public void deleteReview(Long reviewId, Long userId) {
     153        logger.info("=== START deleteReview (SOFT DELETE) ===");
     154       
     155
     156        // Fetch review
     157        logger.info("Fetching review with ID: {}", reviewId);
     158        Review review = reviewRepository.findById(reviewId)
     159                .orElseThrow(() -> {
     160                    logger.error(" Review not found with ID: {}", reviewId);
     161                    return new RuntimeException("Review not found");
     162                });
     163        logger.info(" Review found: reviewer={}, rating={}", review.getReviewer().getUsername(), review.getRating());
     164
     165        // Check authorization
     166        if (!review.getReviewer().getUserId().equals(userId)) {
     167            logger.error(" User {} is not authorized to delete review {}. Reviewer is {}", userId, reviewId, review.getReviewer().getUserId());
     168            throw new RuntimeException("You can only delete your own reviews");
     169        }
     170
     171        // Soft delete: mark as deleted instead of physically removing
     172        review.setIsDeleted(true);
     173        review.setUpdatedAt(LocalDateTime.now());
     174        reviewRepository.save(review);
     175
     176        logger.info("=== END deleteReview - SUCCESS ===");
     177    }
     178}}}
     179=== Creating a new pet
     180{{{
     181@Transactional
     182    public AnimalResponseDTO addPet(Long userId,@RequestBody CreatePetRequest request) {
     183
     184        // Validate required fields
     185        if (request.getName() == null || request.getName().isBlank()) {
     186            throw new RuntimeException("Pet name is required");
     187        }
     188
     189        if (request.getSex() == null || request.getSex().isBlank()) {
     190            throw new RuntimeException("Pet sex is required");
     191        }
     192
     193        if (request.getType() == null || request.getType().isBlank()) {
     194            throw new RuntimeException("Pet type is required");
     195        }
     196
     197
     198        if (request.getSpecies() == null || request.getSpecies().isBlank()) {
     199            throw new RuntimeException("Pet species is required");
     200        }
     201
     202        // Get user
     203        User user = userRepository.findById(userId)
     204                .orElseThrow(() -> {
     205                    logger.error(" User not found with ID: {}", userId);
     206                    return new RuntimeException("User not found");
     207                });
     208
     209        logger.info("Adding pet for user ID: {}", userId);
     210
     211        // Check if user is already an owner, if not, promote them
     212        Owner owner = ownerRepository.findByUserId(userId)
     213                .orElseGet(() -> {
     214                    logger.info("⚠User {} is a CLIENT, promoting to OWNER", userId);
     215                    Owner newOwner = new Owner(user);
     216                    Owner savedOwner = ownerRepository.save(newOwner);
     217                    logger.info(" User promoted to OWNER with ID: {}", savedOwner.getUserId());
     218                    return savedOwner;
     219                });
     220
     221        // Create new pet with all schema fields
     222
     223        Pet pet = new Pet(
     224                request.getName(),
     225                request.getSex(),
     226                request.getDateOfBirth(),
     227                request.getPhotoUrl(),
     228                request.getType(),
     229                request.getSpecies(),
     230                request.getBreed(),
     231                request.getLocatedName(),
     232                owner
     233        );
     234       
     235        Pet savedPet = petRepository.save(pet);
     236        logger.info("Pet ID: {}, Owner ID: {}, Name: {}",
     237                savedPet.getAnimalId(), userId, savedPet.getName());
     238
     239        AnimalResponseDTO result = new AnimalResponseDTO(savedPet);
     240
     241
     242        return result;
     243    }
     244}}}
     245=== Creating a new listing
     246{{{
     247   @Transactional
     248    public ListingDTO createListing(Long userId, CreateListingRequest request) {
     249        // Check if user is an owner
     250        Owner owner = ownerRepository.findByUserId(userId)
     251            .orElseThrow(() -> new RuntimeException("User is not an owner. Only owners can create listings."));
     252
     253        logger.info("Creating listing for owner ID: {}", userId);
     254
     255        // Create new listing with Owner object
     256        Listing listing = new Listing(
     257            owner,
     258            request.getAnimalId(),
     259            request.getPrice(),
     260            request.getDescription()
     261        );
     262
     263        Listing savedListing = listingRepository.save(listing);
     264
     265        logger.info("Listing created successfully - ID: {}, Owner ID: {}, Animal ID: {}",
     266            savedListing.getListingId(), userId, request.getAnimalId());
     267
     268        return mapToDTO(savedListing);
     269    }
     270}}}
     271== Pooling
     272In the backend layer of the Petify application, we use Spring Boot with Spring Data JPA to communicate with the PostgreSQL database. Because of this architecture, database connections are not created manually. Instead, they are automatically managed by HikariCP, which is the default connection pool implementation in Spring Boot.
     273[[BR]]
     274HikariCP is included transitively through:
     275{{{spring-boot-starter-data-jpa}}}
     276[[BR]]
     277Therefore, no additional configuration is required to enable pooling.
     278[[BR]]
     279In this project, we use these HikariCP configuration values:
     280{{{
     281spring.datasource.hikari.maximum-pool-size=20
     282spring.datasource.hikari.minimum-idle=5
     283spring.datasource.hikari.connection-timeout=20000
     284spring.datasource.hikari.idle-timeout=300000
     285spring.datasource.hikari.max-lifetime=1200000
     286spring.datasource.hikari.auto-commit=true
     287}}}
     288These values mean:
     289* A maximum of 20 database connections can be active at the same time.
     290* The pool maintains 5 idle connections ready for immediate use.
     291* If all connections are busy, a request waits up to 20 seconds before failing.
     292* Idle connections are kept alive for up to 5 minutes.
     293* Connections are refreshed every 20 minutes to avoid stale connections.
     294* Auto-commit is enabled by default.
     295{{{
     2962026-03-09T23:42:34.869+01:00 DEBUG 37988 --- [l-1:housekeeper] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Before cleanup stats (total=1/20, idle=1/5, active=0, waiting=0)
     2972026-03-09T23:42:34.869+01:00 DEBUG 37988 --- [l-1:housekeeper] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - After  cleanup stats (total=1/20, idle=1/5, active=0, waiting=0)
     2982026-03-09T23:42:34.870+01:00 DEBUG 37988 --- [onnection-adder] com.zaxxer.hikari.pool.PoolBase          : HikariPool-1 - Attempting to create/setup new connection (3f4fb556-9cc3-4220-9556-e35534a5a863)
     2992026-03-09T23:42:34.891+01:00 DEBUG 37988 --- [onnection-adder] com.zaxxer.hikari.pool.PoolBase          : HikariPool-1 - Established new connection (3f4fb556-9cc3-4220-9556-e35534a5a863)
     3002026-03-09T23:42:34.891+01:00 DEBUG 37988 --- [onnection-adder] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@40ee8801
     3012026-03-09T23:42:34.930+01:00 DEBUG 37988 --- [onnection-adder] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - After adding stats (total=2/20, idle=2/5, active=0, waiting=0)
     3022026-03-09T23:42:34.930+01:00 DEBUG 37988 --- [onnection-adder] com.zaxxer.hikari.pool.PoolBase          : HikariPool-1 - Attempting to create/setup new connection (148676f5-024a-4b14-aed0-8d0727b8def1)
     3032026-03-09T23:42:34.950+01:00 DEBUG 37988 --- [onnection-adder] com.zaxxer.hikari.pool.PoolBase          : HikariPool-1 - Established new connection (148676f5-024a-4b14-aed0-8d0727b8def1)
     3042026-03-09T23:42:34.950+01:00 DEBUG 37988 --- [onnection-adder] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@4e297e6c
     3052026-03-09T23:42:34.992+01:00 DEBUG 37988 --- [onnection-adder] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - After adding stats (total=3/20, idle=3/5, active=0, waiting=0)
     3062026-03-09T23:42:34.992+01:00 DEBUG 37988 --- [onnection-adder] com.zaxxer.hikari.pool.PoolBase          : HikariPool-1 - Attempting to create/setup new connection (f85f6fca-da39-4e54-bdff-e29c851df6cf)
     3072026-03-09T23:42:35.014+01:00 DEBUG 37988 --- [onnection-adder] com.zaxxer.hikari.pool.PoolBase          : HikariPool-1 - Established new connection (f85f6fca-da39-4e54-bdff-e29c851df6cf)
     3082026-03-09T23:42:35.015+01:00 DEBUG 37988 --- [onnection-adder] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@4a16e371
     3092026-03-09T23:42:35.054+01:00 DEBUG 37988 --- [onnection-adder] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - After adding stats (total=4/20, idle=4/5, active=0, waiting=0)
     3102026-03-09T23:42:35.054+01:00 DEBUG 37988 --- [onnection-adder] com.zaxxer.hikari.pool.PoolBase          : HikariPool-1 - Attempting to create/setup new connection (1555b87b-99a2-405d-a1dc-2d65ab6e44ed)
     3112026-03-09T23:42:35.076+01:00 DEBUG 37988 --- [onnection-adder] com.zaxxer.hikari.pool.PoolBase          : HikariPool-1 - Established new connection (1555b87b-99a2-405d-a1dc-2d65ab6e44ed)
     3122026-03-09T23:42:35.077+01:00 DEBUG 37988 --- [onnection-adder] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@f147457
     3132026-03-09T23:42:35.115+01:00 DEBUG 37988 --- [onnection-adder] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - After adding stats (total=5/20, idle=5/5, active=0, waiting=0)
     314}}}