| | 1 | = Напреден апликативен развој |
| | 2 | |
| | 3 | == Трансакции |
| | 4 | |
| | 5 | === Зачувување на нов корисник |
| | 6 | |
| | 7 | {{{ |
| | 8 | @Transactional |
| | 9 | public UserResponseDto registerAndLogIn(HttpServletResponse response, AuthRequestDto authRequestDto) |
| | 10 | throws IOException { |
| | 11 | if (userRepository.findByUsername(authRequestDto.username()).isPresent()){ |
| | 12 | throw new UserAlreadyExistsException(); |
| | 13 | } |
| | 14 | User user = createNonAdminUser(authRequestDto); |
| | 15 | return authenticateAndRespond(response, user); |
| | 16 | } |
| | 17 | |
| | 18 | |
| | 19 | @Transactional |
| | 20 | public User createNonAdminUser(AuthRequestDto authRequestDto) throws IOException { |
| | 21 | User.UserBuilder userBuilder = User.builder() |
| | 22 | .username(authRequestDto.username()) |
| | 23 | .fullName(authRequestDto.fullname()) |
| | 24 | .email(authRequestDto.email()) |
| | 25 | .password(passwordEncoder.encode(authRequestDto.password())) |
| | 26 | .role(Role.NONADMIN) |
| | 27 | .artist(authRequestDto.userType().contains(UserType.ARTIST)) |
| | 28 | .listener(authRequestDto.userType().contains(UserType.LISTENER)); |
| | 29 | |
| | 30 | |
| | 31 | MultipartFile profilePhoto = authRequestDto.profilePhoto(); |
| | 32 | if (profilePhoto != null && !profilePhoto.isEmpty()) { |
| | 33 | String contentType = profilePhoto.getContentType(); |
| | 34 | if (contentType == null || !contentType.startsWith("image/")) { |
| | 35 | throw InvalidFileException.invalidType(); |
| | 36 | } |
| | 37 | |
| | 38 | if (profilePhoto.getSize() > MAX_FILE_SIZE) { |
| | 39 | throw InvalidFileException.tooLarge(MAX_FILE_SIZE); |
| | 40 | } |
| | 41 | |
| | 42 | String filename = UUID.randomUUID() + "-" + profilePhoto.getOriginalFilename(); |
| | 43 | Path path = Paths.get("uploads/profile-pictures", filename); |
| | 44 | Files.copy(profilePhoto.getInputStream(), path); |
| | 45 | userBuilder.profilePhoto("profile-pictures/" + filename); |
| | 46 | } |
| | 47 | |
| | 48 | User user = userBuilder.build(); |
| | 49 | |
| | 50 | NonAdminUser nonAdminUser = new NonAdminUser(); |
| | 51 | nonAdminUser.setUser(user); |
| | 52 | user.setNonAdminUser(nonAdminUser); |
| | 53 | nonAdminUserRepository.save(nonAdminUser); |
| | 54 | |
| | 55 | if (user.isArtist()){ |
| | 56 | Artist artist = new Artist(); |
| | 57 | artist.setNonAdminUser(nonAdminUser); |
| | 58 | artistRepository.save(artist); |
| | 59 | } |
| | 60 | if (user.isListener()){ |
| | 61 | Listener listener = new Listener(); |
| | 62 | listener.setNonAdminUser(nonAdminUser); |
| | 63 | listenerRepository.save(listener); |
| | 64 | } |
| | 65 | |
| | 66 | return user; |
| | 67 | } |
| | 68 | }}} |
| | 69 | |
| | 70 | === Објавување на нова песна |
| | 71 | |
| | 72 | {{{ |
| | 73 | @Transactional |
| | 74 | public void handleSongPublish(PublishSongRequestDto requestDto) throws IOException { |
| | 75 | Long userId = authService.getCurrentUserID(); |
| | 76 | Artist artist = artistService.getArtistById(userId); |
| | 77 | |
| | 78 | MusicalEntity musicalEntity = createMusicalEntity( |
| | 79 | requestDto.getTitle(), |
| | 80 | requestDto.getGenre(), |
| | 81 | requestDto.getCover(), |
| | 82 | artist |
| | 83 | ); |
| | 84 | |
| | 85 | Song song = new Song(); |
| | 86 | song.setAlbum(null); |
| | 87 | song.setLink(requestDto.getLink()); |
| | 88 | song.setMusicalEntities(musicalEntity); |
| | 89 | songRepository.save(song); |
| | 90 | |
| | 91 | // add publishing artist |
| | 92 | saveContribution(artist, musicalEntity, MAIN_VOCAL_ROLE); |
| | 93 | |
| | 94 | // add additional contributors |
| | 95 | saveContributionsFromDto(musicalEntity, requestDto.getContributors()); |
| | 96 | } |
| | 97 | |
| | 98 | @Transactional |
| | 99 | protected MusicalEntity createMusicalEntity(String title, String genre, MultipartFile cover, Artist releasedBy) |
| | 100 | throws IOException { |
| | 101 | MusicalEntity.MusicalEntityBuilder builder = MusicalEntity.builder() |
| | 102 | .title(title) |
| | 103 | .genre(genre) |
| | 104 | .releaseDate(LocalDate.now()) |
| | 105 | .releasedBy(releasedBy); |
| | 106 | |
| | 107 | if (cover != null && !cover.isEmpty()) { |
| | 108 | try { |
| | 109 | String filename = saveCoverPhoto(cover); |
| | 110 | builder.cover("profile-pictures/" + filename); |
| | 111 | } catch (Exception ignored) {} |
| | 112 | } |
| | 113 | |
| | 114 | return musicalEntityRepository.save(builder.build()); |
| | 115 | } |
| | 116 | |
| | 117 | @Transactional |
| | 118 | protected void saveContribution(Artist artist, MusicalEntity musicalEntity, String role) { |
| | 119 | ArtistContributionId id = new ArtistContributionId(); |
| | 120 | id.setArtistId(artist.getId()); |
| | 121 | id.setMusicalEntityId(musicalEntity.getId()); |
| | 122 | |
| | 123 | ArtistContribution contribution = new ArtistContribution(); |
| | 124 | contribution.setId(id); |
| | 125 | contribution.setArtist(artist); |
| | 126 | contribution.setMusicalEntity(musicalEntity); |
| | 127 | contribution.setRole(role); |
| | 128 | |
| | 129 | artistContributionRepository.save(contribution); |
| | 130 | } |
| | 131 | |
| | 132 | @Transactional |
| | 133 | protected void saveContributionsFromDto(MusicalEntity musicalEntity, List<ArtistContributionSummaryDto> contributors) { |
| | 134 | if (contributors == null) return; |
| | 135 | |
| | 136 | for (ArtistContributionSummaryDto contributorDto : contributors) { |
| | 137 | if (contributorDto.getId() == null) continue; |
| | 138 | |
| | 139 | Artist contributor = artistService.getArtistById(contributorDto.getId()); |
| | 140 | saveContribution(contributor, musicalEntity, contributorDto.getRole()); |
| | 141 | } |
| | 142 | } |
| | 143 | }}} |
| | 144 | |
| | 145 | === Објавување на нов албум |
| | 146 | |
| | 147 | {{{ |
| | 148 | @Transactional |
| | 149 | public void handleAlbumPublish(PublishAlbumRequestDto requestDto) throws IOException { |
| | 150 | Long userId = authService.getCurrentUserID(); |
| | 151 | Artist artist = artistService.getArtistById(userId); |
| | 152 | |
| | 153 | MusicalEntity albumMusicalEntity = createMusicalEntity( |
| | 154 | requestDto.getTitle(), |
| | 155 | requestDto.getGenre(), |
| | 156 | requestDto.getCover(), |
| | 157 | artist |
| | 158 | ); |
| | 159 | |
| | 160 | Album album = new Album(); |
| | 161 | album.setMusicalEntities(albumMusicalEntity); |
| | 162 | album = albumRepository.save(album); |
| | 163 | |
| | 164 | // add publishing artist |
| | 165 | saveContribution(artist, albumMusicalEntity, MAIN_ROLE); |
| | 166 | |
| | 167 | // track artists already added to avoid duplicates |
| | 168 | Set<Long> albumContributorIds = new HashSet<>(); |
| | 169 | albumContributorIds.add(artist.getId()); |
| | 170 | |
| | 171 | List<AlbumSongsDto> albumSongs = requestDto.getAlbumSongs(); |
| | 172 | for (AlbumSongsDto songDto : albumSongs) { |
| | 173 | createSongForAlbum(albumMusicalEntity, album, songDto, artist, albumContributorIds); |
| | 174 | } |
| | 175 | } |
| | 176 | |
| | 177 | |
| | 178 | @Transactional |
| | 179 | protected void createSongForAlbum(MusicalEntity albumMe, Album album, AlbumSongsDto songDto, |
| | 180 | Artist publishingArtist, Set<Long> albumContributorIds) { |
| | 181 | MusicalEntity songMusicalEntity = MusicalEntity.builder() |
| | 182 | .title(songDto.getTitle()) |
| | 183 | .releaseDate(albumMe.getReleaseDate()) |
| | 184 | .releasedBy(albumMe.getReleasedBy()) |
| | 185 | .cover(albumMe.getCover()) |
| | 186 | .genre(albumMe.getGenre()) |
| | 187 | .build(); |
| | 188 | songMusicalEntity = musicalEntityRepository.save(songMusicalEntity); |
| | 189 | |
| | 190 | Song song = new Song(); |
| | 191 | song.setLink(songDto.getLink()); |
| | 192 | song.setAlbum(album); |
| | 193 | song.setMusicalEntities(songMusicalEntity); |
| | 194 | songRepository.save(song); |
| | 195 | |
| | 196 | saveContribution(publishingArtist, songMusicalEntity, MAIN_VOCAL_ROLE); |
| | 197 | |
| | 198 | // add song-specific contributors |
| | 199 | List<ArtistContributionSummaryDto> songContributors = songDto.getContributors(); |
| | 200 | if (songContributors != null) { |
| | 201 | for (ArtistContributionSummaryDto contributorDto : songContributors) { |
| | 202 | if (contributorDto.getId() == null) continue; |
| | 203 | |
| | 204 | Artist contributor = artistService.getArtistById(contributorDto.getId()); |
| | 205 | |
| | 206 | // first add to song |
| | 207 | saveContribution(contributor, songMusicalEntity, contributorDto.getRole()); |
| | 208 | |
| | 209 | // then add to album if not already added |
| | 210 | if (!albumContributorIds.contains(contributor.getId())) { |
| | 211 | saveContribution(contributor, album.getMusicalEntities(), contributorDto.getRole()); |
| | 212 | albumContributorIds.add(contributor.getId()); |
| | 213 | } |
| | 214 | } |
| | 215 | } |
| | 216 | } |
| | 217 | }}} |
| | 218 | |
| | 219 | == Pooling |
| | 220 | |
| | 221 | Бидејќи користиме Spring Boot за развој на backend делот од нашата апликација, не треба рачно да ги правиме конекциите кон базата. Pooling во Spring Boot се менаџира од страна на HikariCP. |
| | 222 | |
| | 223 | HikariCP е зависност на `spring-boot-starter-jdbc`, што е зависност на `spring-boot-starter-data-jpa`, така што доколку во `pom.xml` додадеме: |
| | 224 | {{{ |
| | 225 | <dependency> |
| | 226 | <groupId>org.springframework.boot</groupId> |
| | 227 | <artifactId>spring-boot-starter-data-jpa</artifactId> |
| | 228 | </dependency> |
| | 229 | }}} |
| | 230 | |
| | 231 | HikariCP ќе ни биде додадено како транзитивна зависност. |
| | 232 | |
| | 233 | Во нашиот проект не ја менувавме конфигурацијата на Hikari, така што се користат default вредности, кои се следниве: |
| | 234 | {{{ |
| | 235 | spring.datasource.hikari.maximum-pool-size = 10 |
| | 236 | spring.datasource.hikari.minimum-idle = 10 |
| | 237 | spring.datasource.hikari.connection-timeout = 30000 #(30 seconds) |
| | 238 | spring.datasource.hikari.idle-timeout = 600000 #(10 minutes) |
| | 239 | spring.datasource.hikari.max-lifetime = 1800000 #(30 minutes) |
| | 240 | spring.datasource.hikari.auto-commit = true |
| | 241 | }}} |
| | 242 | |
| | 243 | По потреба истите можат да се менуваат. |
| | 244 | |
| | 245 | Дополнително, воспоставување на конекциите можеме да забележиме во логовите на тунел скриптата кога ќе ја започнеме Spring Boot апликацијата |
| | 246 | {{{ |
| | 247 | debug1: Connection to port 9999 forwarding to localhost port 5432 requested. |
| | 248 | debug1: channel 2: new direct-tcpip [direct-tcpip] (inactive timeout: 0) |
| | 249 | debug1: Connection to port 9999 forwarding to localhost port 5432 requested. |
| | 250 | debug1: channel 3: new direct-tcpip [direct-tcpip] (inactive timeout: 0) |
| | 251 | debug1: Connection to port 9999 forwarding to localhost port 5432 requested. |
| | 252 | debug1: channel 4: new direct-tcpip [direct-tcpip] (inactive timeout: 0) |
| | 253 | debug1: Connection to port 9999 forwarding to localhost port 5432 requested. |
| | 254 | debug1: channel 5: new direct-tcpip [direct-tcpip] (inactive timeout: 0) |
| | 255 | debug1: Connection to port 9999 forwarding to localhost port 5432 requested. |
| | 256 | debug1: channel 6: new direct-tcpip [direct-tcpip] (inactive timeout: 0) |
| | 257 | debug1: Connection to port 9999 forwarding to localhost port 5432 requested. |
| | 258 | debug1: channel 7: new direct-tcpip [direct-tcpip] (inactive timeout: 0) |
| | 259 | debug1: Connection to port 9999 forwarding to localhost port 5432 requested. |
| | 260 | debug1: channel 8: new direct-tcpip [direct-tcpip] (inactive timeout: 0) |
| | 261 | debug1: Connection to port 9999 forwarding to localhost port 5432 requested. |
| | 262 | debug1: channel 9: new direct-tcpip [direct-tcpip] (inactive timeout: 0) |
| | 263 | debug1: Connection to port 9999 forwarding to localhost port 5432 requested. |
| | 264 | debug1: channel 10: new direct-tcpip [direct-tcpip] (inactive timeout: 0) |
| | 265 | debug1: Connection to port 9999 forwarding to localhost port 5432 requested. |
| | 266 | debug1: channel 11: new direct-tcpip [direct-tcpip] (inactive timeout: 0) |
| | 267 | }}} |