= Напреден апликативен развој == Трансакции === Зачувување на нов корисник {{{ @Transactional public UserResponseDto registerAndLogIn(HttpServletResponse response, AuthRequestDto authRequestDto) throws IOException { if (userRepository.findByUsername(authRequestDto.username()).isPresent()){ throw new UserAlreadyExistsException(); } User user = createNonAdminUser(authRequestDto); return authenticateAndRespond(response, user); } @Transactional public User createNonAdminUser(AuthRequestDto authRequestDto) throws IOException { User.UserBuilder userBuilder = User.builder() .username(authRequestDto.username()) .fullName(authRequestDto.fullname()) .email(authRequestDto.email()) .password(passwordEncoder.encode(authRequestDto.password())) .role(Role.NONADMIN) .artist(authRequestDto.userType().contains(UserType.ARTIST)) .listener(authRequestDto.userType().contains(UserType.LISTENER)); MultipartFile profilePhoto = authRequestDto.profilePhoto(); if (profilePhoto != null && !profilePhoto.isEmpty()) { String contentType = profilePhoto.getContentType(); if (contentType == null || !contentType.startsWith("image/")) { throw InvalidFileException.invalidType(); } if (profilePhoto.getSize() > MAX_FILE_SIZE) { throw InvalidFileException.tooLarge(MAX_FILE_SIZE); } String filename = UUID.randomUUID() + "-" + profilePhoto.getOriginalFilename(); Path path = Paths.get("uploads/profile-pictures", filename); Files.copy(profilePhoto.getInputStream(), path); userBuilder.profilePhoto("profile-pictures/" + filename); } User user = userBuilder.build(); NonAdminUser nonAdminUser = new NonAdminUser(); nonAdminUser.setUser(user); user.setNonAdminUser(nonAdminUser); nonAdminUserRepository.save(nonAdminUser); if (user.isArtist()){ Artist artist = new Artist(); artist.setNonAdminUser(nonAdminUser); artistRepository.save(artist); } if (user.isListener()){ Listener listener = new Listener(); listener.setNonAdminUser(nonAdminUser); listenerRepository.save(listener); } return user; } }}} === Објавување на нова песна {{{ @Transactional public void handleSongPublish(PublishSongRequestDto requestDto) throws IOException { Long userId = authService.getCurrentUserID(); Artist artist = artistService.getArtistById(userId); MusicalEntity musicalEntity = createMusicalEntity( requestDto.getTitle(), requestDto.getGenre(), requestDto.getCover(), artist ); Song song = new Song(); song.setAlbum(null); song.setLink(requestDto.getLink()); song.setMusicalEntities(musicalEntity); songRepository.save(song); // add publishing artist saveContribution(artist, musicalEntity, MAIN_VOCAL_ROLE); // add additional contributors saveContributionsFromDto(musicalEntity, requestDto.getContributors()); } @Transactional protected MusicalEntity createMusicalEntity(String title, String genre, MultipartFile cover, Artist releasedBy) throws IOException { MusicalEntity.MusicalEntityBuilder builder = MusicalEntity.builder() .title(title) .genre(genre) .releaseDate(LocalDate.now()) .releasedBy(releasedBy); if (cover != null && !cover.isEmpty()) { try { String filename = saveCoverPhoto(cover); builder.cover("profile-pictures/" + filename); } catch (Exception ignored) {} } return musicalEntityRepository.save(builder.build()); } @Transactional protected void saveContribution(Artist artist, MusicalEntity musicalEntity, String role) { ArtistContributionId id = new ArtistContributionId(); id.setArtistId(artist.getId()); id.setMusicalEntityId(musicalEntity.getId()); ArtistContribution contribution = new ArtistContribution(); contribution.setId(id); contribution.setArtist(artist); contribution.setMusicalEntity(musicalEntity); contribution.setRole(role); artistContributionRepository.save(contribution); } @Transactional protected void saveContributionsFromDto(MusicalEntity musicalEntity, List contributors) { if (contributors == null) return; for (ArtistContributionSummaryDto contributorDto : contributors) { if (contributorDto.getId() == null) continue; Artist contributor = artistService.getArtistById(contributorDto.getId()); saveContribution(contributor, musicalEntity, contributorDto.getRole()); } } }}} === Објавување на нов албум {{{ @Transactional public void handleAlbumPublish(PublishAlbumRequestDto requestDto) throws IOException { Long userId = authService.getCurrentUserID(); Artist artist = artistService.getArtistById(userId); MusicalEntity albumMusicalEntity = createMusicalEntity( requestDto.getTitle(), requestDto.getGenre(), requestDto.getCover(), artist ); Album album = new Album(); album.setMusicalEntities(albumMusicalEntity); album = albumRepository.save(album); // add publishing artist saveContribution(artist, albumMusicalEntity, MAIN_ROLE); // track artists already added to avoid duplicates Set albumContributorIds = new HashSet<>(); albumContributorIds.add(artist.getId()); List albumSongs = requestDto.getAlbumSongs(); for (AlbumSongsDto songDto : albumSongs) { createSongForAlbum(albumMusicalEntity, album, songDto, artist, albumContributorIds); } } @Transactional protected void createSongForAlbum(MusicalEntity albumMe, Album album, AlbumSongsDto songDto, Artist publishingArtist, Set albumContributorIds) { MusicalEntity songMusicalEntity = MusicalEntity.builder() .title(songDto.getTitle()) .releaseDate(albumMe.getReleaseDate()) .releasedBy(albumMe.getReleasedBy()) .cover(albumMe.getCover()) .genre(albumMe.getGenre()) .build(); songMusicalEntity = musicalEntityRepository.save(songMusicalEntity); Song song = new Song(); song.setLink(songDto.getLink()); song.setAlbum(album); song.setMusicalEntities(songMusicalEntity); songRepository.save(song); saveContribution(publishingArtist, songMusicalEntity, MAIN_VOCAL_ROLE); // add song-specific contributors List songContributors = songDto.getContributors(); if (songContributors != null) { for (ArtistContributionSummaryDto contributorDto : songContributors) { if (contributorDto.getId() == null) continue; Artist contributor = artistService.getArtistById(contributorDto.getId()); // first add to song saveContribution(contributor, songMusicalEntity, contributorDto.getRole()); // then add to album if not already added if (!albumContributorIds.contains(contributor.getId())) { saveContribution(contributor, album.getMusicalEntities(), contributorDto.getRole()); albumContributorIds.add(contributor.getId()); } } } } }}} == Pooling Бидејќи користиме Spring Boot за развој на backend делот од нашата апликација, не треба рачно да ги правиме конекциите кон базата. Pooling во Spring Boot се менаџира од страна на HikariCP. HikariCP е зависност на `spring-boot-starter-jdbc`, што е зависност на `spring-boot-starter-data-jpa`, така што доколку во `pom.xml` додадеме: {{{ org.springframework.boot spring-boot-starter-data-jpa }}} HikariCP ќе ни биде додадено како транзитивна зависност. Во нашиот проект не ја менувавме конфигурацијата на Hikari, така што се користат default вредности, кои се следниве: {{{ spring.datasource.hikari.maximum-pool-size = 10 spring.datasource.hikari.minimum-idle = 10 spring.datasource.hikari.connection-timeout = 30000 #(30 seconds) spring.datasource.hikari.idle-timeout = 600000 #(10 minutes) spring.datasource.hikari.max-lifetime = 1800000 #(30 minutes) spring.datasource.hikari.auto-commit = true }}} По потреба истите можат да се менуваат. Дополнително, воспоставување на конекциите можеме да забележиме во логовите на тунел скриптата кога ќе ја започнеме Spring Boot апликацијата {{{ debug1: Connection to port 9999 forwarding to localhost port 5432 requested. debug1: channel 2: new direct-tcpip [direct-tcpip] (inactive timeout: 0) debug1: Connection to port 9999 forwarding to localhost port 5432 requested. debug1: channel 3: new direct-tcpip [direct-tcpip] (inactive timeout: 0) debug1: Connection to port 9999 forwarding to localhost port 5432 requested. debug1: channel 4: new direct-tcpip [direct-tcpip] (inactive timeout: 0) debug1: Connection to port 9999 forwarding to localhost port 5432 requested. debug1: channel 5: new direct-tcpip [direct-tcpip] (inactive timeout: 0) debug1: Connection to port 9999 forwarding to localhost port 5432 requested. debug1: channel 6: new direct-tcpip [direct-tcpip] (inactive timeout: 0) debug1: Connection to port 9999 forwarding to localhost port 5432 requested. debug1: channel 7: new direct-tcpip [direct-tcpip] (inactive timeout: 0) debug1: Connection to port 9999 forwarding to localhost port 5432 requested. debug1: channel 8: new direct-tcpip [direct-tcpip] (inactive timeout: 0) debug1: Connection to port 9999 forwarding to localhost port 5432 requested. debug1: channel 9: new direct-tcpip [direct-tcpip] (inactive timeout: 0) debug1: Connection to port 9999 forwarding to localhost port 5432 requested. debug1: channel 10: new direct-tcpip [direct-tcpip] (inactive timeout: 0) debug1: Connection to port 9999 forwarding to localhost port 5432 requested. debug1: channel 11: new direct-tcpip [direct-tcpip] (inactive timeout: 0) }}}