wiki:Advanced Application Development

Version 2 (modified by 231035, 7 days ago) ( diff )

--

Advanced Application Development

Transactional

Adding a listing as a favorite

@Transactional
    public void addFavorite(Long userId, Long listingId) {
        Client client = clientRepository.findByUserId(userId)
            .orElseThrow(() -> new RuntimeException("Client not found"));

        Listing listing = listingRepository.findById(listingId)
            .orElseThrow(() -> new RuntimeException("Listing not found"));

        FavoriteListing favorite = new FavoriteListing(client, listing);
        favoriteRepository.save(favorite);
        logger.info("Added favorite - User: {}, Listing: {}", userId, listingId);
    }

Removing a listing from favorites

@Transactional
    public void removeFavorite(Long userId, Long listingId) {
        Client client = clientRepository.findByUserId(userId)
            .orElseThrow(() -> new RuntimeException("Client not found"));

        Listing listing = listingRepository.findById(listingId)
            .orElseThrow(() -> new RuntimeException("Listing not found"));

        FavoriteListing favorite = favoriteRepository.findByClientAndListing(client, listing)
            .orElseThrow(() -> new RuntimeException("Favorite not found"));

        favoriteRepository.delete(favorite);
        logger.info("Removed favorite - User: {}, Listing: {}", userId, listingId);
    }

Getting information from the favorite_listings table

    @Transactional(readOnly = true)
    public List<ListingDTO> getFavoritedListings(Long userId) {
        return favoriteRepository.findFavoritedListingDTOs(userId);
    }

    @Transactional(readOnly = true)
    public boolean isFavorited(Long userId, Long listingId) {
        Client client = clientRepository.findByUserId(userId)
            .orElse(null);

        if (client == null) return false;

        Listing listing = listingRepository.findById(listingId)
            .orElse(null);

        if (listing == null) return false;

        return favoriteRepository.findByClientAndListing(client, listing).isPresent();
    }

Creating a review

@Transactional
    public ReviewDTO createReview(Long reviewerId, Long targetUserId, CreateReviewRequest request) {
        logger.info("==== START createReview ====");
        // Validate rating
        if (request.getRating() == null || request.getRating() < 1 || request.getRating() > 5) {
            logger.error(" VALIDATION FAILED: Invalid rating: {}", request.getRating());
            throw new RuntimeException("Rating must be between 1 and 5");
        }
     

        // Check if reviewer exists
        User reviewer = userRepository.findById(reviewerId)
                .orElseThrow(() -> {
                    logger.error(" Reviewer not found with ID: {}", reviewerId);
                    return new RuntimeException("Reviewer not found");
                });

        // Check if target user exists
        User targetUser = userRepository.findById(targetUserId)
                .orElseThrow(() -> {
                    logger.error(" Target user not found with ID: {}", targetUserId);
                    return new RuntimeException("Target user not found");
                });

        // Check if review already exists and is not deleted
        logger.info("Checking if reviewer {} has already reviewed user {}", reviewerId, targetUserId);
        var existingReview = userReviewRepository.findTopByReviewReviewerUserIdAndTargetUserIdAndReviewIsDeletedFalseOrderByReviewCreatedAtDesc(reviewerId, targetUserId);

        if (existingReview.isPresent()) {
            Review existingReviewEntity = existingReview.get().getReview();
            logger.info("Found existing review with ID: {}", existingReviewEntity.getReviewId());
            logger.info("Existing review isDeleted status: {}", existingReviewEntity.getIsDeleted());

            if (!existingReviewEntity.getIsDeleted()) {
                logger.error(" User {} has already reviewed user {} and review is NOT deleted", reviewerId, targetUserId);
                throw new RuntimeException("You have already reviewed this user");
            } else {
                logger.info(" User {} has a deleted review for user {} - can create a new one", reviewerId, targetUserId);
            }
        } else {
            logger.info(" No existing review found - safe to create new review");
        }

        // Create Review entity
        Review review = new Review(reviewer, request.getRating(), request.getComment());


        // Save Review to database with flush
        review = reviewRepository.saveAndFlush(review);
        logger.info("Review ID after save: {}", review.getReviewId());

        if (review.getReviewId() == null) {
            logger.error(" CRITICAL: Review ID is NULL after save!");
            throw new RuntimeException("Failed to save review - ID is null");
        }

        // Create UserReview entry
        UserReview userReview = new UserReview();
        logger.info("Setting Review on UserReview (will copy ID via @MapsId)...");
        userReview.setReview(review);
        logger.info("UserReview reviewId after setReview: {}", userReview.getReviewId());

        userReview.setTargetUserId(targetUserId);

        // Save UserReview to database with flush
        userReview = userReviewRepository.saveAndFlush(userReview);
        logger.info(" UserReview saved successfully");

        // Create and return DTO
        ReviewDTO reviewDTO = new ReviewDTO(review);
        logger.info(" ReviewDTO created successfully");

        return reviewDTO;
    }

Deleting a review

@Transactional
    public void deleteReview(Long reviewId, Long userId) {
        logger.info("=== START deleteReview (SOFT DELETE) ===");
        

        // Fetch review
        logger.info("Fetching review with ID: {}", reviewId);
        Review review = reviewRepository.findById(reviewId)
                .orElseThrow(() -> {
                    logger.error(" Review not found with ID: {}", reviewId);
                    return new RuntimeException("Review not found");
                });
        logger.info(" Review found: reviewer={}, rating={}", review.getReviewer().getUsername(), review.getRating());

        // Check authorization
        if (!review.getReviewer().getUserId().equals(userId)) {
            logger.error(" User {} is not authorized to delete review {}. Reviewer is {}", userId, reviewId, review.getReviewer().getUserId());
            throw new RuntimeException("You can only delete your own reviews");
        }

        // Soft delete: mark as deleted instead of physically removing
        review.setIsDeleted(true);
        review.setUpdatedAt(LocalDateTime.now());
        reviewRepository.save(review);

        logger.info("=== END deleteReview - SUCCESS ===");
    }

Creating a new pet

@Transactional
    public AnimalResponseDTO addPet(Long userId,@RequestBody CreatePetRequest request) {

        // Validate required fields
        if (request.getName() == null || request.getName().isBlank()) {
            throw new RuntimeException("Pet name is required");
        }

        if (request.getSex() == null || request.getSex().isBlank()) {
            throw new RuntimeException("Pet sex is required");
        }

        if (request.getType() == null || request.getType().isBlank()) {
            throw new RuntimeException("Pet type is required");
        }


        if (request.getSpecies() == null || request.getSpecies().isBlank()) {
            throw new RuntimeException("Pet species is required");
        }

        // Get user
        User user = userRepository.findById(userId)
                .orElseThrow(() -> {
                    logger.error(" User not found with ID: {}", userId);
                    return new RuntimeException("User not found");
                });

        logger.info("Adding pet for user ID: {}", userId);

        // Check if user is already an owner, if not, promote them
        Owner owner = ownerRepository.findByUserId(userId)
                .orElseGet(() -> {
                    logger.info("⚠User {} is a CLIENT, promoting to OWNER", userId);
                    Owner newOwner = new Owner(user);
                    Owner savedOwner = ownerRepository.save(newOwner);
                    logger.info(" User promoted to OWNER with ID: {}", savedOwner.getUserId());
                    return savedOwner;
                });

        // Create new pet with all schema fields

        Pet pet = new Pet(
                request.getName(),
                request.getSex(),
                request.getDateOfBirth(),
                request.getPhotoUrl(),
                request.getType(),
                request.getSpecies(),
                request.getBreed(),
                request.getLocatedName(),
                owner
        );
        
        Pet savedPet = petRepository.save(pet);
        logger.info("Pet ID: {}, Owner ID: {}, Name: {}",
                savedPet.getAnimalId(), userId, savedPet.getName());

        AnimalResponseDTO result = new AnimalResponseDTO(savedPet);


        return result;
    }

Creating a new listing

   @Transactional
    public ListingDTO createListing(Long userId, CreateListingRequest request) {
        // Check if user is an owner
        Owner owner = ownerRepository.findByUserId(userId)
            .orElseThrow(() -> new RuntimeException("User is not an owner. Only owners can create listings."));

        logger.info("Creating listing for owner ID: {}", userId);

        // Create new listing with Owner object
        Listing listing = new Listing(
            owner,
            request.getAnimalId(),
            request.getPrice(),
            request.getDescription()
        );

        Listing savedListing = listingRepository.save(listing);

        logger.info("Listing created successfully - ID: {}, Owner ID: {}, Animal ID: {}",
            savedListing.getListingId(), userId, request.getAnimalId());

        return mapToDTO(savedListing);
    }

Pooling

In 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.
HikariCP is included transitively through: spring-boot-starter-data-jpa
Therefore, no additional configuration is required to enable pooling.
In this project, we use these HikariCP configuration values:

spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.connection-timeout=20000
spring.datasource.hikari.idle-timeout=300000
spring.datasource.hikari.max-lifetime=1200000
spring.datasource.hikari.auto-commit=true

These values mean:

  • A maximum of 20 database connections can be active at the same time.
  • The pool maintains 5 idle connections ready for immediate use.
  • If all connections are busy, a request waits up to 20 seconds before failing.
  • Idle connections are kept alive for up to 5 minutes.
  • Connections are refreshed every 20 minutes to avoid stale connections.
  • Auto-commit is enabled by default.
Note: See TracWiki for help on using the wiki.