| | 1 | = Advanced Application Development (Transactions, Pooling) = |
| | 2 | |
| | 3 | == Transactional == |
| | 4 | |
| | 5 | In PetSitter, there are strict rules about transactions. Write operations (INSERT, UPDATE, DELETE) are labeled with `@Transactional` to ensure data consistency and enable automatic fallbacks on failure. |
| | 6 | |
| | 7 | Read operations (SELECT) are optimized with `@Transactional(readOnly = true)` to avoid unnecessary operations in the background and improve performance. |
| | 8 | |
| | 9 | === Authenticating a user === |
| | 10 | {{{ |
| | 11 | #!java |
| | 12 | @Transactional(readOnly = true) |
| | 13 | public User authenticate(String username, String password) { |
| | 14 | User user = userRepository.findByUsername(username).orElse(null); |
| | 15 | // P9 - use BCryptPasswordEncoder here |
| | 16 | if (user != null && user.getPassword().equals(password)) { |
| | 17 | return user; |
| | 18 | } |
| | 19 | return null; |
| | 20 | } |
| | 21 | }}} |
| | 22 | |
| | 23 | === Registering a new user === |
| | 24 | {{{ |
| | 25 | #!java |
| | 26 | @Transactional |
| | 27 | public User registerUser(String username, String password, String firstName, String lastName, String email, String role) { |
| | 28 | if (userRepository.findByUsername(username).isPresent()) { |
| | 29 | throw new IllegalArgumentException("Username already exists!"); |
| | 30 | } |
| | 31 | |
| | 32 | User newUser; |
| | 33 | if ("OWNER".equalsIgnoreCase(role)) { |
| | 34 | newUser = new PetOwner(); |
| | 35 | } else if ("SITTER".equalsIgnoreCase(role)) { |
| | 36 | newUser = new PetSitter(); |
| | 37 | } else { |
| | 38 | throw new IllegalArgumentException("Invalid role selected!"); |
| | 39 | } |
| | 40 | |
| | 41 | newUser.setUsername(username); |
| | 42 | newUser.setPassword(password); |
| | 43 | newUser.setFirstName(firstName); |
| | 44 | newUser.setLastName(lastName); |
| | 45 | newUser.setEmail(email); |
| | 46 | |
| | 47 | // Saving a PetOwner or PetSitter automatically saves to the parent 'users' table with InhertianceType.JOINED |
| | 48 | if (newUser instanceof PetOwner) { |
| | 49 | return petOwnerRepository.save((PetOwner) newUser); |
| | 50 | } else { |
| | 51 | return petSitterRepository.save((PetSitter) newUser); |
| | 52 | } |
| | 53 | } |
| | 54 | }}} |
| | 55 | |
| | 56 | === Creating a new booking === |
| | 57 | {{{ |
| | 58 | #!java |
| | 59 | @Transactional |
| | 60 | public Booking createBooking(String ownerId, String sitterId, LocalDate dateFrom, LocalDate dateTo, String address, List<String> petIds, String serviceType) { |
| | 61 | PetOwner owner = petOwnerRepository.findById(ownerId).orElseThrow(() -> new IllegalArgumentException("Invalid owner")); |
| | 62 | PetSitter sitter = petSitterRepository.findById(sitterId).orElseThrow(() -> new IllegalArgumentException("Invalid sitter")); |
| | 63 | List<Pet> pets = petRepository.findAllById(petIds); |
| | 64 | |
| | 65 | Booking booking = new Booking(); |
| | 66 | booking.setOwner(owner); |
| | 67 | booking.setSitter(sitter); |
| | 68 | booking.setDateFrom(dateFrom); |
| | 69 | booking.setDateTo(dateTo); |
| | 70 | booking.setAddress(address); |
| | 71 | booking.setPets(pets); |
| | 72 | |
| | 73 | if (serviceType != null && !serviceType.isEmpty()) { |
| | 74 | serviceRepository.findAll().stream() |
| | 75 | .filter(s -> s.getType().equals(serviceType)) |
| | 76 | .findFirst() |
| | 77 | .ifPresent(service -> booking.setServices(List.of(service))); |
| | 78 | } |
| | 79 | |
| | 80 | return bookingRepository.save(booking); |
| | 81 | } |
| | 82 | }}} |
| | 83 | |
| | 84 | === Fetching bookings for a user === |
| | 85 | {{{ |
| | 86 | #!java |
| | 87 | @Transactional(readOnly = true) |
| | 88 | public List<Booking> getBookingsForSitter(String sitterId) { |
| | 89 | return bookingRepository.findBySitter_UserIdOrderByDateFromDesc(sitterId); |
| | 90 | } |
| | 91 | |
| | 92 | @Transactional(readOnly = true) |
| | 93 | public List<Booking> getBookingsForOwner(String ownerId) { |
| | 94 | return bookingRepository.findByOwner_UserIdOrderByDateFromDesc(ownerId); |
| | 95 | } |
| | 96 | }}} |
| | 97 | |
| | 98 | === Adding a new pet === |
| | 99 | {{{ |
| | 100 | #!java |
| | 101 | @Transactional |
| | 102 | public Pet addPet(String name, Integer age, String specialNeeds, String description, String photoUrl, String ownerId, String petTypeId) { |
| | 103 | PetOwner owner = petOwnerRepository.findById(ownerId).orElseThrow(() -> new IllegalArgumentException("Invalid owner ID")); |
| | 104 | PetType petType = petTypeRepository.findById(petTypeId).orElseThrow(() -> new IllegalArgumentException("Invalid pet type ID")); |
| | 105 | |
| | 106 | Pet pet = new Pet(); |
| | 107 | pet.setName(name); |
| | 108 | pet.setAge(age); |
| | 109 | pet.setSpecialNeeds(specialNeeds); |
| | 110 | pet.setDescription(description); |
| | 111 | pet.setPhoto(photoUrl); |
| | 112 | pet.setOwner(owner); |
| | 113 | pet.setPetType(petType); |
| | 114 | |
| | 115 | return petRepository.save(pet); |
| | 116 | } |
| | 117 | }}} |
| | 118 | |
| | 119 | === Managing Reviews === |
| | 120 | {{{ |
| | 121 | #!java |
| | 122 | @Transactional |
| | 123 | public Review addReview(String bookingId, Integer rating, String comment) { |
| | 124 | Booking booking = bookingRepository.findById(bookingId).orElseThrow(() -> new IllegalArgumentException("Invalid booking")); |
| | 125 | Review review = new Review(); |
| | 126 | review.setBooking(booking); |
| | 127 | review.setRating(rating); |
| | 128 | review.setComment(comment); |
| | 129 | return reviewRepository.save(review); |
| | 130 | } |
| | 131 | |
| | 132 | @Transactional(readOnly = true) |
| | 133 | public Double getAverageRating(String sitterId) { |
| | 134 | return reviewRepository.getAverageRatingForSitter(sitterId); |
| | 135 | } |
| | 136 | }}} |
| | 137 | |
| | 138 | == Pooling == |
| | 139 | |
| | 140 | Since PetSitter uses Spring Boot, the database connections are automatically handled with '''HikariCP'''. |
| | 141 | |
| | 142 | HikariCP comes with the `spring-boot-starter-data-jpa` dependency, but I explicitly forced the library version to `<hikaricp.version>6.2.1</hikaricp.version>` in the `pom.xml` file for more verbose logging capabilities. |
| | 143 | |
| | 144 | HikariCP and logging parameters inside `application.properties`: |
| | 145 | |
| | 146 | {{{ |
| | 147 | #!text |
| | 148 | spring.datasource.hikari.maximum-pool-size=20 |
| | 149 | spring.datasource.hikari.minimum-idle=5 |
| | 150 | spring.datasource.hikari.connection-timeout=20000 |
| | 151 | spring.datasource.hikari.idle-timeout=300000 |
| | 152 | spring.datasource.hikari.max-lifetime=1200000 |
| | 153 | spring.datasource.hikari.auto-commit=true |
| | 154 | |
| | 155 | logging.level.com.zaxxer.hikari.HikariConfig=DEBUG |
| | 156 | logging.level.com.zaxxer.hikari=DEBUG |
| | 157 | logging.level.org.springframework.transaction.interceptor=TRACE |
| | 158 | logging.level.org.springframework.orm.jpa.JpaTransactionManager=DEBUG |
| | 159 | }}} |
| | 160 | |
| | 161 | === Pooling configuration === |
| | 162 | * '''`maximum-pool-size` (20)''': Number of concurrent database connections. This prevents the PostgreSQL server from having too many connections during heavy usage. |
| | 163 | * '''`minimum-idle` (5)''': Instructs HikariCP to always keep at least 5 idle in the background, so sudden incoming requests can query the database instantly without disrupting regular users connection. |
| | 164 | * '''`connection-timeout` (20s)''': If all 20 connections are in use, new requests will wait 20 seconds for a free slot until it throws a timeout exception. |
| | 165 | * '''`idle-timeout` (5m)''': Any connection after the minimum 5th connection that is not actively used for 5 minutes will be terminated |
| | 166 | * '''`max-lifetime` (20m)''': Every connection can last up to maximum of 20 minutes. |
| | 167 | * '''`auto-commit` (true)''': The database automatically commits/saves every change without explicitly being told to do so. |
| | 168 | |
| | 169 | === Logging configuration === |
| | 170 | Additional verbose logs for the connection pools: |
| | 171 | * '''`com.zaxxer.hikari.HikariConfig=DEBUG`''': Outputs the pooling settings Hikari uses when the application is started. |
| | 172 | * '''`com.zaxxer.hikari=DEBUG`''': Shows pooling status in real time (active vs. idle connections) and logs when connections are created or closed. |
| | 173 | * '''`org.springframework.transaction.interceptor=TRACE`''': Logs exactly when Spring executes a `@Transactional` method in the code |
| | 174 | * '''`org.springframework.orm.jpa.JpaTransactionManager=DEBUG`''': Logs the actual database transactions happening in the background |
| | 175 | |
| | 176 | |
| | 177 | === Pooling Logs === |
| | 178 | Below is the log output proving the successful initialization and population of the HikariCP connection pool upon application startup. As configured, exactly 5 background connections are established and kept idle for immediate request processing. |
| | 179 | |
| | 180 | {{{ |
| | 181 | #!text |
| | 182 | 2026-05-25T15:27:56.827Z INFO 1 --- [petsitter] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... |
| | 183 | 2026-05-25T15:27:56.842Z DEBUG 1 --- [petsitter] [ main] com.zaxxer.hikari.pool.PoolBase : HikariPool-1 - Attempting to create/setup new connection (7c6a30fc-4cfe-446a-a352-818177bca0d1) |
| | 184 | 2026-05-25T15:27:57.233Z DEBUG 1 --- [petsitter] [ main] com.zaxxer.hikari.pool.PoolBase : HikariPool-1 - Established new connection (7c6a30fc-4cfe-446a-a352-818177bca0d1) |
| | 185 | 2026-05-25T15:27:57.235Z INFO 1 --- [petsitter] [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@2ab9e43e |
| | 186 | 2026-05-25T15:27:57.340Z DEBUG 1 --- [petsitter] [onnection-adder] com.zaxxer.hikari.pool.PoolBase : HikariPool-1 - Attempting to create/setup new connection (3367dbcd-5010-4605-ab92-423e4562c990) |
| | 187 | 2026-05-25T15:27:57.869Z DEBUG 1 --- [petsitter] [onnection-adder] com.zaxxer.hikari.pool.PoolBase : HikariPool-1 - Established new connection (3367dbcd-5010-4605-ab92-423e4562c990) |
| | 188 | 2026-05-25T15:27:57.870Z DEBUG 1 --- [petsitter] [onnection-adder] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@411815df |
| | 189 | 2026-05-25T15:27:57.900Z DEBUG 1 --- [petsitter] [onnection-adder] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - After adding stats (total=2/20, idle=2/5, active=0, waiting=0) |
| | 190 | 2026-05-25T15:27:57.901Z DEBUG 1 --- [petsitter] [onnection-adder] com.zaxxer.hikari.pool.PoolBase : HikariPool-1 - Attempting to create/setup new connection (a71e892a-e173-41ba-909b-ca4e547437fd) |
| | 191 | 2026-05-25T15:27:58.525Z DEBUG 1 --- [petsitter] [onnection-adder] com.zaxxer.hikari.pool.PoolBase : HikariPool-1 - Established new connection (a71e892a-e173-41ba-909b-ca4e547437fd) |
| | 192 | 2026-05-25T15:27:58.525Z DEBUG 1 --- [petsitter] [onnection-adder] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@4fdfe38c |
| | 193 | 2026-05-25T15:27:58.555Z DEBUG 1 --- [petsitter] [onnection-adder] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - After adding stats (total=3/20, idle=3/5, active=0, waiting=0) |
| | 194 | 2026-05-25T15:27:58.556Z DEBUG 1 --- [petsitter] [onnection-adder] com.zaxxer.hikari.pool.PoolBase : HikariPool-1 - Attempting to create/setup new connection (670b5f5c-bddb-4a45-83ef-aa7a30a54530) |
| | 195 | 2026-05-25T15:27:58.631Z DEBUG 1 --- [petsitter] [onnection-adder] com.zaxxer.hikari.pool.PoolBase : HikariPool-1 - Established new connection (670b5f5c-bddb-4a45-83ef-aa7a30a54530) |
| | 196 | 2026-05-25T15:27:58.632Z DEBUG 1 --- [petsitter] [onnection-adder] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@4be07eaf |
| | 197 | 2026-05-25T15:27:58.662Z DEBUG 1 --- [petsitter] [onnection-adder] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - After adding stats (total=4/20, idle=4/5, active=0, waiting=0) |
| | 198 | 2026-05-25T15:27:58.662Z DEBUG 1 --- [petsitter] [onnection-adder] com.zaxxer.hikari.pool.PoolBase : HikariPool-1 - Attempting to create/setup new connection (81793045-e2cb-42de-9b40-e06e476bbea7) |
| | 199 | 2026-05-25T15:27:58.735Z DEBUG 1 --- [petsitter] [onnection-adder] com.zaxxer.hikari.pool.PoolBase : HikariPool-1 - Established new connection (81793045-e2cb-42de-9b40-e06e476bbea7) |
| | 200 | 2026-05-25T15:27:58.735Z DEBUG 1 --- [petsitter] [onnection-adder] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@ee2e9b7 |
| | 201 | 2026-05-25T15:27:58.765Z DEBUG 1 --- [petsitter] [onnection-adder] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - After adding stats (total=5/20, idle=5/5, active=0, waiting=0) |
| | 202 | }}} |