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.
Last modified
7 days ago
Last modified on 02/21/26 20:39:19
Note:
See TracWiki
for help on using the wiki.
