From cf727f179951f717ab651def4b75c2046b707b67 Mon Sep 17 00:00:00 2001 From: LightInTheFire <109972737+LightInTheFire@users.noreply.github.com> Date: Sun, 12 Oct 2025 22:53:22 +0300 Subject: [PATCH 01/13] feat: added endpoint to find common films of user and his friend (#9) --- .../filmorate/controller/FilmController.java | 7 ++++ .../repository/film/FilmRepository.java | 2 ++ .../repository/film/JdbcFilmRepository.java | 34 +++++++++++++++++++ .../friendship/FriendshipsRepository.java | 2 ++ .../friendship/JdbcFriendshipsRepository.java | 18 ++++++++-- .../filmorate/service/FilmService.java | 2 ++ .../filmorate/service/FilmServiceImpl.java | 11 ++++++ 7 files changed, 74 insertions(+), 2 deletions(-) diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java index 3852fc6..5a133dc 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -69,6 +69,13 @@ public void deleteLike(@PathVariable @Positive long id, filmService.removeLike(id, userId); } + @GetMapping("/common") + public Collection findCommonFilms(@RequestParam(name = "userId") @Positive long userId, + @RequestParam(name = "friendId") @Positive long friendId) { + log.trace("Find common films requested for user {} and friend {}", userId, friendId); + return filmService.findCommonFilms(userId, friendId); + } + @GetMapping("/popular") public Collection findPopular(@RequestParam(defaultValue = "10") @Positive int count) { log.trace("Find popular film requested with count: {}", count); diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRepository.java index 3317862..edf5e85 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRepository.java @@ -17,4 +17,6 @@ public interface FilmRepository { void deleteById(long id); Collection findTopPopularFilms(int count); + + Collection findCommonFilms(long userId, long friendId); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java index 73b73d2..fd5b42e 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java @@ -161,6 +161,40 @@ ORDER BY COUNT(fl.user_id) DESC, f.film_id return jdbc.query(selectTopFilmsSql, params, filmResultSetExtractor); } + @Override + public Collection findCommonFilms(long userId, long friendId) { + String selectCommonFilmsSql = """ + SELECT f.film_id, + f.name, + f.description, + f.release_date, + f.duration_in_minutes, + f.mpa_id, + mr.name AS mpa_name, + g.genre_id, + g.name AS genre_name + FROM films f + JOIN mpa_ratings mr ON f.mpa_id = mr.mpa_id + LEFT JOIN film_genres fg ON f.film_id = fg.film_id + LEFT JOIN genres g ON g.genre_id = fg.genre_id + LEFT JOIN likes fl ON f.film_id = fl.film_id + WHERE f.film_id in (SELECT l1.film_id + FROM likes l1 + WHERE l1.user_id = :user_id + INTERSECT + SELECT l2.film_id + FROM likes l2 + WHERE l2.user_id = :friend_id) + GROUP BY f.film_id, f.name, g.genre_id + ORDER BY COUNT(fl.user_id) DESC, f.film_id + """; + MapSqlParameterSource params = new MapSqlParameterSource() + .addValue("user_id", userId) + .addValue("friend_id", friendId); + + return jdbc.query(selectCommonFilmsSql, params, filmResultSetExtractor); + } + private void saveGenres(Set genres, long filmId) { String insertGenresSql = """ INSERT INTO film_genres (film_id, genre_id) diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/friendship/FriendshipsRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/friendship/FriendshipsRepository.java index 8f58c07..0dfeaa1 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/repository/friendship/FriendshipsRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/friendship/FriendshipsRepository.java @@ -4,4 +4,6 @@ public interface FriendshipsRepository { void addFriendship(long userId, long friendId); void removeFriendship(long userId, long friendId); + + boolean isFriends(long userId, long friendId); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/friendship/JdbcFriendshipsRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/friendship/JdbcFriendshipsRepository.java index 8fe0296..bf45d9c 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/repository/friendship/JdbcFriendshipsRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/friendship/JdbcFriendshipsRepository.java @@ -28,9 +28,23 @@ public void removeFriendship(long userId, long friendId) { jdbc.update(deleteFriendshipSql, params); } + @Override + public boolean isFriends(long userId, long friendId) { + String checkFriendshipSql = """ + SELECT EXISTS ( + SELECT 1 + FROM friendships + WHERE user_id1 = :user_id AND user_id2 = :friend_id)"""; + MapSqlParameterSource params = new MapSqlParameterSource() + .addValue("user_id", userId) + .addValue("friend_id", friendId); + + return Boolean.TRUE.equals(jdbc.queryForObject(checkFriendshipSql, params, Boolean.class)); + } + private MapSqlParameterSource getParameterMap(long userId, long friendId) { return new MapSqlParameterSource() - .addValue("user_id1", userId) - .addValue("user_id2", friendId); + .addValue("user_id1", userId) + .addValue("user_id2", friendId); } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java index f0a790f..db4c14f 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java @@ -22,4 +22,6 @@ public interface FilmService { void removeLike(long filmId, long userId); Collection findFilmsWithTopLikes(int count); + + Collection findCommonFilms(long userId, long friendId); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/FilmServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/FilmServiceImpl.java index e03ab81..4252b35 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmServiceImpl.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmServiceImpl.java @@ -126,6 +126,17 @@ public Collection findFilmsWithTopLikes(int count) { .toList(); } + @Override + public Collection findCommonFilms(long userId, long friendId) { + throwIfUserNotFound(userId); + throwIfUserNotFound(friendId); + + return filmRepository.findCommonFilms(userId, friendId) + .stream() + .map(FilmMapper::toFilmDto) + .toList(); + } + private void throwIfUserNotFound(long userId) { userRepository.findById(userId) .orElseThrow(NotFoundException.supplier("User with id %d not found", userId)); From 8034c6b076833cbf8cdd58fb07b071bd5d1df982 Mon Sep 17 00:00:00 2001 From: LightInTheFire <109972737+LightInTheFire@users.noreply.github.com> Date: Tue, 14 Oct 2025 12:21:11 +0300 Subject: [PATCH 02/13] feat: update readme (#11) * feat: added contributors, tables description and updated db_schema.svg in README * fix: added line breaks in readme --- README.md | 38 ++++++++++++++++++- assets/readme/db_schema.svg | 4 +- .../repository/film/JdbcFilmRepository.java | 2 +- .../repository/user/JdbcUserRepository.java | 1 + 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 36857da..50a7f64 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,46 @@ Repository for filmorate - social network for film rating. Written in java. +# Contributors +Search - [Alexander-skipper](https://github.com/Alexander-skipper)
+Feed - [n20va](https://github.com/n20va)
+Recommendations - [Basementdoor](https://github.com/basementdoor)
+Reviews, top popular films by year and genre - [Nikolay Aleksandrov](https://github.com/Alextrusk27)
+ # DB schema ![db schema](assets/readme/db_schema.svg) +# Tables description +This section provides a detailed description of each table in the database schema. + +* Events +This table logs user activities such as likes, reviews, and friendships, serving as an audit or history trail for operations. +* Friendships +This table stores user-to-user friendship relationships. +* Users +This table holds user profile information for personalization. +* Likes +This table records users' likes on films, for recommendation systems or popularity tracking. +* Film Reviews +This table links users and films to their reviews, acting as a junction for many-to-many relationships. +* Reviews +This table stores the content and metadata of user reviews. +* Review Likes +This table tracks likes on reviews, similar to upvoting for helpfulness. +* Films +This core table contains movie details for the platform's catalog. +* Film Directors +This junction table associates films with their directors. +* Directors +This table lists film directors. +* MPA Ratings +This table defines Motion Picture Association ratings (e.g., G, PG, R). +* Film Genres +This junction table links films to genres. +* Genres +This table categorizes film genres (e.g., Action, Drama). + ## Examples of database queries ### Get all films @@ -86,5 +122,5 @@ WHERE u.user_id IN (SELECT user_id2 SELECT user_id2 FROM friendships - WHERE user_id1 = 2 + WHERE user_id1 = 2) ``` diff --git a/assets/readme/db_schema.svg b/assets/readme/db_schema.svg index b08713a..cce2b77 100644 --- a/assets/readme/db_schema.svg +++ b/assets/readme/db_schema.svg @@ -1,4 +1,4 @@ -1*1*1*1*1*1*1*1*1**1*1*1111**1usersuser_idBIGINTemailVARCHAR(80)loginVARCHAR(20)nameVARCHAR(20)birthdayDATEeventstimestampTIMESTAMPuser_idBIGINTevent_typeENUM(LIKE,REVIEW,FRIEND)operationENUM(REMOVE,ADD,UPDATE)event_idBIGINTentity_idBIGINTgenresgenre_idBIGINTnameVARCHAR(64)mpa_ratingsmpa_idBIGINTnameVARCHAR(20)directorsdirector_idBIGINTnameVARCHAR(120)filmsfilm_idBIGINTnameVARCHAR(120)descriptionVARCHAR(200)release_dateDATEduration_in_minutesINTEGERmpa_idBIGINTfilm_directorsfilm_idBIGINTdirector_idBIGINTreviewsreview_idBIGINTcontentVARCHAR(500)is_positiveBOOLEANuseful_ratingBIGINTfilm_reviewsuser_idBIGINTfilm_idBIGINTreview_idBIGINTreview_likesreview_idBIGINTuser_idBIGINTis_positiveBOOLEANfilm_genresfilm_idBIGINTgenre_idBIGINTlikesuser_idBIGINTfilm_idBIGINTfriendshipsuser_id1BIGINTuser_id2BIGINT \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java index fd5b42e..10e91cc 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java @@ -15,7 +15,6 @@ import java.util.function.Function; import java.util.stream.Collectors; - @Repository @RequiredArgsConstructor public class JdbcFilmRepository implements FilmRepository { @@ -132,6 +131,7 @@ public void deleteById(long id) { MapSqlParameterSource params = new MapSqlParameterSource() .addValue("id", id); String deleteFilmByIdSql = "DELETE FROM films WHERE film_id = :id"; + jdbc.update(deleteFilmByIdSql, params); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/user/JdbcUserRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/user/JdbcUserRepository.java index 6851f9b..78799fb 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/repository/user/JdbcUserRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/user/JdbcUserRepository.java @@ -76,6 +76,7 @@ public void deleteById(long id) { MapSqlParameterSource params = new MapSqlParameterSource() .addValue("id", id); String deleteUserByIdSql = "DELETE FROM users WHERE user_id = :id"; + jdbc.update(deleteUserByIdSql, params); } From 11c3d8c4573584e30044016fb724330398bf815f Mon Sep 17 00:00:00 2001 From: LightInTheFire <109972737+LightInTheFire@users.noreply.github.com> Date: Tue, 14 Oct 2025 20:08:03 +0300 Subject: [PATCH 03/13] Add directors to films * feat: added director model, it's dto's, mapper and added tables in schema * feat: added director controller impl and service interface * feat: added request body message not readable handler * fix:changed errorName to name to satisfy tests * feat: added dao layer for director * feat: added director service * feat: updated film model and dto's to contain directors * feat: refactored film to use group_concat inside select, transferred all methods to use rowmapper instead of RSEz * feat: added endpoint to find films of director sorted by year or likes count --- .../controller/DirectorController.java | 57 ++++++ .../filmorate/controller/FilmController.java | 17 +- .../filmorate/controller/FilmsSortBy.java | 14 ++ .../handler/ExceptionApiHandler.java | 15 ++ .../filmorate/dto/director/DirectorDto.java | 7 + .../dto/director/NewDirectorRequest.java | 10 + .../dto/director/UpdateDirectorRequest.java | 18 ++ .../filmorate/dto/error/ErrorResponse.java | 2 +- .../practicum/filmorate/dto/film/FilmDto.java | 2 + .../filmorate/dto/film/NewFilmRequest.java | 3 + .../filmorate/dto/film/UpdateFilmRequest.java | 6 + .../filmorate/mapper/DirectorMapper.java | 36 ++++ .../filmorate/mapper/FilmMapper.java | 14 ++ .../practicum/filmorate/model/Director.java | 14 ++ .../practicum/filmorate/model/Film.java | 2 +- .../director/DirectorRepository.java | 18 ++ .../director/DirectorRowMapper.java | 19 ++ .../director/JdbcDirectorRepository.java | 73 ++++++++ .../repository/film/FilmRepository.java | 3 + .../film/FilmResultSetExtractor.java | 66 ------- .../repository/film/FilmRowMapper.java | 41 ++++ .../repository/film/JdbcFilmRepository.java | 175 ++++++++---------- .../filmorate/service/DirectorService.java | 19 ++ .../service/DirectorServiceImpl.java | 61 ++++++ .../filmorate/service/FilmService.java | 3 + .../filmorate/service/FilmServiceImpl.java | 18 ++ src/main/resources/schema.sql | 15 ++ .../repository/JdbcFilmRepositoryTest.java | 4 +- 28 files changed, 563 insertions(+), 169 deletions(-) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/controller/DirectorController.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/controller/FilmsSortBy.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dto/director/DirectorDto.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dto/director/NewDirectorRequest.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dto/director/UpdateDirectorRequest.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/mapper/DirectorMapper.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/model/Director.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/repository/director/DirectorRepository.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/repository/director/DirectorRowMapper.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/repository/director/JdbcDirectorRepository.java delete mode 100644 src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmResultSetExtractor.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/service/DirectorService.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/service/DirectorServiceImpl.java diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/DirectorController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/DirectorController.java new file mode 100644 index 0000000..80b7033 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/DirectorController.java @@ -0,0 +1,57 @@ +package ru.yandex.practicum.filmorate.controller; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.dto.director.DirectorDto; +import ru.yandex.practicum.filmorate.dto.director.NewDirectorRequest; +import ru.yandex.practicum.filmorate.dto.director.UpdateDirectorRequest; +import ru.yandex.practicum.filmorate.service.DirectorService; + +import java.util.Collection; + +@RestController +@RequestMapping("/directors") +@Slf4j +@RequiredArgsConstructor +@Validated +public class DirectorController { + private final DirectorService directorService; + + @GetMapping + public Collection findAll() { + log.trace("Collection of all directors requested"); + return directorService.findAll(); + } + + @GetMapping("/{id}") + public DirectorDto findById(@PathVariable @Positive long id) { + log.trace("Find director by id requested, id: {}", id); + return directorService.findById(id); + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + @Validated + public DirectorDto create(@RequestBody @Valid NewDirectorRequest request) { + log.trace("Create new director requested: {}", request); + return directorService.create(request); + } + + @PutMapping + @Validated + public DirectorDto update(@RequestBody @Valid UpdateDirectorRequest request) { + log.trace("Update director requested: {}", request); + return directorService.update(request); + } + + @DeleteMapping("/{id}") + public void deleteById(@PathVariable @Positive long id) { + log.trace("Delete director by id requested, id: {}", id); + directorService.deleteById(id); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java index 5a133dc..956c125 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -72,8 +72,8 @@ public void deleteLike(@PathVariable @Positive long id, @GetMapping("/common") public Collection findCommonFilms(@RequestParam(name = "userId") @Positive long userId, @RequestParam(name = "friendId") @Positive long friendId) { - log.trace("Find common films requested for user {} and friend {}", userId, friendId); - return filmService.findCommonFilms(userId, friendId); + log.trace("Find common films requested for user {} and friend {}", userId, friendId); + return filmService.findCommonFilms(userId, friendId); } @GetMapping("/popular") @@ -81,4 +81,17 @@ public Collection findPopular(@RequestParam(defaultValue = "10") @Posit log.trace("Find popular film requested with count: {}", count); return filmService.findFilmsWithTopLikes(count); } + + @GetMapping("/director/{directorId}") + public Collection findFilmsOfDirector(@PathVariable @Positive long directorId, + @RequestParam String sortBy) { + + FilmsSortBy sortFilmsBy = FilmsSortBy.fromString(sortBy); + if (sortFilmsBy == null) { + throw new IllegalArgumentException("invalid sort by: %s".formatted(sortBy)); + } + + log.trace("Find films of director with id {} requested", directorId); + return filmService.findFilmsOfDirector(directorId, sortFilmsBy); + } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmsSortBy.java b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmsSortBy.java new file mode 100644 index 0000000..0fac786 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmsSortBy.java @@ -0,0 +1,14 @@ +package ru.yandex.practicum.filmorate.controller; + +public enum FilmsSortBy { + YEAR, + LIKES; + + public static FilmsSortBy fromString(String sortBy) { + return switch (sortBy.toLowerCase()) { + case "year" -> YEAR; + case "likes" -> LIKES; + default -> null; + }; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/handler/ExceptionApiHandler.java b/src/main/java/ru/yandex/practicum/filmorate/controller/handler/ExceptionApiHandler.java index 63381ab..553c3d6 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/handler/ExceptionApiHandler.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/handler/ExceptionApiHandler.java @@ -3,6 +3,7 @@ import jakarta.validation.ConstraintViolationException; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; +import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; @@ -27,6 +28,20 @@ public ErrorResponse handleException(Exception ex) { "An error occurred while processing request"); } + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse handleIllegalArgumentException(IllegalArgumentException e) { + log.warn(e.getMessage()); + return new ErrorResponse("Bad request", e.getMessage()); + } + + @ExceptionHandler(HttpMessageNotReadableException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse handleHttpMessageNotReadableException(HttpMessageNotReadableException e) { + log.warn(e.getMessage()); + return new ErrorResponse("bad request", "Request body is not readable"); + } + @ExceptionHandler(ConstraintViolationException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public ValidationErrorResponse onConstraintValidationException( diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/director/DirectorDto.java b/src/main/java/ru/yandex/practicum/filmorate/dto/director/DirectorDto.java new file mode 100644 index 0000000..d62ab7c --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/director/DirectorDto.java @@ -0,0 +1,7 @@ +package ru.yandex.practicum.filmorate.dto.director; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public record DirectorDto(Long id, String name) { +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/director/NewDirectorRequest.java b/src/main/java/ru/yandex/practicum/filmorate/dto/director/NewDirectorRequest.java new file mode 100644 index 0000000..cf7c130 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/director/NewDirectorRequest.java @@ -0,0 +1,10 @@ +package ru.yandex.practicum.filmorate.dto.director; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +public class NewDirectorRequest { + @NotBlank + String name; +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/director/UpdateDirectorRequest.java b/src/main/java/ru/yandex/practicum/filmorate/dto/director/UpdateDirectorRequest.java new file mode 100644 index 0000000..f3602cb --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/director/UpdateDirectorRequest.java @@ -0,0 +1,18 @@ +package ru.yandex.practicum.filmorate.dto.director; + +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Data; +import lombok.experimental.FieldDefaults; + +@Data +@FieldDefaults(level = AccessLevel.PRIVATE) +public class UpdateDirectorRequest { + @NotNull + Long id; + String name; + + public boolean hasName() { + return name != null && !name.isEmpty(); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/error/ErrorResponse.java b/src/main/java/ru/yandex/practicum/filmorate/dto/error/ErrorResponse.java index 9d2d3b4..35c4cfa 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dto/error/ErrorResponse.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/error/ErrorResponse.java @@ -1,4 +1,4 @@ package ru.yandex.practicum.filmorate.dto.error; -public record ErrorResponse(String errorName, String description) { +public record ErrorResponse(String error, String description) { } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/film/FilmDto.java b/src/main/java/ru/yandex/practicum/filmorate/dto/film/FilmDto.java index f9ffcc8..2536314 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dto/film/FilmDto.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/film/FilmDto.java @@ -5,6 +5,7 @@ import lombok.Data; import lombok.Singular; import lombok.experimental.FieldDefaults; +import ru.yandex.practicum.filmorate.dto.director.DirectorDto; import ru.yandex.practicum.filmorate.dto.genre.GenreDto; import ru.yandex.practicum.filmorate.dto.mparating.MPARatingDto; @@ -23,4 +24,5 @@ public class FilmDto { MPARatingDto mpa; @Singular List genres; + List directors; } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/film/NewFilmRequest.java b/src/main/java/ru/yandex/practicum/filmorate/dto/film/NewFilmRequest.java index fa9f827..b256069 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dto/film/NewFilmRequest.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/film/NewFilmRequest.java @@ -9,6 +9,7 @@ import lombok.AccessLevel; import lombok.Data; import lombok.experimental.FieldDefaults; +import ru.yandex.practicum.filmorate.dto.director.DirectorDto; import ru.yandex.practicum.filmorate.dto.genre.GenreDto; import ru.yandex.practicum.filmorate.dto.mparating.MPARatingDto; import ru.yandex.practicum.filmorate.validation.AfterDate; @@ -34,4 +35,6 @@ public class NewFilmRequest { MPARatingDto mpa; @JsonSetter(nulls = Nulls.AS_EMPTY) List genres = new ArrayList<>(); + @JsonSetter(nulls = Nulls.AS_EMPTY) + List directors = new ArrayList<>(); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/film/UpdateFilmRequest.java b/src/main/java/ru/yandex/practicum/filmorate/dto/film/UpdateFilmRequest.java index 90a5902..98ed1a7 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dto/film/UpdateFilmRequest.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/film/UpdateFilmRequest.java @@ -6,6 +6,7 @@ import lombok.AccessLevel; import lombok.Data; import lombok.experimental.FieldDefaults; +import ru.yandex.practicum.filmorate.dto.director.DirectorDto; import ru.yandex.practicum.filmorate.dto.genre.GenreDto; import ru.yandex.practicum.filmorate.dto.mparating.MPARatingDto; import ru.yandex.practicum.filmorate.validation.AfterDate; @@ -27,6 +28,7 @@ public class UpdateFilmRequest { Integer duration; MPARatingDto mpa; List genres; + List directors; public boolean hasName() { return name != null && !name.isEmpty(); @@ -51,4 +53,8 @@ public boolean hasGenres() { public boolean hasMpa() { return mpa != null && mpa.id() != null; } + + public boolean hasDirectors() { + return directors != null && !directors.isEmpty(); + } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/mapper/DirectorMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mapper/DirectorMapper.java new file mode 100644 index 0000000..df423f6 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/DirectorMapper.java @@ -0,0 +1,36 @@ +package ru.yandex.practicum.filmorate.mapper; + +import lombok.experimental.UtilityClass; +import ru.yandex.practicum.filmorate.dto.director.DirectorDto; +import ru.yandex.practicum.filmorate.dto.director.NewDirectorRequest; +import ru.yandex.practicum.filmorate.dto.director.UpdateDirectorRequest; +import ru.yandex.practicum.filmorate.model.Director; + +@UtilityClass +public class DirectorMapper { + + public DirectorDto toDirectorDto(Director director) { + return new DirectorDto(director.getId(), director.getName()); + } + + public Director toDirector(NewDirectorRequest directorDto) { + return Director.builder() + .name(directorDto.getName()) + .build(); + } + + public Director toDirector(DirectorDto directorDto) { + return Director.builder() + .name(directorDto.name()) + .id(directorDto.id()) + .build(); + } + + public Director updateDirectorFields(Director director, UpdateDirectorRequest request) { + if (request.hasName()) { + director.setName(request.getName()); + } + + return director; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmMapper.java index 3be6adc..d034f99 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmMapper.java +++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmMapper.java @@ -24,6 +24,10 @@ public FilmDto toFilmDto(Film film) { .stream() .map(genre -> new GenreDto(genre.getId(), genre.getName())) .toList()) + .directors(film.getDirectors() + .stream() + .map(DirectorMapper::toDirectorDto) + .toList()) .build(); } @@ -41,6 +45,10 @@ public Film toFilm(NewFilmRequest request) { .name(genreDto.name()) .build()) .collect(Collectors.toSet())) + .directors(request.getDirectors() + .stream() + .map(DirectorMapper::toDirector) + .collect(Collectors.toSet())) .build(); } @@ -71,6 +79,12 @@ public Film updateFilmFields(Film film, UpdateFilmRequest request) { film.setMpaRating(MPARatingMapper.toMPARating(request.getMpa())); } + if (request.hasDirectors()) { + film.setDirectors(request.getDirectors().stream() + .map(DirectorMapper::toDirector) + .collect(Collectors.toSet())); + } + return film; } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Director.java b/src/main/java/ru/yandex/practicum/filmorate/model/Director.java new file mode 100644 index 0000000..d38e6bf --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Director.java @@ -0,0 +1,14 @@ +package ru.yandex.practicum.filmorate.model; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Data; +import lombok.experimental.FieldDefaults; + +@Data +@Builder +@FieldDefaults(level = AccessLevel.PRIVATE) +public class Director { + Long id; + String name; +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java index 684c52c..ff6b254 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java @@ -22,7 +22,7 @@ public class Film { @Builder.Default Set genres = new HashSet<>(); @Builder.Default - Set usersWhoLiked = new HashSet<>(); + Set directors = new HashSet<>(); public void addGenre(Genre genre) { this.genres.add(genre); diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/director/DirectorRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/director/DirectorRepository.java new file mode 100644 index 0000000..fd09440 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/director/DirectorRepository.java @@ -0,0 +1,18 @@ +package ru.yandex.practicum.filmorate.repository.director; + +import ru.yandex.practicum.filmorate.model.Director; + +import java.util.Collection; +import java.util.Optional; + +public interface DirectorRepository { + Collection findAll(); + + Optional findById(long id); + + Director create(Director director); + + void update(Director director); + + void deleteById(long id); +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/director/DirectorRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/repository/director/DirectorRowMapper.java new file mode 100644 index 0000000..b7f45b5 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/director/DirectorRowMapper.java @@ -0,0 +1,19 @@ +package ru.yandex.practicum.filmorate.repository.director; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.Director; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@Component +public class DirectorRowMapper implements RowMapper { + @Override + public Director mapRow(ResultSet rs, int rowNum) throws SQLException { + return Director.builder() + .id(rs.getLong("director_id")) + .name(rs.getString("name")) + .build(); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/director/JdbcDirectorRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/director/JdbcDirectorRepository.java new file mode 100644 index 0000000..89aa49c --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/director/JdbcDirectorRepository.java @@ -0,0 +1,73 @@ +package ru.yandex.practicum.filmorate.repository.director; + +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.model.Director; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +@Repository +@RequiredArgsConstructor +public class JdbcDirectorRepository implements DirectorRepository { + private final NamedParameterJdbcTemplate jdbc; + private final RowMapper rowMapper; + + @Override + public Collection findAll() { + String selectAllDirectorsSql = "SELECT * FROM directors ORDER BY director_id"; + return jdbc.query(selectAllDirectorsSql, rowMapper); + } + + @Override + public Optional findById(long id) { + String selectDirectorByIdSql = "SELECT * FROM directors WHERE director_id = :id"; + MapSqlParameterSource params = new MapSqlParameterSource() + .addValue("id", id); + + List directors = jdbc.query(selectDirectorByIdSql, params, rowMapper); + + return directors.isEmpty() ? Optional.empty() : Optional.of(directors.getFirst()); + } + + @Override + public Director create(Director director) { + String saveDirectorSql = """ + INSERT INTO directors (name) + VALUES (:name)"""; + GeneratedKeyHolder keyHolder = new GeneratedKeyHolder(); + MapSqlParameterSource params = new MapSqlParameterSource() + .addValue("name", director.getName()); + + jdbc.update(saveDirectorSql, params, keyHolder, new String[]{"director_id"}); + + director.setId(keyHolder.getKeyAs(Long.class)); + return director; + } + + @Override + public void update(Director director) { + String updateDirectorSql = """ + UPDATE directors + SET name = :name + WHERE director_id = :id"""; + MapSqlParameterSource params = new MapSqlParameterSource() + .addValue("name", director.getName()) + .addValue("id", director.getId()); + jdbc.update(updateDirectorSql, params); + } + + @Override + public void deleteById(long id) { + String deleteDirectorByIdSql = "DELETE FROM directors WHERE director_id = :id"; + MapSqlParameterSource params = new MapSqlParameterSource() + .addValue("id", id); + + jdbc.update(deleteDirectorByIdSql, params); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRepository.java index edf5e85..54b895c 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRepository.java @@ -1,5 +1,6 @@ package ru.yandex.practicum.filmorate.repository.film; +import ru.yandex.practicum.filmorate.controller.FilmsSortBy; import ru.yandex.practicum.filmorate.model.Film; import java.util.Collection; @@ -19,4 +20,6 @@ public interface FilmRepository { Collection findTopPopularFilms(int count); Collection findCommonFilms(long userId, long friendId); + + Collection findFilmsOfDirector(long directorId, FilmsSortBy sortFilmsBy); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmResultSetExtractor.java b/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmResultSetExtractor.java deleted file mode 100644 index 7b11ad9..0000000 --- a/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmResultSetExtractor.java +++ /dev/null @@ -1,66 +0,0 @@ -package ru.yandex.practicum.filmorate.repository.film; - -import org.springframework.jdbc.core.ResultSetExtractor; -import org.springframework.stereotype.Component; -import ru.yandex.practicum.filmorate.model.Film; -import ru.yandex.practicum.filmorate.model.Genre; -import ru.yandex.practicum.filmorate.model.MPARating; - -import java.sql.ResultSet; -import java.sql.SQLException; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.List; - -@Component -public class FilmResultSetExtractor implements ResultSetExtractor> { - - @Override - public List extractData(ResultSet rs) throws SQLException { - List films = new ArrayList<>(); - Film currentFilm = null; - Long previousFilmId = null; - - while (rs.next()) { - Long currentFilmId = rs.getLong("film_id"); - - if (!currentFilmId.equals(previousFilmId)) { - if (currentFilm != null) { - films.add(currentFilm); - } - - MPARating mpaRating = MPARating.builder() - .id(rs.getLong("mpa_id")) - .name(rs.getString("mpa_name")) - .build(); - - currentFilm = Film.builder() - .id(currentFilmId) - .name(rs.getString("name")) - .description(rs.getString("description")) - .releaseDate(rs.getObject("release_date", LocalDate.class)) - .duration(rs.getInt("duration_in_minutes")) - .mpaRating(mpaRating) - .build(); - - previousFilmId = currentFilmId; - } - - - - Genre genre = Genre.builder() - .id(rs.getLong("genre_id")) - .name(rs.getString("genre_name")) - .build(); - if (!rs.wasNull()) { - currentFilm.addGenre(genre); - } - } - - if (currentFilm != null) { - films.add(currentFilm); - } - - return films; - } -} diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRowMapper.java index 5707b4a..53d7c9c 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRowMapper.java +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRowMapper.java @@ -2,12 +2,18 @@ import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.Director; import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.model.Genre; import ru.yandex.practicum.filmorate.model.MPARating; import java.sql.ResultSet; import java.sql.SQLException; import java.time.LocalDate; +import java.util.Arrays; +import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; @Component public class FilmRowMapper implements RowMapper { @@ -17,6 +23,39 @@ public Film mapRow(ResultSet rs, int rowNum) throws SQLException { .id(rs.getLong("mpa_id")) .name(rs.getString("mpa_name")) .build(); + + String genresString = rs.getString("genres"); + Set genres; + if (genresString != null && !genresString.isEmpty() && !genresString.startsWith(":")) { + genres = Arrays.stream(genresString.split(";")) + .map(genreIdNameStr -> { + String[] splitIdNameStr = genreIdNameStr.split(":"); + return Genre.builder() + .id(Long.valueOf(splitIdNameStr[0])) + .name(splitIdNameStr[1]) + .build(); + }) + .collect(Collectors.toSet()); + } else { + genres = Collections.emptySet(); + } + + String directorsString = rs.getString("directors"); + Set directors; + if (directorsString != null && !directorsString.isEmpty() && !directorsString.startsWith(":")) { + directors = Arrays.stream(directorsString.split(";")) + .map(directorIdNameStr -> { + String[] splitIdNameStr = directorIdNameStr.split(":"); + return Director.builder() + .id(Long.valueOf(splitIdNameStr[0])) + .name(splitIdNameStr[1]) + .build(); + }) + .collect(Collectors.toSet()); + } else { + directors = Collections.emptySet(); + } + return Film.builder() .id(rs.getLong("film_id")) .name(rs.getString("name")) @@ -24,6 +63,8 @@ public Film mapRow(ResultSet rs, int rowNum) throws SQLException { .releaseDate(rs.getObject("release_date", LocalDate.class)) .duration(rs.getInt("duration_in_minutes")) .mpaRating(mpaRating) + .genres(genres) + .directors(directors) .build(); } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java index 10e91cc..55d4376 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java @@ -1,93 +1,63 @@ package ru.yandex.practicum.filmorate.repository.film; import lombok.RequiredArgsConstructor; -import org.springframework.jdbc.core.ResultSetExtractor; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.jdbc.core.namedparam.SqlParameterSource; import org.springframework.jdbc.support.GeneratedKeyHolder; import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.controller.FilmsSortBy; +import ru.yandex.practicum.filmorate.model.Director; import ru.yandex.practicum.filmorate.model.Film; import ru.yandex.practicum.filmorate.model.Genre; -import java.util.*; -import java.util.function.Function; -import java.util.stream.Collectors; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.Set; @Repository @RequiredArgsConstructor public class JdbcFilmRepository implements FilmRepository { + private static final String BASE_SELECT_SQL = """ + SELECT f.film_id, + f.name, + f.description, + f.release_date, + f.duration_in_minutes, + f.mpa_id, + mr.name AS mpa_name, + GROUP_CONCAT(DISTINCT CONCAT(g.genre_id, ':', g.name) ORDER BY g.genre_id SEPARATOR ';') AS genres, + GROUP_CONCAT(DISTINCT CONCAT(d.director_id, ':', d.name) ORDER BY g.genre_id SEPARATOR ';') AS directors + FROM films f + JOIN mpa_ratings mr ON f.mpa_id = mr.mpa_id + LEFT JOIN film_genres fg ON f.film_id = fg.film_id + LEFT JOIN genres g ON g.genre_id = fg.genre_id + LEFT JOIN film_directors fd on f.film_id = fd.film_id + LEFT JOIN directors d ON fd.director_id = d.director_id + """; private final NamedParameterJdbcOperations jdbc; - private final RowMapper genreRowMapper; private final RowMapper filmRowMapper; - private final ResultSetExtractor> filmResultSetExtractor; @Override public Optional findById(long id) { MapSqlParameterSource params = new MapSqlParameterSource() .addValue("id", id); - String selectFilmByIdSql = """ - SELECT f.film_id, - f.name, - f.description, - f.release_date, - f.duration_in_minutes, - f.mpa_id, - mr.name AS mpa_name, - g.genre_id, - g.name AS genre_name - FROM films f - JOIN mpa_ratings mr ON f.mpa_id = mr.mpa_id - LEFT JOIN film_genres fg ON f.film_id = fg.film_id - LEFT JOIN genres g ON g.genre_id = fg.genre_id - WHERE f.film_id = :id"""; + String selectFilmByIdSql = BASE_SELECT_SQL.concat(""" + WHERE f.film_id = :id + GROUP BY f.film_id"""); - List films = jdbc.query(selectFilmByIdSql, params, filmResultSetExtractor); + List films = jdbc.query(selectFilmByIdSql, params, filmRowMapper); return films.isEmpty() ? Optional.empty() : Optional.of(films.getFirst()); } @Override public Collection findAll() { - String filmsAndMpaSql = """ - SELECT f.film_id, - f.name, - f.description, - f.release_date, - f.duration_in_minutes, - f.mpa_id, - mr.name AS mpa_name - FROM films f - JOIN mpa_ratings mr ON f.mpa_id = mr.mpa_id"""; - Map films = jdbc.query(filmsAndMpaSql, filmRowMapper).stream() - .collect(Collectors.toMap( - Film::getId, - Function.identity() - )); - - String genresSql = "SELECT * FROM genres"; - Map genres = jdbc.query(genresSql, genreRowMapper).stream() - .collect(Collectors.toMap( - Genre::getId, - Function.identity() - )); - - String filmsGenresSql = """ - SELECT film_id, - genre_id - FROM film_genres"""; - List> filmsGenresList = jdbc.query(filmsGenresSql, - (rs, rowNum) -> Map.entry(rs.getLong("film_id"), - rs.getLong("genre_id")) - ); - - for (Map.Entry entry : filmsGenresList) { - films.get(entry.getKey()).addGenre(genres.get(entry.getValue())); - } - - return films.values(); + String findAllSql = BASE_SELECT_SQL.concat("GROUP BY f.film_id"); + return jdbc.query(findAllSql, filmRowMapper); } @Override @@ -103,6 +73,7 @@ INSERT INTO films(name, description, release_date, duration_in_minutes, mpa_id) film.setId(keyHolder.getKeyAs(Long.class)); saveGenres(film.getGenres(), film.getId()); + saveDirectors(film.getDirectors(), film.getId()); return film; } @@ -119,11 +90,17 @@ public void update(Film film) { String deleteFilmGenresSql = """ DELETE FROM film_genres WHERE film_id = :film_id"""; - MapSqlParameterSource deleteParams = new MapSqlParameterSource(); - deleteParams.addValue("film_id", film.getId()); - jdbc.update(deleteFilmGenresSql, deleteParams); - + MapSqlParameterSource deleteFilmGenresParams = new MapSqlParameterSource() + .addValue("film_id", film.getId()); + jdbc.update(deleteFilmGenresSql, deleteFilmGenresParams); saveGenres(film.getGenres(), film.getId()); + + String deleteFilmDirectorsSql = """ + DELETE FROM film_directors WHERE film_id = :film_id"""; + MapSqlParameterSource deleteFilmDirectorsParams = new MapSqlParameterSource() + .addValue("film_id", film.getId()); + jdbc.update(deleteFilmDirectorsSql, deleteFilmDirectorsParams); + saveDirectors(film.getDirectors(), film.getId()); } @Override @@ -137,46 +114,20 @@ public void deleteById(long id) { @Override public Collection findTopPopularFilms(int count) { - String selectTopFilmsSql = """ - SELECT f.film_id, - f.name, - f.description, - f.release_date, - f.duration_in_minutes, - f.mpa_id, - mr.name AS mpa_name, - g.genre_id, - g.name AS genre_name - FROM films f - JOIN mpa_ratings mr ON f.mpa_id = mr.mpa_id - LEFT JOIN film_genres fg ON f.film_id = fg.film_id - LEFT JOIN genres g ON g.genre_id = fg.genre_id + String selectTopFilmsSql = BASE_SELECT_SQL.concat(""" LEFT JOIN likes fl ON f.film_id = fl.film_id GROUP BY f.film_id, f.name, g.genre_id ORDER BY COUNT(fl.user_id) DESC, f.film_id - LIMIT :count"""; + LIMIT :count"""); MapSqlParameterSource params = new MapSqlParameterSource() .addValue("count", count); - return jdbc.query(selectTopFilmsSql, params, filmResultSetExtractor); + return jdbc.query(selectTopFilmsSql, params, filmRowMapper); } @Override public Collection findCommonFilms(long userId, long friendId) { - String selectCommonFilmsSql = """ - SELECT f.film_id, - f.name, - f.description, - f.release_date, - f.duration_in_minutes, - f.mpa_id, - mr.name AS mpa_name, - g.genre_id, - g.name AS genre_name - FROM films f - JOIN mpa_ratings mr ON f.mpa_id = mr.mpa_id - LEFT JOIN film_genres fg ON f.film_id = fg.film_id - LEFT JOIN genres g ON g.genre_id = fg.genre_id + String selectCommonFilmsSql = BASE_SELECT_SQL.concat(""" LEFT JOIN likes fl ON f.film_id = fl.film_id WHERE f.film_id in (SELECT l1.film_id FROM likes l1 @@ -185,14 +136,32 @@ WHERE f.film_id in (SELECT l1.film_id SELECT l2.film_id FROM likes l2 WHERE l2.user_id = :friend_id) - GROUP BY f.film_id, f.name, g.genre_id + GROUP BY f.film_id, g.genre_id ORDER BY COUNT(fl.user_id) DESC, f.film_id - """; + """); MapSqlParameterSource params = new MapSqlParameterSource() .addValue("user_id", userId) .addValue("friend_id", friendId); - return jdbc.query(selectCommonFilmsSql, params, filmResultSetExtractor); + return jdbc.query(selectCommonFilmsSql, params, filmRowMapper); + } + + @Override + public Collection findFilmsOfDirector(long directorId, FilmsSortBy sortFilmsBy) { + String sortBySql = switch (sortFilmsBy) { + case YEAR -> "EXTRACT(YEAR from f.release_date)"; + case LIKES -> "COUNT(fl.user_id) DESC"; + }; + + String selectFilmsOfDirectorSortedSql = BASE_SELECT_SQL.concat(""" + LEFT JOIN likes fl ON f.film_id = fl.film_id + WHERE d.director_id = :director_id + GROUP BY f.film_id, g.genre_id, f.release_date + ORDER BY %s""".formatted(sortBySql) + ); + MapSqlParameterSource params = new MapSqlParameterSource() + .addValue("director_id", directorId); + return jdbc.query(selectFilmsOfDirectorSortedSql, params, filmRowMapper); } private void saveGenres(Set genres, long filmId) { @@ -210,6 +179,20 @@ INSERT INTO film_genres (film_id, genre_id) jdbc.batchUpdate(insertGenresSql, batchParams); } + private void saveDirectors(Set directors, long filmId) { + String insertDirectorsSql = """ + INSERT INTO film_directors (film_id, director_id) + VALUES (:film_id, :director_id)"""; + SqlParameterSource[] batchParams = directors.stream() + .map(Director::getId) + .map(directorId -> new MapSqlParameterSource() + .addValue("film_id", filmId) + .addValue("director_id", directorId)) + .toArray(SqlParameterSource[]::new); + + jdbc.batchUpdate(insertDirectorsSql, batchParams); + } + private MapSqlParameterSource prepareParamMap(Film film) { return new MapSqlParameterSource() .addValue("name", film.getName()) diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/DirectorService.java b/src/main/java/ru/yandex/practicum/filmorate/service/DirectorService.java new file mode 100644 index 0000000..2e541ae --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/DirectorService.java @@ -0,0 +1,19 @@ +package ru.yandex.practicum.filmorate.service; + +import ru.yandex.practicum.filmorate.dto.director.DirectorDto; +import ru.yandex.practicum.filmorate.dto.director.NewDirectorRequest; +import ru.yandex.practicum.filmorate.dto.director.UpdateDirectorRequest; + +import java.util.Collection; + +public interface DirectorService { + Collection findAll(); + + DirectorDto findById(long id); + + DirectorDto create(NewDirectorRequest request); + + DirectorDto update(UpdateDirectorRequest request); + + void deleteById(long id); +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/DirectorServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/DirectorServiceImpl.java new file mode 100644 index 0000000..0708132 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/DirectorServiceImpl.java @@ -0,0 +1,61 @@ +package ru.yandex.practicum.filmorate.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.dto.director.DirectorDto; +import ru.yandex.practicum.filmorate.dto.director.NewDirectorRequest; +import ru.yandex.practicum.filmorate.dto.director.UpdateDirectorRequest; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.mapper.DirectorMapper; +import ru.yandex.practicum.filmorate.model.Director; +import ru.yandex.practicum.filmorate.repository.director.DirectorRepository; + +import java.util.Collection; + +@Service +@Slf4j +@RequiredArgsConstructor +public class DirectorServiceImpl implements DirectorService { + private final DirectorRepository directorRepository; + + @Override + public Collection findAll() { + return directorRepository.findAll() + .stream() + .map(DirectorMapper::toDirectorDto) + .toList(); + } + + @Override + public DirectorDto findById(long id) { + return directorRepository.findById(id) + .map(DirectorMapper::toDirectorDto) + .orElseThrow(NotFoundException.supplier("Director with id %d not found", id)); + } + + @Override + public DirectorDto create(NewDirectorRequest request) { + Director director = DirectorMapper.toDirector(request); + director = directorRepository.create(director); + log.info("Director with id {} created", director.getId()); + return DirectorMapper.toDirectorDto(director); + } + + @Override + public DirectorDto update(UpdateDirectorRequest request) { + Director director = directorRepository.findById(request.getId()).orElseThrow( + NotFoundException.supplier("Director with id %d not found", request.getId()) + ); + director = DirectorMapper.updateDirectorFields(director, request); + directorRepository.update(director); + log.info("Director with id {} updated", director.getId()); + return DirectorMapper.toDirectorDto(director); + } + + @Override + public void deleteById(long id) { + directorRepository.deleteById(id); + log.info("Director with id {} deleted", id); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java index db4c14f..1d253f7 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java @@ -1,5 +1,6 @@ package ru.yandex.practicum.filmorate.service; +import ru.yandex.practicum.filmorate.controller.FilmsSortBy; import ru.yandex.practicum.filmorate.dto.film.FilmDto; import ru.yandex.practicum.filmorate.dto.film.NewFilmRequest; import ru.yandex.practicum.filmorate.dto.film.UpdateFilmRequest; @@ -24,4 +25,6 @@ public interface FilmService { Collection findFilmsWithTopLikes(int count); Collection findCommonFilms(long userId, long friendId); + + Collection findFilmsOfDirector(long directorId, FilmsSortBy sortFilmsBy); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/FilmServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/FilmServiceImpl.java index 4252b35..92a84e1 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmServiceImpl.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmServiceImpl.java @@ -5,6 +5,7 @@ import lombok.experimental.FieldDefaults; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.controller.FilmsSortBy; import ru.yandex.practicum.filmorate.dto.film.FilmDto; import ru.yandex.practicum.filmorate.dto.film.NewFilmRequest; import ru.yandex.practicum.filmorate.dto.film.UpdateFilmRequest; @@ -13,6 +14,7 @@ import ru.yandex.practicum.filmorate.mapper.FilmMapper; import ru.yandex.practicum.filmorate.model.Film; import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.repository.director.DirectorRepository; import ru.yandex.practicum.filmorate.repository.film.FilmRepository; import ru.yandex.practicum.filmorate.repository.genre.GenreRepository; import ru.yandex.practicum.filmorate.repository.like.LikesRepository; @@ -32,6 +34,7 @@ public class FilmServiceImpl implements FilmService { LikesRepository likesRepository; GenreRepository genreRepository; MPARatingRepository mpaRepository; + DirectorRepository directorRepository; @Override public Collection findAll() { @@ -137,6 +140,21 @@ public Collection findCommonFilms(long userId, long friendId) { .toList(); } + @Override + public Collection findFilmsOfDirector(long directorId, FilmsSortBy sortFilmsBy) { + throwIfDirectorNotFound(directorId); + + return filmRepository.findFilmsOfDirector(directorId, sortFilmsBy) + .stream() + .map(FilmMapper::toFilmDto) + .toList(); + } + + private void throwIfDirectorNotFound(long directorId) { + directorRepository.findById(directorId) + .orElseThrow(NotFoundException.supplier("Director with id %d not found", directorId)); + } + private void throwIfUserNotFound(long userId) { userRepository.findById(userId) .orElseThrow(NotFoundException.supplier("User with id %d not found", userId)); diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 08c651a..7515cea 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -19,6 +19,12 @@ CREATE TABLE IF NOT EXISTS mpa_ratings name varchar(20) NOT NULL UNIQUE ); +CREATE TABLE IF NOT EXISTS directors +( + director_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name varchar(120) NOT NULL UNIQUE +); + CREATE TABLE IF NOT EXISTS films ( film_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, @@ -30,6 +36,15 @@ CREATE TABLE IF NOT EXISTS films FOREIGN KEY (mpa_id) REFERENCES mpa_ratings (mpa_id) ); +CREATE TABLE IF NOT EXISTS film_directors +( + film_id BIGINT NOT NULL, + director_id BIGINT NOT NULL, + PRIMARY KEY (film_id, director_id), + FOREIGN KEY (film_id) REFERENCES films (film_id) ON DELETE CASCADE, + FOREIGN KEY (director_id) REFERENCES directors (director_id) ON DELETE CASCADE +); + CREATE TABLE IF NOT EXISTS film_genres ( film_id BIGINT NOT NULL, diff --git a/src/test/java/ru/yandex/practicum/filmorate/repository/JdbcFilmRepositoryTest.java b/src/test/java/ru/yandex/practicum/filmorate/repository/JdbcFilmRepositoryTest.java index 4f6b5dd..d4ade53 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/repository/JdbcFilmRepositoryTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/repository/JdbcFilmRepositoryTest.java @@ -7,10 +7,8 @@ import org.springframework.context.annotation.Import; import ru.yandex.practicum.filmorate.model.Film; import ru.yandex.practicum.filmorate.model.MPARating; -import ru.yandex.practicum.filmorate.repository.film.FilmResultSetExtractor; import ru.yandex.practicum.filmorate.repository.film.FilmRowMapper; import ru.yandex.practicum.filmorate.repository.film.JdbcFilmRepository; -import ru.yandex.practicum.filmorate.repository.genre.GenreRowMapper; import ru.yandex.practicum.filmorate.util.TestFilmUtils; import java.time.LocalDate; @@ -20,7 +18,7 @@ import static org.assertj.core.api.Assertions.assertThat; @JdbcTest -@Import({JdbcFilmRepository.class, FilmRowMapper.class, GenreRowMapper.class, FilmResultSetExtractor.class}) +@Import({JdbcFilmRepository.class, FilmRowMapper.class}) public class JdbcFilmRepositoryTest { @Autowired private JdbcFilmRepository filmRepository; From 03173092eb65646177962935715b77ee643d7800 Mon Sep 17 00:00:00 2001 From: Nikolay Aleksandrov Date: Thu, 16 Oct 2025 14:15:52 +0300 Subject: [PATCH 04/13] Add most populars (#16) * feat: added search by genres and years * fix: changed type params genreId and year to wrappers --- .../practicum/filmorate/controller/FilmController.java | 8 +++++--- .../filmorate/repository/film/FilmRepository.java | 2 +- .../filmorate/repository/film/JdbcFilmRepository.java | 10 +++++++--- .../practicum/filmorate/service/FilmService.java | 2 +- .../practicum/filmorate/service/FilmServiceImpl.java | 4 ++-- 5 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java index 956c125..3d09f66 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -77,9 +77,11 @@ public Collection findCommonFilms(@RequestParam(name = "userId") @Posit } @GetMapping("/popular") - public Collection findPopular(@RequestParam(defaultValue = "10") @Positive int count) { - log.trace("Find popular film requested with count: {}", count); - return filmService.findFilmsWithTopLikes(count); + public Collection findPopular(@RequestParam(defaultValue = "10") @Positive int count, + @RequestParam(required = false) @Positive Integer genreId, + @RequestParam(required = false) @Positive Integer year) { + log.trace("Find popular film requested with count: {} by genre: {} for year: {}", count, genreId, year); + return filmService.findFilmsWithTopLikes(count, genreId, year); } @GetMapping("/director/{directorId}") diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRepository.java index 54b895c..9ec2945 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRepository.java @@ -17,7 +17,7 @@ public interface FilmRepository { void deleteById(long id); - Collection findTopPopularFilms(int count); + Collection findTopPopularFilms(int count, Integer genreId, Integer year); Collection findCommonFilms(long userId, long friendId); diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java index 55d4376..c307b2b 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java @@ -113,14 +113,18 @@ public void deleteById(long id) { } @Override - public Collection findTopPopularFilms(int count) { + public Collection findTopPopularFilms(int count, Integer genreId, Integer year) { String selectTopFilmsSql = BASE_SELECT_SQL.concat(""" LEFT JOIN likes fl ON f.film_id = fl.film_id - GROUP BY f.film_id, f.name, g.genre_id + WHERE (g.genre_id = :genreId OR :genreId IS NULL) + AND (YEAR(f.release_date) =:year OR :year IS NULL) + GROUP BY f.film_id ORDER BY COUNT(fl.user_id) DESC, f.film_id LIMIT :count"""); MapSqlParameterSource params = new MapSqlParameterSource() - .addValue("count", count); + .addValue("count", count) + .addValue("genreId", genreId) + .addValue("year", year); return jdbc.query(selectTopFilmsSql, params, filmRowMapper); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java index 1d253f7..2ca937f 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java @@ -22,7 +22,7 @@ public interface FilmService { void removeLike(long filmId, long userId); - Collection findFilmsWithTopLikes(int count); + Collection findFilmsWithTopLikes(int count, Integer genreId, Integer year); Collection findCommonFilms(long userId, long friendId); diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/FilmServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/FilmServiceImpl.java index 92a84e1..8ddfd81 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmServiceImpl.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmServiceImpl.java @@ -122,8 +122,8 @@ public void removeLike(long filmId, long userId) { } @Override - public Collection findFilmsWithTopLikes(int count) { - return filmRepository.findTopPopularFilms(count) + public Collection findFilmsWithTopLikes(int count, Integer genreId, Integer year) { + return filmRepository.findTopPopularFilms(count, genreId, year) .stream() .map(FilmMapper::toFilmDto) .toList(); From 8e07d62b94f88bf0c36f0fb0cc4645a68de1567a Mon Sep 17 00:00:00 2001 From: basementdoor <119163446+basementdoor@users.noreply.github.com> Date: Sat, 18 Oct 2025 09:09:41 +0300 Subject: [PATCH 05/13] add film recommendations (#14) * up to date to changes * fix conflicts * rebase changes * update methods for new code --------- Co-authored-by: Egor Ilyin --- .../filmorate/controller/UserController.java | 9 +++++ .../repository/film/FilmRepository.java | 2 + .../repository/film/JdbcFilmRepository.java | 18 +++++++++ .../repository/user/JdbcUserRepository.java | 25 ++++++++++++ .../repository/user/UserRepository.java | 2 + .../service/RecommendationService.java | 9 +++++ .../service/RecommendationServiceImpl.java | 40 +++++++++++++++++++ 7 files changed, 105 insertions(+) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/service/RecommendationService.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/service/RecommendationServiceImpl.java diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java index 4e8063f..6133fb5 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java @@ -7,9 +7,11 @@ import org.springframework.http.HttpStatus; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.dto.film.FilmDto; import ru.yandex.practicum.filmorate.dto.user.NewUserRequest; import ru.yandex.practicum.filmorate.dto.user.UpdateUserRequest; import ru.yandex.practicum.filmorate.dto.user.UserDto; +import ru.yandex.practicum.filmorate.service.RecommendationService; import ru.yandex.practicum.filmorate.service.UserService; import java.util.Collection; @@ -22,6 +24,7 @@ @Validated public class UserController { private final UserService userService; + private final RecommendationService recommendationService; @GetMapping public Collection findAll() { @@ -82,4 +85,10 @@ public Collection findCommonFriends(@PathVariable @Positive long id, log.trace("Find common friends requested for id {}, other id: {}", id, otherId); return userService.findAllCommonFriends(id, otherId); } + + @GetMapping("/{id}/recommendations") + public Collection findFilmRecommendations(@PathVariable @Positive long id) { + log.trace("Find film recommendations requested for id {}", id); + return recommendationService.findFilmRecommendations(id); + } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRepository.java index 9ec2945..46326de 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRepository.java @@ -22,4 +22,6 @@ public interface FilmRepository { Collection findCommonFilms(long userId, long friendId); Collection findFilmsOfDirector(long directorId, FilmsSortBy sortFilmsBy); + + Collection findFilmRecommendations(long userId, long similarUserId); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java index c307b2b..2933fa0 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java @@ -168,6 +168,24 @@ public Collection findFilmsOfDirector(long directorId, FilmsSortBy sortFil return jdbc.query(selectFilmsOfDirectorSortedSql, params, filmRowMapper); } + @Override + public Collection findFilmRecommendations(long userId, long similarUserId) { + String sqlRecommendations = BASE_SELECT_SQL.concat(""" + LEFT JOIN likes l ON f.film_id = l.film_id + WHERE l.user_id = :similarUserId + AND f.film_id NOT IN ( + SELECT film_id FROM likes WHERE user_id = :userId + ) + GROUP BY f.film_id + """); + + MapSqlParameterSource params = new MapSqlParameterSource() + .addValue("similarUserId", similarUserId) + .addValue("userId", userId); + + return jdbc.query(sqlRecommendations, params, filmRowMapper); + } + private void saveGenres(Set genres, long filmId) { String insertGenresSql = """ INSERT INTO film_genres (film_id, genre_id) diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/user/JdbcUserRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/user/JdbcUserRepository.java index 78799fb..6b6ec95 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/repository/user/JdbcUserRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/user/JdbcUserRepository.java @@ -114,4 +114,29 @@ WHERE u.user_id IN (SELECT user_id2 return jdbc.query(selectCommonFriendsSql, params, rowMapper); } + + public Optional findSimilarFilmTasteUser(long userId) { + MapSqlParameterSource params = new MapSqlParameterSource() + .addValue("userId", userId); + + String sql = """ + SELECT l2.user_id AS other_user, + COUNT(*) AS common_like_count + FROM likes l1 + JOIN likes l2 ON l1.film_id = l2.film_id + WHERE l1.user_id = :userId + AND l2.user_id != :userId + GROUP BY l2.user_id + ORDER BY common_like_count DESC + LIMIT 1"""; + + Long result = jdbc.query(sql, params, rs -> { + if (rs.next()) { + return rs.getObject("other_user", Long.class); + } + return null; + }); + + return Optional.ofNullable(result); + } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/user/UserRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/user/UserRepository.java index e15ae2e..92f37c0 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/repository/user/UserRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/user/UserRepository.java @@ -19,4 +19,6 @@ public interface UserRepository { Collection findAllFriends(long userId); Collection findAllCommonFriends(long userId1, long userId2); + + Optional findSimilarFilmTasteUser(long userId); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/RecommendationService.java b/src/main/java/ru/yandex/practicum/filmorate/service/RecommendationService.java new file mode 100644 index 0000000..624624f --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/RecommendationService.java @@ -0,0 +1,9 @@ +package ru.yandex.practicum.filmorate.service; + +import ru.yandex.practicum.filmorate.dto.film.FilmDto; + +import java.util.Collection; + +public interface RecommendationService { + Collection findFilmRecommendations(long userId); +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/RecommendationServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/RecommendationServiceImpl.java new file mode 100644 index 0000000..c5fa259 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/RecommendationServiceImpl.java @@ -0,0 +1,40 @@ +package ru.yandex.practicum.filmorate.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.dto.film.FilmDto; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.mapper.FilmMapper; +import ru.yandex.practicum.filmorate.repository.film.FilmRepository; +import ru.yandex.practicum.filmorate.repository.user.UserRepository; + +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; + +@Service +@Slf4j +@RequiredArgsConstructor +public class RecommendationServiceImpl implements RecommendationService { + private final UserRepository userRepository; + private final FilmRepository filmRepository; + + @Override + public Collection findFilmRecommendations(long userId) { + userRepository.findById(userId) + .orElseThrow(NotFoundException.supplier("User with id %d not found", userId)); + + Optional similarUserOpt = userRepository.findSimilarFilmTasteUser(userId); + + if (similarUserOpt.isEmpty()) { + return Collections.emptyList(); + } + + long similarUserId = similarUserOpt.get(); + return filmRepository.findFilmRecommendations(userId, similarUserId) + .stream() + .map(FilmMapper::toFilmDto) + .toList(); + } +} From ef514172d727bec1971dba1a6c487bdb6685406c Mon Sep 17 00:00:00 2001 From: Nikolay Aleksandrov Date: Sat, 18 Oct 2025 11:11:10 +0300 Subject: [PATCH 06/13] feat: added reviews (#21) * feat: added reviews * fix: checkstyle * fix: refactored ReviewServiceImpl, added responseStatus --- .../controller/ReviewController.java | 84 +++++++++ .../dto/review/NewReviewRequest.java | 25 +++ .../filmorate/dto/review/ReviewDto.java | 19 ++ .../dto/review/UpdateReviewRequest.java | 29 +++ .../filmorate/mapper/ReviewMapper.java | 40 ++++ .../practicum/filmorate/model/Review.java | 18 ++ .../review/JdbcReviewRepository.java | 174 ++++++++++++++++++ .../repository/review/ReviewRepository.java | 26 +++ .../repository/review/ReviewRowMapper.java | 23 +++ .../filmorate/service/ReviewService.java | 28 +++ .../filmorate/service/ReviewServiceImpl.java | 109 +++++++++++ src/main/resources/application.properties | 2 +- src/main/resources/schema.sql | 37 +++- 13 files changed, 609 insertions(+), 5 deletions(-) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/controller/ReviewController.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dto/review/NewReviewRequest.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dto/review/ReviewDto.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dto/review/UpdateReviewRequest.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/mapper/ReviewMapper.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/model/Review.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/repository/review/JdbcReviewRepository.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/repository/review/ReviewRepository.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/repository/review/ReviewRowMapper.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/service/ReviewService.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/service/ReviewServiceImpl.java diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/ReviewController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/ReviewController.java new file mode 100644 index 0000000..a255845 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/ReviewController.java @@ -0,0 +1,84 @@ +package ru.yandex.practicum.filmorate.controller; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.dto.review.NewReviewRequest; +import ru.yandex.practicum.filmorate.dto.review.ReviewDto; +import ru.yandex.practicum.filmorate.dto.review.UpdateReviewRequest; +import ru.yandex.practicum.filmorate.service.ReviewService; + +import java.util.Collection; + +@RestController +@RequestMapping("/reviews") +@Slf4j +@Validated +@RequiredArgsConstructor +public class ReviewController { + private final ReviewService reviewService; + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public ReviewDto create(@Valid @RequestBody NewReviewRequest request) { + log.trace("Create review request: {}", request); + return reviewService.create(request); + } + + @PutMapping + public ReviewDto update(@Valid @RequestBody UpdateReviewRequest request) { + log.trace("Update review request: {}", request); + return reviewService.update(request); + } + + @DeleteMapping("/{id}") + public void delete(@PathVariable @Positive long id) { + log.trace("Delete review request by reviewId: {}", id); + reviewService.delete(id); + } + + @GetMapping("/{id}") + public ReviewDto findById(@PathVariable @Positive long id) { + log.trace("Find review by reviewId: {}", id); + return reviewService.findById(id); + } + + @GetMapping + public Collection findAllByFilm(@RequestParam(required = false) @Positive Long filmId, + @RequestParam(defaultValue = "10") @Positive long count) { + log.trace("Find all reviews by film Id: {}, count: {}", filmId, count); + return reviewService.findAllByFilm(filmId, count); + } + + @PutMapping("/{id}/like/{userId}") + public void setLike(@PathVariable @Positive long id, + @PathVariable @Positive long userId) { + log.trace("Set review like by reviewId: {}, from userId: {}", id, userId); + reviewService.setLike(id, userId); + } + + @PutMapping("/{id}/dislike/{userId}") + public void setDislike(@PathVariable @Positive long id, + @PathVariable @Positive long userId) { + log.trace("Set review dislike by reviewId: {}, from userId: {}", id, userId); + reviewService.setDislike(id, userId); + } + + @DeleteMapping("/{id}/like/{userId}") + public void removeLike(@PathVariable @Positive long id, + @PathVariable @Positive long userId) { + log.trace("Remove review like by reviewId: {}, from userId: {}", id, userId); + reviewService.removeLike(id, userId); + } + + @DeleteMapping("/{id}/dislike/{userId}") + public void removeDislike(@PathVariable @Positive long id, + @PathVariable @Positive long userId) { + log.trace("Remove review dislike by reviewId: {}, from userId: {}", id, userId); + reviewService.removeDislike(id, userId); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/review/NewReviewRequest.java b/src/main/java/ru/yandex/practicum/filmorate/dto/review/NewReviewRequest.java new file mode 100644 index 0000000..5115277 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/review/NewReviewRequest.java @@ -0,0 +1,25 @@ +package ru.yandex.practicum.filmorate.dto.review; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.AccessLevel; +import lombok.Data; +import lombok.experimental.FieldDefaults; + +@Data +@FieldDefaults(level = AccessLevel.PRIVATE) +public class NewReviewRequest { + @NotBlank(message = "Text must not be null or empty") + @Size(message = "Text length must be less than 500", max = 500) + String content; + + @NotNull(message = "Rating must not be null") + Boolean isPositive; + + @NotNull(message = "User ID must be present") + Long userId; + + @NotNull(message = "Film ID must be present") + Long filmId; +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/review/ReviewDto.java b/src/main/java/ru/yandex/practicum/filmorate/dto/review/ReviewDto.java new file mode 100644 index 0000000..0c61df4 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/review/ReviewDto.java @@ -0,0 +1,19 @@ +package ru.yandex.practicum.filmorate.dto.review; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Data; +import lombok.experimental.FieldDefaults; + +@Data +@Builder +@FieldDefaults(level = AccessLevel.PRIVATE) +public class ReviewDto { + Long reviewId; + String content; + Boolean isPositive; + Long userId; + Long filmId; + Long useful; +} + diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/review/UpdateReviewRequest.java b/src/main/java/ru/yandex/practicum/filmorate/dto/review/UpdateReviewRequest.java new file mode 100644 index 0000000..e4e0ada --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/review/UpdateReviewRequest.java @@ -0,0 +1,29 @@ +package ru.yandex.practicum.filmorate.dto.review; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Data; +import lombok.experimental.FieldDefaults; + +@Data +@Builder +@FieldDefaults(level = AccessLevel.PRIVATE) +public class UpdateReviewRequest { + @NotNull + Long reviewId; + + @Size(message = "Text length must be less than 500", max = 500) + String content; + + Boolean isPositive; + + public boolean hasContent() { + return this.content != null && !this.content.isEmpty(); + } + + public boolean hasIsPositive() { + return this.isPositive != null; + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/mapper/ReviewMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mapper/ReviewMapper.java new file mode 100644 index 0000000..c51ba37 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/ReviewMapper.java @@ -0,0 +1,40 @@ +package ru.yandex.practicum.filmorate.mapper; + +import lombok.experimental.UtilityClass; +import ru.yandex.practicum.filmorate.dto.review.NewReviewRequest; +import ru.yandex.practicum.filmorate.dto.review.ReviewDto; +import ru.yandex.practicum.filmorate.dto.review.UpdateReviewRequest; +import ru.yandex.practicum.filmorate.model.Review; + +@UtilityClass +public class ReviewMapper { + public ReviewDto toDto(Review review) { + return ReviewDto.builder() + .reviewId(review.getId()) + .content(review.getContent()) + .isPositive(review.getIsPositive()) + .userId(review.getUserId()) + .filmId(review.getFilmId()) + .useful(review.getUsefulRating()) + .build(); + } + + public Review toReview(NewReviewRequest request) { + return Review.builder() + .content(request.getContent()) + .isPositive(request.getIsPositive()) + .userId(request.getUserId()) + .filmId(request.getFilmId()) + .build(); + } + + public Review updateReviewFields(Review review, UpdateReviewRequest request) { + if (request.hasContent()) { + review.setContent(request.getContent()); + } + if (request.hasIsPositive()) { + review.setIsPositive(request.getIsPositive()); + } + return review; + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Review.java b/src/main/java/ru/yandex/practicum/filmorate/model/Review.java new file mode 100644 index 0000000..983b06d --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Review.java @@ -0,0 +1,18 @@ +package ru.yandex.practicum.filmorate.model; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Data; +import lombok.experimental.FieldDefaults; + +@Data +@Builder +@FieldDefaults(level = AccessLevel.PRIVATE) +public class Review { + Long id; + String content; + Boolean isPositive; + Long userId; + Long filmId; + Long usefulRating; +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/review/JdbcReviewRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/review/JdbcReviewRepository.java new file mode 100644 index 0000000..91e2aaa --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/review/JdbcReviewRepository.java @@ -0,0 +1,174 @@ +package ru.yandex.practicum.filmorate.repository.review; + +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.model.Review; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +@Repository +@RequiredArgsConstructor +public class JdbcReviewRepository implements ReviewRepository { + private static final String BASE_SELECT_REVIEW_SQL = """ + SELECT r.review_id, + r.content, + r.is_positive, + fr.film_id, + fr.user_id, + r.useful_rating + FROM reviews r + JOIN film_reviews fr ON r.review_id = fr.review_id + """; + private static final int UP_UR = 1; + private static final int DOUBLE_UP_UR = 2; + private static final int DROP_UR = -1; + private static final int DOUBLE_DROP_UR = -2; + private final NamedParameterJdbcOperations jdbc; + private final RowMapper mapper; + + + @Override + public Review save(Review review) { + GeneratedKeyHolder keyHolder = new GeneratedKeyHolder(); + MapSqlParameterSource params = getBaseParams(review); + jdbc.update(""" + INSERT INTO reviews(content, is_positive) + VALUES (:content, :is_Positive) + """, params, keyHolder, new String[]{"review_id"}); + params.addValue("film_id", review.getFilmId()); + params.addValue("user_id", review.getUserId()); + review.setId(keyHolder.getKeyAs(Long.class)); + params.addValue("review_id", review.getId()); + jdbc.update(""" + INSERT INTO film_reviews(film_id, user_id, review_id) + VALUES (:film_id, :user_id, :review_id) + """, params); + review.setUsefulRating(0L); + return review; + } + + @Override + public void update(Review review) { + MapSqlParameterSource params = getBaseParams(review) + .addValue("review_id", review.getId()); + jdbc.update(""" + UPDATE reviews + SET content = :content, is_positive = :is_Positive + WHERE review_id = :review_id + """, params); + } + + @Override + public void delete(long id) { + MapSqlParameterSource params = new MapSqlParameterSource("review_id", id); + jdbc.update(""" + DELETE FROM reviews + WHERE review_id = :review_id + """, params); + } + + @Override + public Optional findById(long id) { + MapSqlParameterSource params = new MapSqlParameterSource("review_id", id); + List list = jdbc.query(BASE_SELECT_REVIEW_SQL.concat(""" + WHERE r.review_id = :review_id + """), params, mapper); + return list.isEmpty() ? Optional.empty() : Optional.of(list.getFirst()); + } + + @Override + public Collection findAllByFilm(Long filmId, long count) { + MapSqlParameterSource params = new MapSqlParameterSource("film_id", filmId) + .addValue("count", count); + return jdbc.query(BASE_SELECT_REVIEW_SQL.concat(""" + WHERE fr.film_id = COALESCE(:film_id, fr.film_id) + LIMIT :count + """), params, mapper); + } + + @Override + public void setLike(long id, long userId) { + MapSqlParameterSource params = new MapSqlParameterSource("review_id", id) + .addValue("user_id", userId); + Boolean isPositiveYet = isPositive(params); + jdbc.update(""" + MERGE INTO review_likes (review_id, user_id, is_positive) + KEY (review_id, user_id) + VALUES (:review_id, :user_id, TRUE) + """, params); + if (isPositiveYet == null) { + changeUsefulRating(params, UP_UR); + } else if (isPositiveYet) { + changeUsefulRating(params, DOUBLE_UP_UR); + } + } + + @Override + public void setDislike(long id, long userId) { + MapSqlParameterSource params = new MapSqlParameterSource("review_id", id) + .addValue("user_id", userId); + Boolean isPositiveYet = isPositive(params); + jdbc.update(""" + MERGE INTO review_likes (review_id, user_id, is_positive) + KEY (review_id, user_id) + VALUES (:review_id, :user_id, FALSE) + """, params); + if (isPositiveYet == null) { + changeUsefulRating(params, DROP_UR); + } else if (isPositiveYet) { + changeUsefulRating(params, DOUBLE_DROP_UR); + } + } + + @Override + public void removeLike(long id, long userId) { + MapSqlParameterSource params = new MapSqlParameterSource("review_id", id) + .addValue("user_id", userId); + jdbc.update(""" + DELETE FROM review_likes + WHERE review_id = :review_id AND user_id = :user_id + """, params); + changeUsefulRating(params, DROP_UR); + } + + @Override + public void removeDislike(long id, long userId) { + MapSqlParameterSource params = new MapSqlParameterSource("review_id", id) + .addValue("user_id", userId); + jdbc.update(""" + DELETE FROM review_likes + WHERE review_id = :review_id AND user_id = :user_id + """, params); + changeUsefulRating(params, UP_UR); + } + + private MapSqlParameterSource getBaseParams(Review review) { + return new MapSqlParameterSource() + .addValue("content", review.getContent()) + .addValue("is_Positive", review.getIsPositive()); + } + + private Boolean isPositive(MapSqlParameterSource params) { + List list = jdbc.query(""" + SELECT is_positive + FROM review_likes + WHERE review_id = :review_id AND user_id = :user_id + """, params, (rs, rowNum) -> rs.getBoolean("is_positive")); + return list.isEmpty() ? null : list.getFirst(); + } + + private void changeUsefulRating(MapSqlParameterSource params, int points) { + params.addValue("points", points); + jdbc.update(""" + UPDATE reviews + SET useful_rating = useful_rating + :points + WHERE review_id = :review_id + """, params); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/review/ReviewRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/review/ReviewRepository.java new file mode 100644 index 0000000..e1bca95 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/review/ReviewRepository.java @@ -0,0 +1,26 @@ +package ru.yandex.practicum.filmorate.repository.review; + +import ru.yandex.practicum.filmorate.model.Review; + +import java.util.Collection; +import java.util.Optional; + +public interface ReviewRepository { + Review save(Review review); + + void update(Review review); + + void delete(long id); + + Optional findById(long id); + + Collection findAllByFilm(Long filmId, long count); + + void setLike(long id, long userId); + + void setDislike(long id, long userId); + + void removeLike(long id, long userId); + + void removeDislike(long id, long userId); +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/review/ReviewRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/repository/review/ReviewRowMapper.java new file mode 100644 index 0000000..870c76d --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/review/ReviewRowMapper.java @@ -0,0 +1,23 @@ +package ru.yandex.practicum.filmorate.repository.review; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.Review; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@Component +public class ReviewRowMapper implements RowMapper { + @Override + public Review mapRow(ResultSet rs, int rowNum) throws SQLException { + return Review.builder() + .id(rs.getLong("review_id")) + .content(rs.getString("content")) + .isPositive(rs.getBoolean("is_positive")) + .filmId(rs.getLong("film_id")) + .userId(rs.getLong("user_id")) + .usefulRating(rs.getLong("useful_rating")) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/ReviewService.java b/src/main/java/ru/yandex/practicum/filmorate/service/ReviewService.java new file mode 100644 index 0000000..6333f9d --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/ReviewService.java @@ -0,0 +1,28 @@ +package ru.yandex.practicum.filmorate.service; + +import ru.yandex.practicum.filmorate.dto.review.NewReviewRequest; +import ru.yandex.practicum.filmorate.dto.review.ReviewDto; +import ru.yandex.practicum.filmorate.dto.review.UpdateReviewRequest; + +import java.util.Collection; + +public interface ReviewService { + + ReviewDto create(NewReviewRequest request); + + ReviewDto update(UpdateReviewRequest request); + + void delete(long id); + + ReviewDto findById(long id); + + Collection findAllByFilm(Long filmId, long count); + + void setLike(long id, long userId); + + void setDislike(long id, long userId); + + void removeLike(long id, long userId); + + void removeDislike(long id, long userId); +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/ReviewServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/ReviewServiceImpl.java new file mode 100644 index 0000000..29c0763 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/ReviewServiceImpl.java @@ -0,0 +1,109 @@ +package ru.yandex.practicum.filmorate.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.dto.review.*; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.mapper.ReviewMapper; +import ru.yandex.practicum.filmorate.model.Review; +import ru.yandex.practicum.filmorate.repository.film.FilmRepository; +import ru.yandex.practicum.filmorate.repository.review.ReviewRepository; +import ru.yandex.practicum.filmorate.repository.user.UserRepository; + +import java.util.Collection; + +@Service +@Slf4j +@RequiredArgsConstructor +public class ReviewServiceImpl implements ReviewService { + private final ReviewRepository reviewRepository; + private final UserRepository userRepository; + private final FilmRepository filmRepository; + + @Override + public ReviewDto create(NewReviewRequest request) { + userExistenceCheck(request.getUserId()); + filmExistenceCheck(request.getFilmId()); + Review review = ReviewMapper.toReview(request); + review = reviewRepository.save(review); + log.info("Review with reviewId {} has been created", review.getId()); + return ReviewMapper.toDto(review); + } + + @Override + public ReviewDto update(UpdateReviewRequest request) { + Review review = reviewExistenceCheck(request.getReviewId()); + review = ReviewMapper.updateReviewFields(review, request); + log.info("Review with reviewId {} has been updated", review.getId()); + reviewRepository.update(review); + return ReviewMapper.toDto(review); + } + + @Override + public void delete(long id) { + reviewExistenceCheck(id); + log.info("Review with reviewId {} has been deleted", id); + reviewRepository.delete(id); + } + + @Override + public ReviewDto findById(long id) { + return ReviewMapper.toDto(reviewExistenceCheck(id)); + } + + @Override + public Collection findAllByFilm(Long filmId, long count) { + filmExistenceCheck(filmId); + return reviewRepository.findAllByFilm(filmId, count).stream() + .map(ReviewMapper::toDto) + .toList(); + } + + @Override + public void setLike(long id, long userId) { + reviewExistenceCheck(id); + userExistenceCheck(userId); + log.info("Like to review {} has been added by user {}", id, userId); + reviewRepository.setLike(id, userId); + } + + @Override + public void setDislike(long id, long userId) { + reviewExistenceCheck(id); + userExistenceCheck(userId); + log.info("Dislike to review {} has been added by user {}", id, userId); + reviewRepository.setDislike(id, userId); + } + + @Override + public void removeLike(long id, long userId) { + reviewExistenceCheck(id); + userExistenceCheck(userId); + log.info("Like to review {} has been removed by user {}", id, userId); + reviewRepository.removeLike(id, userId); + } + + @Override + public void removeDislike(long id, long userId) { + reviewExistenceCheck(id); + userExistenceCheck(userId); + log.info("Dislike to review {} has been removed by user {}", id, userId); + reviewRepository.removeDislike(id, userId); + } + + public Review reviewExistenceCheck(long id) { + return reviewRepository.findById(id) + .orElseThrow(NotFoundException.supplier("Review with reviewId %d not found", id)); + } + + public void userExistenceCheck(long id) { + userRepository.findById(id) + .orElseThrow(NotFoundException.supplier("User with userId %d not found", id)); + } + + public void filmExistenceCheck(long id) { + filmRepository.findById(id) + .orElseThrow(NotFoundException.supplier("Film with filmId %d not found", id)); + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index a840ff7..ac5b1c6 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,4 +1,4 @@ -logging.level.org.zalando.logbook=Trace +logging.level.org.zalando.logbook=INFO spring.sql.init.mode=ALWAYS diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 7515cea..0b7b1a0 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -31,8 +31,8 @@ CREATE TABLE IF NOT EXISTS films name varchar(120) NOT NULL, description varchar(200) NOT NULL, release_date date NOT NULL CHECK (release_date >= '1895-12-28'), - duration_in_minutes BIGINT NOT NULL CHECK (duration_in_minutes > 0), - mpa_id BIGINT NOT NULL, + duration_in_minutes BIGINT NOT NULL CHECK (duration_in_minutes > 0), + mpa_id BIGINT NOT NULL, FOREIGN KEY (mpa_id) REFERENCES mpa_ratings (mpa_id) ); @@ -65,11 +65,40 @@ CREATE TABLE IF NOT EXISTS likes CREATE TABLE IF NOT EXISTS friendships ( - user_id1 BIGINT NOT NULL, - user_id2 BIGINT NOT NULL, + user_id1 BIGINT NOT NULL, + user_id2 BIGINT NOT NULL, status ENUM ('pending', 'accepted') NOT NULL DEFAULT 'pending', PRIMARY KEY (user_id1, user_id2), FOREIGN KEY (user_id1) REFERENCES users (user_id) ON DELETE CASCADE, FOREIGN KEY (user_id2) REFERENCES users (user_id) ON DELETE CASCADE, CHECK (user_id1 <> user_id2) +); + +CREATE TABLE IF NOT EXISTS reviews +( + review_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + content VARCHAR(500) NOT NULL, + is_positive BOOL NOT NULL, + useful_rating BIGINT NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS film_reviews +( + review_id BIGINT NOT NULL UNIQUE, + film_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + PRIMARY KEY (film_id, user_id), + FOREIGN KEY (film_id) REFERENCES films (film_id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users (user_id) ON DELETE CASCADE, + FOREIGN KEY (review_id) REFERENCES reviews (review_id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS review_likes +( + review_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + is_positive BOOL NOT NULL, + PRIMARY KEY (review_id, user_id), + FOREIGN KEY (review_id) REFERENCES reviews (review_id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users (user_id) ON DELETE CASCADE ); \ No newline at end of file From aa6826f891e490e0457ab0f403bcfe316d320806 Mon Sep 17 00:00:00 2001 From: LightInTheFire <109972737+LightInTheFire@users.noreply.github.com> Date: Sat, 18 Oct 2025 16:37:41 +0300 Subject: [PATCH 07/13] Changed directories and small bugfixes for tests (#22) * fix: added subdirectories to service package fixed tests * fix: distinct genres and directors --- .../controller/DirectorController.java | 2 +- .../filmorate/controller/FilmController.java | 10 ++--- .../filmorate/controller/GenreController.java | 2 +- .../controller/MPARatingController.java | 2 +- .../controller/ReviewController.java | 4 +- .../filmorate/controller/UserController.java | 8 ++-- .../filmorate/dto/film/UpdateFilmRequest.java | 11 ++++- .../filmorate/mapper/FilmMapper.java | 20 ++++----- .../practicum/filmorate/model/Film.java | 12 ++---- .../director/DirectorRepository.java | 3 ++ .../director/JdbcDirectorRepository.java | 8 ++++ .../repository/film/FilmRowMapper.java | 15 ++++--- .../repository/film/JdbcFilmRepository.java | 31 +++++++------- .../review/JdbcReviewRepository.java | 2 +- .../{ => director}/DirectorService.java | 2 +- .../{ => director}/DirectorServiceImpl.java | 2 +- .../service/{ => film}/FilmService.java | 2 +- .../service/{ => film}/FilmServiceImpl.java | 29 ++++++++++++- .../service/{ => genre}/GenreService.java | 2 +- .../service/{ => genre}/GenreServiceImpl.java | 2 +- .../{ => mparating}/MPARatingService.java | 2 +- .../{ => mparating}/MPARatingServiceImpl.java | 2 +- .../RecommendationService.java | 2 +- .../RecommendationServiceImpl.java | 2 +- .../service/{ => review}/ReviewService.java | 4 +- .../{ => review}/ReviewServiceImpl.java | 41 ++++++++++--------- .../service/{ => user}/UserService.java | 2 +- .../service/{ => user}/UserServiceImpl.java | 4 +- 28 files changed, 134 insertions(+), 94 deletions(-) rename src/main/java/ru/yandex/practicum/filmorate/service/{ => director}/DirectorService.java (89%) rename src/main/java/ru/yandex/practicum/filmorate/service/{ => director}/DirectorServiceImpl.java (97%) rename src/main/java/ru/yandex/practicum/filmorate/service/{ => film}/FilmService.java (94%) rename src/main/java/ru/yandex/practicum/filmorate/service/{ => film}/FilmServiceImpl.java (86%) rename src/main/java/ru/yandex/practicum/filmorate/service/{ => genre}/GenreService.java (78%) rename src/main/java/ru/yandex/practicum/filmorate/service/{ => genre}/GenreServiceImpl.java (94%) rename src/main/java/ru/yandex/practicum/filmorate/service/{ => mparating}/MPARatingService.java (79%) rename src/main/java/ru/yandex/practicum/filmorate/service/{ => mparating}/MPARatingServiceImpl.java (94%) rename src/main/java/ru/yandex/practicum/filmorate/service/{ => recommendation}/RecommendationService.java (75%) rename src/main/java/ru/yandex/practicum/filmorate/service/{ => recommendation}/RecommendationServiceImpl.java (95%) rename src/main/java/ru/yandex/practicum/filmorate/service/{ => review}/ReviewService.java (92%) rename src/main/java/ru/yandex/practicum/filmorate/service/{ => review}/ReviewServiceImpl.java (79%) rename src/main/java/ru/yandex/practicum/filmorate/service/{ => user}/UserService.java (92%) rename src/main/java/ru/yandex/practicum/filmorate/service/{ => user}/UserServiceImpl.java (96%) diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/DirectorController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/DirectorController.java index 80b7033..b8ffea9 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/DirectorController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/DirectorController.java @@ -10,7 +10,7 @@ import ru.yandex.practicum.filmorate.dto.director.DirectorDto; import ru.yandex.practicum.filmorate.dto.director.NewDirectorRequest; import ru.yandex.practicum.filmorate.dto.director.UpdateDirectorRequest; -import ru.yandex.practicum.filmorate.service.DirectorService; +import ru.yandex.practicum.filmorate.service.director.DirectorService; import java.util.Collection; diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java index 3d09f66..1359e8a 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -10,7 +10,7 @@ import ru.yandex.practicum.filmorate.dto.film.FilmDto; import ru.yandex.practicum.filmorate.dto.film.NewFilmRequest; import ru.yandex.practicum.filmorate.dto.film.UpdateFilmRequest; -import ru.yandex.practicum.filmorate.service.FilmService; +import ru.yandex.practicum.filmorate.service.film.FilmService; import java.util.Collection; @@ -56,15 +56,15 @@ public FilmDto update(@RequestBody @Valid UpdateFilmRequest request) { } @PutMapping("/{id}/like/{userId}") - public void addLike(@PathVariable @Positive long id, - @PathVariable @Positive long userId) { + public void addLike(@PathVariable long id, + @PathVariable long userId) { log.trace("Add like requested film id: {}, user id: {}", id, userId); filmService.addLike(id, userId); } @DeleteMapping("/{id}/like/{userId}") - public void deleteLike(@PathVariable @Positive long id, - @PathVariable @Positive long userId) { + public void deleteLike(@PathVariable long id, + @PathVariable long userId) { log.trace("Delete like requested film id: {}, user id: {}", id, userId); filmService.removeLike(id, userId); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java index 0151f21..15f0ad9 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java @@ -9,7 +9,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import ru.yandex.practicum.filmorate.dto.genre.GenreDto; -import ru.yandex.practicum.filmorate.service.GenreService; +import ru.yandex.practicum.filmorate.service.genre.GenreService; import java.util.Collection; diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/MPARatingController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/MPARatingController.java index 3eee3ce..236b665 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/MPARatingController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/MPARatingController.java @@ -9,7 +9,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import ru.yandex.practicum.filmorate.dto.mparating.MPARatingDto; -import ru.yandex.practicum.filmorate.service.MPARatingService; +import ru.yandex.practicum.filmorate.service.mparating.MPARatingService; import java.util.Collection; diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/ReviewController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/ReviewController.java index a255845..d0489de 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/ReviewController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/ReviewController.java @@ -10,7 +10,7 @@ import ru.yandex.practicum.filmorate.dto.review.NewReviewRequest; import ru.yandex.practicum.filmorate.dto.review.ReviewDto; import ru.yandex.practicum.filmorate.dto.review.UpdateReviewRequest; -import ru.yandex.practicum.filmorate.service.ReviewService; +import ru.yandex.practicum.filmorate.service.review.ReviewService; import java.util.Collection; @@ -81,4 +81,4 @@ public void removeDislike(@PathVariable @Positive long id, log.trace("Remove review dislike by reviewId: {}, from userId: {}", id, userId); reviewService.removeDislike(id, userId); } -} \ No newline at end of file +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java index 6133fb5..397b773 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java @@ -11,8 +11,8 @@ import ru.yandex.practicum.filmorate.dto.user.NewUserRequest; import ru.yandex.practicum.filmorate.dto.user.UpdateUserRequest; import ru.yandex.practicum.filmorate.dto.user.UserDto; -import ru.yandex.practicum.filmorate.service.RecommendationService; -import ru.yandex.practicum.filmorate.service.UserService; +import ru.yandex.practicum.filmorate.service.recommendation.RecommendationService; +import ru.yandex.practicum.filmorate.service.user.UserService; import java.util.Collection; @@ -60,8 +60,8 @@ public UserDto update(@RequestBody @Valid UpdateUserRequest user) { } @PutMapping("/{id}/friends/{friendId}") - public void addFriend(@PathVariable @Positive long id, - @PathVariable @Positive long friendId) { + public void addFriend(@PathVariable long id, + @PathVariable long friendId) { log.trace("Add friend requested for id {}, friend id: {}", id, friendId); userService.addFriend(id, friendId); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/film/UpdateFilmRequest.java b/src/main/java/ru/yandex/practicum/filmorate/dto/film/UpdateFilmRequest.java index 98ed1a7..24ba4dd 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dto/film/UpdateFilmRequest.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/film/UpdateFilmRequest.java @@ -1,5 +1,7 @@ package ru.yandex.practicum.filmorate.dto.film; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.Size; @@ -30,6 +32,11 @@ public class UpdateFilmRequest { List genres; List directors; + @JsonSetter(nulls = Nulls.AS_EMPTY) + public void setDirectors(List directors) { + this.directors = directors; + } + public boolean hasName() { return name != null && !name.isEmpty(); } @@ -47,7 +54,7 @@ public boolean hasDuration() { } public boolean hasGenres() { - return genres != null && !genres.isEmpty(); + return genres != null; } public boolean hasMpa() { @@ -55,6 +62,6 @@ public boolean hasMpa() { } public boolean hasDirectors() { - return directors != null && !directors.isEmpty(); + return directors != null; } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmMapper.java index d034f99..b97e912 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmMapper.java +++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmMapper.java @@ -6,9 +6,6 @@ import ru.yandex.practicum.filmorate.dto.film.UpdateFilmRequest; import ru.yandex.practicum.filmorate.dto.genre.GenreDto; import ru.yandex.practicum.filmorate.model.Film; -import ru.yandex.practicum.filmorate.model.Genre; - -import java.util.stream.Collectors; @UtilityClass public class FilmMapper { @@ -40,15 +37,14 @@ public Film toFilm(NewFilmRequest request) { .mpaRating(MPARatingMapper.toMPARating(request.getMpa())) .genres(request.getGenres() .stream() - .map(genreDto -> Genre.builder() - .id(genreDto.id()) - .name(genreDto.name()) - .build()) - .collect(Collectors.toSet())) + .map(GenreMapper::toGenre) + .distinct() + .toList()) .directors(request.getDirectors() .stream() .map(DirectorMapper::toDirector) - .collect(Collectors.toSet())) + .distinct() + .toList()) .build(); } @@ -72,7 +68,8 @@ public Film updateFilmFields(Film film, UpdateFilmRequest request) { if (request.hasGenres()) { film.setGenres(request.getGenres().stream() .map(GenreMapper::toGenre) - .collect(Collectors.toSet())); + .distinct() + .toList()); } if (request.hasMpa()) { @@ -82,7 +79,8 @@ public Film updateFilmFields(Film film, UpdateFilmRequest request) { if (request.hasDirectors()) { film.setDirectors(request.getDirectors().stream() .map(DirectorMapper::toDirector) - .collect(Collectors.toSet())); + .distinct() + .toList()); } return film; diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java index ff6b254..82e0a3c 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java @@ -6,8 +6,8 @@ import lombok.experimental.FieldDefaults; import java.time.LocalDate; -import java.util.HashSet; -import java.util.Set; +import java.util.ArrayList; +import java.util.List; @Data @Builder @@ -20,11 +20,7 @@ public class Film { Integer duration; MPARating mpaRating; @Builder.Default - Set genres = new HashSet<>(); + List genres = new ArrayList<>(); @Builder.Default - Set directors = new HashSet<>(); - - public void addGenre(Genre genre) { - this.genres.add(genre); - } + List directors = new ArrayList<>(); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/director/DirectorRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/director/DirectorRepository.java index fd09440..b26d76d 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/repository/director/DirectorRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/director/DirectorRepository.java @@ -3,6 +3,7 @@ import ru.yandex.practicum.filmorate.model.Director; import java.util.Collection; +import java.util.List; import java.util.Optional; public interface DirectorRepository { @@ -15,4 +16,6 @@ public interface DirectorRepository { void update(Director director); void deleteById(long id); + + List getByIds(List directorIds); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/director/JdbcDirectorRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/director/JdbcDirectorRepository.java index 89aa49c..122404f 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/repository/director/JdbcDirectorRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/director/JdbcDirectorRepository.java @@ -70,4 +70,12 @@ public void deleteById(long id) { jdbc.update(deleteDirectorByIdSql, params); } + + @Override + public List getByIds(List directorIds) { + MapSqlParameterSource params = new MapSqlParameterSource() + .addValue("list", directorIds); + String selectAllDirectorsByIdsSql = "SELECT * FROM directors WHERE director_id IN (:list)"; + return jdbc.query(selectAllDirectorsByIdsSql, params, rowMapper); + } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRowMapper.java index 53d7c9c..c2bac65 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRowMapper.java +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRowMapper.java @@ -12,8 +12,7 @@ import java.time.LocalDate; import java.util.Arrays; import java.util.Collections; -import java.util.Set; -import java.util.stream.Collectors; +import java.util.List; @Component public class FilmRowMapper implements RowMapper { @@ -25,7 +24,7 @@ public Film mapRow(ResultSet rs, int rowNum) throws SQLException { .build(); String genresString = rs.getString("genres"); - Set genres; + List genres; if (genresString != null && !genresString.isEmpty() && !genresString.startsWith(":")) { genres = Arrays.stream(genresString.split(";")) .map(genreIdNameStr -> { @@ -35,13 +34,13 @@ public Film mapRow(ResultSet rs, int rowNum) throws SQLException { .name(splitIdNameStr[1]) .build(); }) - .collect(Collectors.toSet()); + .toList(); } else { - genres = Collections.emptySet(); + genres = Collections.emptyList(); } String directorsString = rs.getString("directors"); - Set directors; + List directors; if (directorsString != null && !directorsString.isEmpty() && !directorsString.startsWith(":")) { directors = Arrays.stream(directorsString.split(";")) .map(directorIdNameStr -> { @@ -51,9 +50,9 @@ public Film mapRow(ResultSet rs, int rowNum) throws SQLException { .name(splitIdNameStr[1]) .build(); }) - .collect(Collectors.toSet()); + .toList(); } else { - directors = Collections.emptySet(); + directors = Collections.emptyList(); } return Film.builder() diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java index 2933fa0..a150f85 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java @@ -15,7 +15,6 @@ import java.util.Collection; import java.util.List; import java.util.Optional; -import java.util.Set; @Repository @RequiredArgsConstructor @@ -29,7 +28,8 @@ public class JdbcFilmRepository implements FilmRepository { f.mpa_id, mr.name AS mpa_name, GROUP_CONCAT(DISTINCT CONCAT(g.genre_id, ':', g.name) ORDER BY g.genre_id SEPARATOR ';') AS genres, - GROUP_CONCAT(DISTINCT CONCAT(d.director_id, ':', d.name) ORDER BY g.genre_id SEPARATOR ';') AS directors + GROUP_CONCAT(DISTINCT CONCAT(d.director_id, ':', d.name) + ORDER BY d.director_id SEPARATOR ';') AS directors FROM films f JOIN mpa_ratings mr ON f.mpa_id = mr.mpa_id LEFT JOIN film_genres fg ON f.film_id = fg.film_id @@ -90,16 +90,15 @@ public void update(Film film) { String deleteFilmGenresSql = """ DELETE FROM film_genres WHERE film_id = :film_id"""; - MapSqlParameterSource deleteFilmGenresParams = new MapSqlParameterSource() + MapSqlParameterSource filmIdParams = new MapSqlParameterSource() .addValue("film_id", film.getId()); - jdbc.update(deleteFilmGenresSql, deleteFilmGenresParams); + jdbc.update(deleteFilmGenresSql, filmIdParams); saveGenres(film.getGenres(), film.getId()); String deleteFilmDirectorsSql = """ DELETE FROM film_directors WHERE film_id = :film_id"""; - MapSqlParameterSource deleteFilmDirectorsParams = new MapSqlParameterSource() - .addValue("film_id", film.getId()); - jdbc.update(deleteFilmDirectorsSql, deleteFilmDirectorsParams); + + jdbc.update(deleteFilmDirectorsSql, filmIdParams); saveDirectors(film.getDirectors(), film.getId()); } @@ -171,13 +170,13 @@ public Collection findFilmsOfDirector(long directorId, FilmsSortBy sortFil @Override public Collection findFilmRecommendations(long userId, long similarUserId) { String sqlRecommendations = BASE_SELECT_SQL.concat(""" - LEFT JOIN likes l ON f.film_id = l.film_id - WHERE l.user_id = :similarUserId - AND f.film_id NOT IN ( - SELECT film_id FROM likes WHERE user_id = :userId - ) - GROUP BY f.film_id - """); + LEFT JOIN likes l ON f.film_id = l.film_id + WHERE l.user_id = :similarUserId + AND f.film_id NOT IN ( + SELECT film_id FROM likes WHERE user_id = :userId + ) + GROUP BY f.film_id + """); MapSqlParameterSource params = new MapSqlParameterSource() .addValue("similarUserId", similarUserId) @@ -186,7 +185,7 @@ AND f.film_id NOT IN ( return jdbc.query(sqlRecommendations, params, filmRowMapper); } - private void saveGenres(Set genres, long filmId) { + private void saveGenres(Collection genres, long filmId) { String insertGenresSql = """ INSERT INTO film_genres (film_id, genre_id) VALUES (:film_id, :genre_id)"""; @@ -201,7 +200,7 @@ INSERT INTO film_genres (film_id, genre_id) jdbc.batchUpdate(insertGenresSql, batchParams); } - private void saveDirectors(Set directors, long filmId) { + private void saveDirectors(Collection directors, long filmId) { String insertDirectorsSql = """ INSERT INTO film_directors (film_id, director_id) VALUES (:film_id, :director_id)"""; diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/review/JdbcReviewRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/review/JdbcReviewRepository.java index 91e2aaa..ab66ed5 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/repository/review/JdbcReviewRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/review/JdbcReviewRepository.java @@ -171,4 +171,4 @@ private void changeUsefulRating(MapSqlParameterSource params, int points) { WHERE review_id = :review_id """, params); } -} \ No newline at end of file +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/DirectorService.java b/src/main/java/ru/yandex/practicum/filmorate/service/director/DirectorService.java similarity index 89% rename from src/main/java/ru/yandex/practicum/filmorate/service/DirectorService.java rename to src/main/java/ru/yandex/practicum/filmorate/service/director/DirectorService.java index 2e541ae..57be400 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/DirectorService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/director/DirectorService.java @@ -1,4 +1,4 @@ -package ru.yandex.practicum.filmorate.service; +package ru.yandex.practicum.filmorate.service.director; import ru.yandex.practicum.filmorate.dto.director.DirectorDto; import ru.yandex.practicum.filmorate.dto.director.NewDirectorRequest; diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/DirectorServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/director/DirectorServiceImpl.java similarity index 97% rename from src/main/java/ru/yandex/practicum/filmorate/service/DirectorServiceImpl.java rename to src/main/java/ru/yandex/practicum/filmorate/service/director/DirectorServiceImpl.java index 0708132..c5df349 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/DirectorServiceImpl.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/director/DirectorServiceImpl.java @@ -1,4 +1,4 @@ -package ru.yandex.practicum.filmorate.service; +package ru.yandex.practicum.filmorate.service.director; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java b/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmService.java similarity index 94% rename from src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java rename to src/main/java/ru/yandex/practicum/filmorate/service/film/FilmService.java index 2ca937f..d2ad983 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmService.java @@ -1,4 +1,4 @@ -package ru.yandex.practicum.filmorate.service; +package ru.yandex.practicum.filmorate.service.film; import ru.yandex.practicum.filmorate.controller.FilmsSortBy; import ru.yandex.practicum.filmorate.dto.film.FilmDto; diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/FilmServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmServiceImpl.java similarity index 86% rename from src/main/java/ru/yandex/practicum/filmorate/service/FilmServiceImpl.java rename to src/main/java/ru/yandex/practicum/filmorate/service/film/FilmServiceImpl.java index 8ddfd81..c4397da 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmServiceImpl.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmServiceImpl.java @@ -1,4 +1,4 @@ -package ru.yandex.practicum.filmorate.service; +package ru.yandex.practicum.filmorate.service.film; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; @@ -6,12 +6,14 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import ru.yandex.practicum.filmorate.controller.FilmsSortBy; +import ru.yandex.practicum.filmorate.dto.director.DirectorDto; import ru.yandex.practicum.filmorate.dto.film.FilmDto; import ru.yandex.practicum.filmorate.dto.film.NewFilmRequest; import ru.yandex.practicum.filmorate.dto.film.UpdateFilmRequest; import ru.yandex.practicum.filmorate.dto.genre.GenreDto; import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.mapper.FilmMapper; +import ru.yandex.practicum.filmorate.model.Director; import ru.yandex.practicum.filmorate.model.Film; import ru.yandex.practicum.filmorate.model.Genre; import ru.yandex.practicum.filmorate.repository.director.DirectorRepository; @@ -67,6 +69,17 @@ public FilmDto create(NewFilmRequest request) { throw new NotFoundException("No such genres found"); } + List directorIds = request.getDirectors().stream() + .distinct() + .map(DirectorDto::id) + .toList(); + + List directors = directorRepository.getByIds(directorIds); + + if (directorIds.size() != directors.size()) { + throw new NotFoundException("No such directors found"); + } + film = filmRepository.save(film); log.info("Film with id {} has been created", film.getId()); return FilmMapper.toFilmDto(film); @@ -93,6 +106,20 @@ public FilmDto update(UpdateFilmRequest request) { } } + if (request.hasDirectors()) { + + List directorIds = request.getDirectors().stream() + .distinct() + .map(DirectorDto::id) + .toList(); + + List directors = directorRepository.getByIds(directorIds); + + if (directorIds.size() != directors.size()) { + throw new NotFoundException("No such directors found"); + } + } + film = FilmMapper.updateFilmFields(film, request); log.info("Film with id {} has been updated", film.getId()); filmRepository.update(film); diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/GenreService.java b/src/main/java/ru/yandex/practicum/filmorate/service/genre/GenreService.java similarity index 78% rename from src/main/java/ru/yandex/practicum/filmorate/service/GenreService.java rename to src/main/java/ru/yandex/practicum/filmorate/service/genre/GenreService.java index 04be5dc..182990c 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/GenreService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/genre/GenreService.java @@ -1,4 +1,4 @@ -package ru.yandex.practicum.filmorate.service; +package ru.yandex.practicum.filmorate.service.genre; import ru.yandex.practicum.filmorate.dto.genre.GenreDto; diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/GenreServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/genre/GenreServiceImpl.java similarity index 94% rename from src/main/java/ru/yandex/practicum/filmorate/service/GenreServiceImpl.java rename to src/main/java/ru/yandex/practicum/filmorate/service/genre/GenreServiceImpl.java index 23f435e..848c11e 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/GenreServiceImpl.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/genre/GenreServiceImpl.java @@ -1,4 +1,4 @@ -package ru.yandex.practicum.filmorate.service; +package ru.yandex.practicum.filmorate.service.genre; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/MPARatingService.java b/src/main/java/ru/yandex/practicum/filmorate/service/mparating/MPARatingService.java similarity index 79% rename from src/main/java/ru/yandex/practicum/filmorate/service/MPARatingService.java rename to src/main/java/ru/yandex/practicum/filmorate/service/mparating/MPARatingService.java index 675edc1..68a73d6 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/MPARatingService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/mparating/MPARatingService.java @@ -1,4 +1,4 @@ -package ru.yandex.practicum.filmorate.service; +package ru.yandex.practicum.filmorate.service.mparating; import ru.yandex.practicum.filmorate.dto.mparating.MPARatingDto; diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/MPARatingServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/mparating/MPARatingServiceImpl.java similarity index 94% rename from src/main/java/ru/yandex/practicum/filmorate/service/MPARatingServiceImpl.java rename to src/main/java/ru/yandex/practicum/filmorate/service/mparating/MPARatingServiceImpl.java index 85d5140..d9d80d5 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/MPARatingServiceImpl.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/mparating/MPARatingServiceImpl.java @@ -1,4 +1,4 @@ -package ru.yandex.practicum.filmorate.service; +package ru.yandex.practicum.filmorate.service.mparating; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/RecommendationService.java b/src/main/java/ru/yandex/practicum/filmorate/service/recommendation/RecommendationService.java similarity index 75% rename from src/main/java/ru/yandex/practicum/filmorate/service/RecommendationService.java rename to src/main/java/ru/yandex/practicum/filmorate/service/recommendation/RecommendationService.java index 624624f..2b048b6 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/RecommendationService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/recommendation/RecommendationService.java @@ -1,4 +1,4 @@ -package ru.yandex.practicum.filmorate.service; +package ru.yandex.practicum.filmorate.service.recommendation; import ru.yandex.practicum.filmorate.dto.film.FilmDto; diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/RecommendationServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/recommendation/RecommendationServiceImpl.java similarity index 95% rename from src/main/java/ru/yandex/practicum/filmorate/service/RecommendationServiceImpl.java rename to src/main/java/ru/yandex/practicum/filmorate/service/recommendation/RecommendationServiceImpl.java index c5fa259..9ad2b04 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/RecommendationServiceImpl.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/recommendation/RecommendationServiceImpl.java @@ -1,4 +1,4 @@ -package ru.yandex.practicum.filmorate.service; +package ru.yandex.practicum.filmorate.service.recommendation; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/ReviewService.java b/src/main/java/ru/yandex/practicum/filmorate/service/review/ReviewService.java similarity index 92% rename from src/main/java/ru/yandex/practicum/filmorate/service/ReviewService.java rename to src/main/java/ru/yandex/practicum/filmorate/service/review/ReviewService.java index 6333f9d..08a2212 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/ReviewService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/review/ReviewService.java @@ -1,4 +1,4 @@ -package ru.yandex.practicum.filmorate.service; +package ru.yandex.practicum.filmorate.service.review; import ru.yandex.practicum.filmorate.dto.review.NewReviewRequest; import ru.yandex.practicum.filmorate.dto.review.ReviewDto; @@ -25,4 +25,4 @@ public interface ReviewService { void removeLike(long id, long userId); void removeDislike(long id, long userId); -} \ No newline at end of file +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/ReviewServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/review/ReviewServiceImpl.java similarity index 79% rename from src/main/java/ru/yandex/practicum/filmorate/service/ReviewServiceImpl.java rename to src/main/java/ru/yandex/practicum/filmorate/service/review/ReviewServiceImpl.java index 29c0763..ba5c5a6 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/ReviewServiceImpl.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/review/ReviewServiceImpl.java @@ -1,4 +1,4 @@ -package ru.yandex.practicum.filmorate.service; +package ru.yandex.practicum.filmorate.service.review; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -23,8 +23,8 @@ public class ReviewServiceImpl implements ReviewService { @Override public ReviewDto create(NewReviewRequest request) { - userExistenceCheck(request.getUserId()); - filmExistenceCheck(request.getFilmId()); + throwIfUserNotFound(request.getUserId()); + throwIfFilmNotFound(request.getFilmId()); Review review = ReviewMapper.toReview(request); review = reviewRepository.save(review); log.info("Review with reviewId {} has been created", review.getId()); @@ -33,7 +33,7 @@ public ReviewDto create(NewReviewRequest request) { @Override public ReviewDto update(UpdateReviewRequest request) { - Review review = reviewExistenceCheck(request.getReviewId()); + Review review = getReviewOrThrow(request.getReviewId()); review = ReviewMapper.updateReviewFields(review, request); log.info("Review with reviewId {} has been updated", review.getId()); reviewRepository.update(review); @@ -42,19 +42,22 @@ public ReviewDto update(UpdateReviewRequest request) { @Override public void delete(long id) { - reviewExistenceCheck(id); + getReviewOrThrow(id); log.info("Review with reviewId {} has been deleted", id); reviewRepository.delete(id); } @Override public ReviewDto findById(long id) { - return ReviewMapper.toDto(reviewExistenceCheck(id)); + return ReviewMapper.toDto(getReviewOrThrow(id)); } @Override public Collection findAllByFilm(Long filmId, long count) { - filmExistenceCheck(filmId); + if (filmId != null) { + throwIfFilmNotFound(filmId); + } + return reviewRepository.findAllByFilm(filmId, count).stream() .map(ReviewMapper::toDto) .toList(); @@ -62,48 +65,48 @@ public Collection findAllByFilm(Long filmId, long count) { @Override public void setLike(long id, long userId) { - reviewExistenceCheck(id); - userExistenceCheck(userId); + getReviewOrThrow(id); + throwIfUserNotFound(userId); log.info("Like to review {} has been added by user {}", id, userId); reviewRepository.setLike(id, userId); } @Override public void setDislike(long id, long userId) { - reviewExistenceCheck(id); - userExistenceCheck(userId); + getReviewOrThrow(id); + throwIfUserNotFound(userId); log.info("Dislike to review {} has been added by user {}", id, userId); reviewRepository.setDislike(id, userId); } @Override public void removeLike(long id, long userId) { - reviewExistenceCheck(id); - userExistenceCheck(userId); + getReviewOrThrow(id); + throwIfUserNotFound(userId); log.info("Like to review {} has been removed by user {}", id, userId); reviewRepository.removeLike(id, userId); } @Override public void removeDislike(long id, long userId) { - reviewExistenceCheck(id); - userExistenceCheck(userId); + getReviewOrThrow(id); + throwIfUserNotFound(userId); log.info("Dislike to review {} has been removed by user {}", id, userId); reviewRepository.removeDislike(id, userId); } - public Review reviewExistenceCheck(long id) { + private Review getReviewOrThrow(long id) { return reviewRepository.findById(id) .orElseThrow(NotFoundException.supplier("Review with reviewId %d not found", id)); } - public void userExistenceCheck(long id) { + private void throwIfUserNotFound(long id) { userRepository.findById(id) .orElseThrow(NotFoundException.supplier("User with userId %d not found", id)); } - public void filmExistenceCheck(long id) { + private void throwIfFilmNotFound(long id) { filmRepository.findById(id) .orElseThrow(NotFoundException.supplier("Film with filmId %d not found", id)); } -} \ No newline at end of file +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java b/src/main/java/ru/yandex/practicum/filmorate/service/user/UserService.java similarity index 92% rename from src/main/java/ru/yandex/practicum/filmorate/service/UserService.java rename to src/main/java/ru/yandex/practicum/filmorate/service/user/UserService.java index 5163030..fc35bd8 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/user/UserService.java @@ -1,4 +1,4 @@ -package ru.yandex.practicum.filmorate.service; +package ru.yandex.practicum.filmorate.service.user; import ru.yandex.practicum.filmorate.dto.user.NewUserRequest; import ru.yandex.practicum.filmorate.dto.user.UpdateUserRequest; diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/UserServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/user/UserServiceImpl.java similarity index 96% rename from src/main/java/ru/yandex/practicum/filmorate/service/UserServiceImpl.java rename to src/main/java/ru/yandex/practicum/filmorate/service/user/UserServiceImpl.java index f7baaaf..ac7ae26 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/UserServiceImpl.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/user/UserServiceImpl.java @@ -1,4 +1,4 @@ -package ru.yandex.practicum.filmorate.service; +package ru.yandex.practicum.filmorate.service.user; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -38,7 +38,7 @@ public UserDto findById(Long id) { @Override public UserDto create(NewUserRequest request) { - if (request.getName() == null) { + if (request.getName() == null || request.getName().isEmpty()) { request.setName(request.getLogin()); } User user = UserMapper.mapToUser(request); From 496c3fa40d076feec249c70e9e6b26c74135daee Mon Sep 17 00:00:00 2001 From: n20va <99407852+n20va@users.noreply.github.com> Date: Sat, 18 Oct 2025 21:35:20 +0300 Subject: [PATCH 08/13] Add feed (#23) * Add files via upload * Add files via upload * Add files via upload * Delete src/main/java/ru/yandex/practicum/filmorate/repository/feed * Add files via upload * Add files via upload * Update UserServiceImpl.java * Update ReviewServiceImpl.java * Update FilmServiceImpl.java * Update schema.sql * Update Event.java * Add files via upload * Add files via upload * Add files via upload * Update FeedService.java * Update FeedServiceImpl.java * Update FeedController.java * Update UserServiceImpl.java * Update FilmServiceImpl.java * Update ReviewServiceImpl.java * Update JdbcFeedRepository.java * Delete src/main/java/ru/yandex/practicum/filmorate/mapper/EventRowMapper.java * Add files via upload * Update FeedController.java * Update FeedController.java * Update JdbcFeedRepository.java * Update FeedService.java * Update FeedServiceImpl.java * Update FilmServiceImpl.java * Update UserServiceImpl.java * Update ReviewServiceImpl.java * Update EventDto.java * Update EventMapper.java * Update EventMapper.java --- .../filmorate/controller/FeedController.java | 23 +++++++++ .../filmorate/dto/event/EventDto.java | 18 +++++++ .../filmorate/mapper/EventMapper.java | 21 ++++++++ .../practicum/filmorate/model/Event.java | 18 +++++++ .../practicum/filmorate/model/EventType.java | 5 ++ .../practicum/filmorate/model/Operation.java | 5 ++ .../repository/feed/EventRowMapper.java | 25 ++++++++++ .../repository/feed/FeedRepository.java | 10 ++++ .../repository/feed/JdbcFeedRepository.java | 43 ++++++++++++++++ .../filmorate/service/feed/FeedService.java | 13 +++++ .../service/feed/FeedServiceImpl.java | 49 +++++++++++++++++++ .../service/film/FilmServiceImpl.java | 6 +++ .../service/review/ReviewServiceImpl.java | 13 ++++- .../service/user/UserServiceImpl.java | 7 ++- src/main/resources/schema.sql | 12 ++++- 15 files changed, 264 insertions(+), 4 deletions(-) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/controller/FeedController.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dto/event/EventDto.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/mapper/EventMapper.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/model/Event.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/model/EventType.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/model/Operation.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/repository/feed/EventRowMapper.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/repository/feed/FeedRepository.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/repository/feed/JdbcFeedRepository.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/service/feed/FeedService.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/service/feed/FeedServiceImpl.java diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/FeedController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/FeedController.java new file mode 100644 index 0000000..b5fad4b --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FeedController.java @@ -0,0 +1,23 @@ +package ru.yandex.practicum.filmorate.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.dto.event.EventDto; +import ru.yandex.practicum.filmorate.service.feed.FeedService; + +import java.util.List; + +@RestController +@RequestMapping("/users") +@Slf4j +@RequiredArgsConstructor +public class FeedController { + private final FeedService feedService; + + @GetMapping("/{id}/feed") + public List getUserFeed(@PathVariable long id) { + log.info("GET /users/{}/feed", id); + return feedService.getUserFeed(id); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/event/EventDto.java b/src/main/java/ru/yandex/practicum/filmorate/dto/event/EventDto.java new file mode 100644 index 0000000..bcd9a40 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/event/EventDto.java @@ -0,0 +1,18 @@ +package ru.yandex.practicum.filmorate.dto.event; + +import lombok.Builder; +import lombok.Data; +import ru.yandex.practicum.filmorate.model.EventType; +import ru.yandex.practicum.filmorate.model.Operation; + +@Data +@Builder +public class EventDto { + private Long timestamp; + private Long userId; + private EventType eventType; + private Operation operation; + private Long eventId; + private Long entityId; + +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/mapper/EventMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mapper/EventMapper.java new file mode 100644 index 0000000..9ac3f6f --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/EventMapper.java @@ -0,0 +1,21 @@ +package ru.yandex.practicum.filmorate.mapper; + +import lombok.experimental.UtilityClass; +import ru.yandex.practicum.filmorate.dto.event.EventDto; +import ru.yandex.practicum.filmorate.model.Event; + +@UtilityClass +public class EventMapper { + + public EventDto toEventDto(Event event) { + return EventDto.builder() + .timestamp(event.getTimestamp()) + .userId(event.getUserId()) + .eventType(event.getEventType()) + .operation(event.getOperation()) + .eventId(event.getEventId()) + .entityId(event.getEntityId()) + .build(); + } +} + diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Event.java b/src/main/java/ru/yandex/practicum/filmorate/model/Event.java new file mode 100644 index 0000000..29dad68 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Event.java @@ -0,0 +1,18 @@ +package ru.yandex.practicum.filmorate.model; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Data; +import lombok.experimental.FieldDefaults; + +@Data +@Builder +@FieldDefaults(level = AccessLevel.PRIVATE) +public class Event { + Long eventId; + Long timestamp; + Long userId; + EventType eventType; + Operation operation; + Long entityId; +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/EventType.java b/src/main/java/ru/yandex/practicum/filmorate/model/EventType.java new file mode 100644 index 0000000..508d573 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/EventType.java @@ -0,0 +1,5 @@ +package ru.yandex.practicum.filmorate.model; + +public enum EventType { + LIKE, REVIEW, FRIEND +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Operation.java b/src/main/java/ru/yandex/practicum/filmorate/model/Operation.java new file mode 100644 index 0000000..74ef03b --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Operation.java @@ -0,0 +1,5 @@ +package ru.yandex.practicum.filmorate.model; + +public enum Operation { + REMOVE, ADD, UPDATE +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/feed/EventRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/repository/feed/EventRowMapper.java new file mode 100644 index 0000000..de91215 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/feed/EventRowMapper.java @@ -0,0 +1,25 @@ +package ru.yandex.practicum.filmorate.repository.feed; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.Event; +import ru.yandex.practicum.filmorate.model.EventType; +import ru.yandex.practicum.filmorate.model.Operation; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@Component +public class EventRowMapper implements RowMapper { + @Override + public Event mapRow(ResultSet rs, int rowNum) throws SQLException { + return Event.builder() + .eventId(rs.getLong("event_id")) + .timestamp(rs.getLong("timestamp")) + .userId(rs.getLong("user_id")) + .eventType(EventType.valueOf(rs.getString("event_type"))) + .operation(Operation.valueOf(rs.getString("operation"))) + .entityId(rs.getLong("entity_id")) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/feed/FeedRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/feed/FeedRepository.java new file mode 100644 index 0000000..7645f79 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/feed/FeedRepository.java @@ -0,0 +1,10 @@ +package ru.yandex.practicum.filmorate.repository.feed; + +import ru.yandex.practicum.filmorate.model.Event; +import java.util.List; + +public interface FeedRepository { + List findByUserId(long userId); + + Event save(Event event); +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/feed/JdbcFeedRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/feed/JdbcFeedRepository.java new file mode 100644 index 0000000..c2b17c2 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/feed/JdbcFeedRepository.java @@ -0,0 +1,43 @@ +package ru.yandex.practicum.filmorate.repository.feed; + +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.model.Event; + +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class JdbcFeedRepository implements FeedRepository { + private final NamedParameterJdbcOperations jdbc; + private final EventRowMapper eventRowMapper; + + @Override + public List findByUserId(long userId) { + String sql = "SELECT * FROM feed_events WHERE user_id = :user_id ORDER BY timestamp ASC"; + return jdbc.query(sql, + new MapSqlParameterSource("user_id", userId), + eventRowMapper); + } + + @Override + public Event save(Event event) { + GeneratedKeyHolder keyHolder = new GeneratedKeyHolder(); + MapSqlParameterSource params = new MapSqlParameterSource() + .addValue("timestamp", event.getTimestamp()) + .addValue("user_id", event.getUserId()) + .addValue("event_type", event.getEventType().toString()) + .addValue("operation", event.getOperation().toString()) + .addValue("entity_id", event.getEntityId()); + String sql = """ + INSERT INTO feed_events (timestamp, user_id, event_type, operation, entity_id) + VALUES (:timestamp, :user_id, :event_type, :operation, :entity_id) + """; + jdbc.update(sql, params, keyHolder, new String[]{"event_id"}); + event.setEventId(keyHolder.getKeyAs(Long.class)); + return event; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/feed/FeedService.java b/src/main/java/ru/yandex/practicum/filmorate/service/feed/FeedService.java new file mode 100644 index 0000000..d9f9df3 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/feed/FeedService.java @@ -0,0 +1,13 @@ +package ru.yandex.practicum.filmorate.service.feed; + +import ru.yandex.practicum.filmorate.dto.event.EventDto; +import ru.yandex.practicum.filmorate.model.EventType; +import ru.yandex.practicum.filmorate.model.Operation; + +import java.util.List; + +public interface FeedService { + List getUserFeed(long userId); + + void addEvent(EventType eventType, Operation operation, long userId, long entityId); +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/feed/FeedServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/feed/FeedServiceImpl.java new file mode 100644 index 0000000..f941602 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/feed/FeedServiceImpl.java @@ -0,0 +1,49 @@ +package ru.yandex.practicum.filmorate.service.feed; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.dto.event.EventDto; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.mapper.EventMapper; +import ru.yandex.practicum.filmorate.model.Event; +import ru.yandex.practicum.filmorate.model.EventType; +import ru.yandex.practicum.filmorate.model.Operation; +import ru.yandex.practicum.filmorate.repository.feed.FeedRepository; +import ru.yandex.practicum.filmorate.repository.user.UserRepository; + +import java.util.List; + +@Service +@Slf4j +@RequiredArgsConstructor +public class FeedServiceImpl implements FeedService { + private final FeedRepository feedRepository; + private final UserRepository userRepository; + + @Override + public List getUserFeed(long userId) { + userRepository.findById(userId) + .orElseThrow(NotFoundException.supplier("User with id %d not found", userId)); + + List events = feedRepository.findByUserId(userId); + log.info("Retrieved {} events for user {}", events.size(), userId); + return events.stream() + .map(EventMapper::toEventDto) + .toList(); + } + + @Override + public void addEvent(EventType eventType, Operation operation, long userId, long entityId) { + Event event = Event.builder() + .timestamp(System.currentTimeMillis()) + .userId(userId) + .eventType(eventType) + .operation(operation) + .entityId(entityId) + .build(); + feedRepository.save(event); + log.debug("Event saved: type={}, operation={}, userId={}, entityId={}", + eventType, operation, userId, entityId); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmServiceImpl.java index c4397da..c21a947 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmServiceImpl.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmServiceImpl.java @@ -14,14 +14,17 @@ import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.mapper.FilmMapper; import ru.yandex.practicum.filmorate.model.Director; +import ru.yandex.practicum.filmorate.model.EventType; import ru.yandex.practicum.filmorate.model.Film; import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.model.Operation; import ru.yandex.practicum.filmorate.repository.director.DirectorRepository; import ru.yandex.practicum.filmorate.repository.film.FilmRepository; import ru.yandex.practicum.filmorate.repository.genre.GenreRepository; import ru.yandex.practicum.filmorate.repository.like.LikesRepository; import ru.yandex.practicum.filmorate.repository.mparating.MPARatingRepository; import ru.yandex.practicum.filmorate.repository.user.UserRepository; +import ru.yandex.practicum.filmorate.service.feed.FeedService; import java.util.Collection; import java.util.List; @@ -37,6 +40,7 @@ public class FilmServiceImpl implements FilmService { GenreRepository genreRepository; MPARatingRepository mpaRepository; DirectorRepository directorRepository; + FeedService feedService; @Override public Collection findAll() { @@ -137,6 +141,7 @@ public void addLike(long filmId, long userId) { throwIfFilmNotFound(filmId); throwIfUserNotFound(userId); likesRepository.addLike(userId, filmId); + feedService.addEvent(EventType.LIKE, Operation.ADD, userId, filmId); log.info("Like to film with id {} has been added by user {}", filmId, userId); } @@ -145,6 +150,7 @@ public void removeLike(long filmId, long userId) { throwIfFilmNotFound(filmId); throwIfUserNotFound(userId); likesRepository.removeLike(userId, filmId); + feedService.addEvent(EventType.LIKE, Operation.REMOVE, userId, filmId); log.info("Like to film with id {} has been removed by user {}", filmId, userId); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/review/ReviewServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/review/ReviewServiceImpl.java index ba5c5a6..d6e29b1 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/review/ReviewServiceImpl.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/review/ReviewServiceImpl.java @@ -3,13 +3,18 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import ru.yandex.practicum.filmorate.dto.review.*; +import ru.yandex.practicum.filmorate.dto.review.NewReviewRequest; +import ru.yandex.practicum.filmorate.dto.review.ReviewDto; +import ru.yandex.practicum.filmorate.dto.review.UpdateReviewRequest; import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.mapper.ReviewMapper; +import ru.yandex.practicum.filmorate.model.EventType; +import ru.yandex.practicum.filmorate.model.Operation; import ru.yandex.practicum.filmorate.model.Review; import ru.yandex.practicum.filmorate.repository.film.FilmRepository; import ru.yandex.practicum.filmorate.repository.review.ReviewRepository; import ru.yandex.practicum.filmorate.repository.user.UserRepository; +import ru.yandex.practicum.filmorate.service.feed.FeedService; import java.util.Collection; @@ -20,6 +25,7 @@ public class ReviewServiceImpl implements ReviewService { private final ReviewRepository reviewRepository; private final UserRepository userRepository; private final FilmRepository filmRepository; + private final FeedService feedService; @Override public ReviewDto create(NewReviewRequest request) { @@ -27,6 +33,7 @@ public ReviewDto create(NewReviewRequest request) { throwIfFilmNotFound(request.getFilmId()); Review review = ReviewMapper.toReview(request); review = reviewRepository.save(review); + feedService.addEvent(EventType.REVIEW, Operation.ADD, request.getUserId(), review.getId()); log.info("Review with reviewId {} has been created", review.getId()); return ReviewMapper.toDto(review); } @@ -35,6 +42,7 @@ public ReviewDto create(NewReviewRequest request) { public ReviewDto update(UpdateReviewRequest request) { Review review = getReviewOrThrow(request.getReviewId()); review = ReviewMapper.updateReviewFields(review, request); + feedService.addEvent(EventType.REVIEW, Operation.UPDATE, review.getUserId(), review.getId()); log.info("Review with reviewId {} has been updated", review.getId()); reviewRepository.update(review); return ReviewMapper.toDto(review); @@ -42,7 +50,8 @@ public ReviewDto update(UpdateReviewRequest request) { @Override public void delete(long id) { - getReviewOrThrow(id); + Review review = getReviewOrThrow(id); + feedService.addEvent(EventType.REVIEW, Operation.REMOVE, review.getUserId(), id); log.info("Review with reviewId {} has been deleted", id); reviewRepository.delete(id); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/user/UserServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/user/UserServiceImpl.java index ac7ae26..f199adf 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/user/UserServiceImpl.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/user/UserServiceImpl.java @@ -8,9 +8,12 @@ import ru.yandex.practicum.filmorate.dto.user.UserDto; import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.mapper.UserMapper; +import ru.yandex.practicum.filmorate.model.EventType; +import ru.yandex.practicum.filmorate.model.Operation; import ru.yandex.practicum.filmorate.model.User; import ru.yandex.practicum.filmorate.repository.friendship.FriendshipsRepository; import ru.yandex.practicum.filmorate.repository.user.UserRepository; +import ru.yandex.practicum.filmorate.service.feed.FeedService; import java.util.Collection; @@ -20,6 +23,7 @@ public class UserServiceImpl implements UserService { private final UserRepository userRepository; private final FriendshipsRepository friendshipsRepository; + private final FeedService feedService; @Override public Collection findAll() { @@ -68,8 +72,8 @@ public void deleteById(long id) { public void addFriend(long userId, long friendId) { throwIfUserNotFound(userId); throwIfUserNotFound(friendId); - friendshipsRepository.addFriendship(userId, friendId); + feedService.addEvent(EventType.FRIEND, Operation.ADD, userId, friendId); log.info("User with id {} added user with id {} as friend", userId, friendId); } @@ -78,6 +82,7 @@ public void removeFriend(long userId, long friendId) { throwIfUserNotFound(userId); throwIfUserNotFound(friendId); friendshipsRepository.removeFriendship(userId, friendId); + feedService.addEvent(EventType.FRIEND, Operation.REMOVE, userId, friendId); log.info("User with id {} removed user with id {} from friends", userId, friendId); } diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 0b7b1a0..40ecaab 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -101,4 +101,14 @@ CREATE TABLE IF NOT EXISTS review_likes PRIMARY KEY (review_id, user_id), FOREIGN KEY (review_id) REFERENCES reviews (review_id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users (user_id) ON DELETE CASCADE -); \ No newline at end of file +); + +CREATE TABLE IF NOT EXISTS feed_events ( + event_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + timestamp BIGINT NOT NULL, + user_id BIGINT NOT NULL, + event_type ENUM('LIKE', 'REVIEW', 'FRIEND') NOT NULL, + operation ENUM('REMOVE', 'ADD', 'UPDATE') NOT NULL, + entity_id BIGINT NOT NULL, + FOREIGN KEY (user_id) REFERENCES users (user_id) ON DELETE CASCADE +); From cc8751a59f25924a1d4d13ee587255b1fe78462b Mon Sep 17 00:00:00 2001 From: LightInTheFire <109972737+LightInTheFire@users.noreply.github.com> Date: Sun, 19 Oct 2025 12:41:44 +0300 Subject: [PATCH 09/13] feat: add search (#19) --- .../filmorate/controller/FilmController.java | 18 +++++++++ .../filmorate/controller/SearchFilmsBy.java | 39 +++++++++++++++++++ .../repository/film/FilmRepository.java | 4 ++ .../repository/film/JdbcFilmRepository.java | 33 ++++++++++++++++ .../filmorate/service/film/FilmService.java | 4 ++ .../service/film/FilmServiceImpl.java | 15 ++++--- 6 files changed, 108 insertions(+), 5 deletions(-) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/controller/SearchFilmsBy.java diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java index 1359e8a..a86e957 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -1,6 +1,7 @@ package ru.yandex.practicum.filmorate.controller; import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Positive; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -13,6 +14,7 @@ import ru.yandex.practicum.filmorate.service.film.FilmService; import java.util.Collection; +import java.util.List; @RestController @RequestMapping("/films") @@ -69,6 +71,22 @@ public void deleteLike(@PathVariable long id, filmService.removeLike(id, userId); } + @GetMapping("/search") + public Collection searchFilms(@RequestParam @NotBlank String query, + @RequestParam(defaultValue = "title") String by) { + log.trace("Search films requested with query: {}, by: {}", query, by); + + List parsedInput = SearchFilmsBy.parseStr(by); + + for (SearchFilmsBy searchFilmsBy : parsedInput) { + if (searchFilmsBy == null) { + throw new IllegalArgumentException("Unknown search params %s".formatted(by)); + } + } + + return filmService.searchFilms(query, parsedInput); + } + @GetMapping("/common") public Collection findCommonFilms(@RequestParam(name = "userId") @Positive long userId, @RequestParam(name = "friendId") @Positive long friendId) { diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/SearchFilmsBy.java b/src/main/java/ru/yandex/practicum/filmorate/controller/SearchFilmsBy.java new file mode 100644 index 0000000..2e598f8 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/SearchFilmsBy.java @@ -0,0 +1,39 @@ +package ru.yandex.practicum.filmorate.controller; + +import lombok.Getter; + +import java.util.ArrayList; +import java.util.List; + +@Getter +public enum SearchFilmsBy { + TITLE("title"), + DIRECTOR("director"); + + private final String value; + + SearchFilmsBy(String value) { + this.value = value; + } + + public static List parseStr(String value) { + String[] splittedStr = value.trim().toLowerCase().split(","); + List parsedList = new ArrayList<>(); + + for (String str : splittedStr) { + SearchFilmsBy searchFilmsBy = SearchFilmsBy.fromString(str); + parsedList.add(searchFilmsBy); + } + + return parsedList; + } + + public static SearchFilmsBy fromString(String value) { + return switch (value) { + case "title" -> SearchFilmsBy.TITLE; + case "director" -> SearchFilmsBy.DIRECTOR; + default -> null; + }; + } + +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRepository.java index 46326de..159e292 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRepository.java @@ -1,9 +1,11 @@ package ru.yandex.practicum.filmorate.repository.film; import ru.yandex.practicum.filmorate.controller.FilmsSortBy; +import ru.yandex.practicum.filmorate.controller.SearchFilmsBy; import ru.yandex.practicum.filmorate.model.Film; import java.util.Collection; +import java.util.List; import java.util.Optional; public interface FilmRepository { @@ -24,4 +26,6 @@ public interface FilmRepository { Collection findFilmsOfDirector(long directorId, FilmsSortBy sortFilmsBy); Collection findFilmRecommendations(long userId, long similarUserId); + + Collection searchFilms(String query, List searchBy); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java index a150f85..31adb6a 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java @@ -8,10 +8,12 @@ import org.springframework.jdbc.support.GeneratedKeyHolder; import org.springframework.stereotype.Repository; import ru.yandex.practicum.filmorate.controller.FilmsSortBy; +import ru.yandex.practicum.filmorate.controller.SearchFilmsBy; import ru.yandex.practicum.filmorate.model.Director; import ru.yandex.practicum.filmorate.model.Film; import ru.yandex.practicum.filmorate.model.Genre; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Optional; @@ -185,6 +187,37 @@ AND f.film_id NOT IN ( return jdbc.query(sqlRecommendations, params, filmRowMapper); } + @Override + public Collection searchFilms(String query, List searchBy) { + String searchParamName = "query"; + String searchFilmsSql = buildSearchSql(searchBy, searchParamName); + String searchPattern = "%%%s%%".formatted(query.toLowerCase()); + + MapSqlParameterSource params = new MapSqlParameterSource() + .addValue(searchParamName, searchPattern); + + return jdbc.query(searchFilmsSql, params, filmRowMapper); + } + + private String buildSearchSql(List searchFilmsBy, String searchParamName) { + List conditions = new ArrayList<>(); + + for (SearchFilmsBy searchBy : searchFilmsBy) { + switch (searchBy) { + case TITLE -> conditions.add("LOWER(f.name) LIKE :%s".formatted(searchParamName)); + case DIRECTOR -> conditions.add("LOWER(d.name) LIKE :%s".formatted(searchParamName)); + } + } + + String whereClause = String.join(" OR ", conditions); + + return BASE_SELECT_SQL.concat(""" + WHERE (%s) + GROUP BY f.film_id + ORDER BY (SELECT COUNT(*) FROM likes l WHERE l.film_id = f.film_id) DESC, f.film_id""".formatted( + whereClause)); + } + private void saveGenres(Collection genres, long filmId) { String insertGenresSql = """ INSERT INTO film_genres (film_id, genre_id) diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmService.java b/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmService.java index d2ad983..c175560 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmService.java @@ -1,11 +1,13 @@ package ru.yandex.practicum.filmorate.service.film; import ru.yandex.practicum.filmorate.controller.FilmsSortBy; +import ru.yandex.practicum.filmorate.controller.SearchFilmsBy; import ru.yandex.practicum.filmorate.dto.film.FilmDto; import ru.yandex.practicum.filmorate.dto.film.NewFilmRequest; import ru.yandex.practicum.filmorate.dto.film.UpdateFilmRequest; import java.util.Collection; +import java.util.List; public interface FilmService { Collection findAll(); @@ -27,4 +29,6 @@ public interface FilmService { Collection findCommonFilms(long userId, long friendId); Collection findFilmsOfDirector(long directorId, FilmsSortBy sortFilmsBy); + + Collection searchFilms(String query, List searchFilmsBy); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmServiceImpl.java index c21a947..a1f17a0 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmServiceImpl.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmServiceImpl.java @@ -6,6 +6,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import ru.yandex.practicum.filmorate.controller.FilmsSortBy; +import ru.yandex.practicum.filmorate.controller.SearchFilmsBy; import ru.yandex.practicum.filmorate.dto.director.DirectorDto; import ru.yandex.practicum.filmorate.dto.film.FilmDto; import ru.yandex.practicum.filmorate.dto.film.NewFilmRequest; @@ -13,11 +14,7 @@ import ru.yandex.practicum.filmorate.dto.genre.GenreDto; import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.mapper.FilmMapper; -import ru.yandex.practicum.filmorate.model.Director; -import ru.yandex.practicum.filmorate.model.EventType; -import ru.yandex.practicum.filmorate.model.Film; -import ru.yandex.practicum.filmorate.model.Genre; -import ru.yandex.practicum.filmorate.model.Operation; +import ru.yandex.practicum.filmorate.model.*; import ru.yandex.practicum.filmorate.repository.director.DirectorRepository; import ru.yandex.practicum.filmorate.repository.film.FilmRepository; import ru.yandex.practicum.filmorate.repository.genre.GenreRepository; @@ -183,6 +180,14 @@ public Collection findFilmsOfDirector(long directorId, FilmsSortBy sort .toList(); } + @Override + public Collection searchFilms(String query, List searchFilmsBy) { + return filmRepository.searchFilms(query, searchFilmsBy) + .stream() + .map(FilmMapper::toFilmDto) + .toList(); + } + private void throwIfDirectorNotFound(long directorId) { directorRepository.findById(directorId) .orElseThrow(NotFoundException.supplier("Director with id %d not found", directorId)); From 8ab85748bbec73361240440359abaa4d25763a03 Mon Sep 17 00:00:00 2001 From: LightInTheFire <109972737+LightInTheFire@users.noreply.github.com> Date: Sun, 19 Oct 2025 14:49:57 +0300 Subject: [PATCH 10/13] Fix: updated project to satisfy tests (#24) * fix: order reviews by useful rating * fix: change group concat to subquery for correct genres mapping * fix: changed insert to do nothing when data is present --- .../filmorate/dto/film/UpdateFilmRequest.java | 7 ++----- .../repository/film/JdbcFilmRepository.java | 13 +++++++++---- .../repository/like/JdbcLikesRepository.java | 6 ++++-- .../repository/review/JdbcReviewRepository.java | 2 ++ src/main/resources/application.properties | 2 +- 5 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/film/UpdateFilmRequest.java b/src/main/java/ru/yandex/practicum/filmorate/dto/film/UpdateFilmRequest.java index 24ba4dd..2f7c0d7 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dto/film/UpdateFilmRequest.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/film/UpdateFilmRequest.java @@ -14,6 +14,7 @@ import ru.yandex.practicum.filmorate.validation.AfterDate; import java.time.LocalDate; +import java.util.ArrayList; import java.util.List; @Data @@ -30,12 +31,8 @@ public class UpdateFilmRequest { Integer duration; MPARatingDto mpa; List genres; - List directors; - @JsonSetter(nulls = Nulls.AS_EMPTY) - public void setDirectors(List directors) { - this.directors = directors; - } + List directors = new ArrayList<>(); public boolean hasName() { return name != null && !name.isEmpty(); diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java index 31adb6a..e2aa20b 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java @@ -28,10 +28,15 @@ public class JdbcFilmRepository implements FilmRepository { f.release_date, f.duration_in_minutes, f.mpa_id, - mr.name AS mpa_name, - GROUP_CONCAT(DISTINCT CONCAT(g.genre_id, ':', g.name) ORDER BY g.genre_id SEPARATOR ';') AS genres, - GROUP_CONCAT(DISTINCT CONCAT(d.director_id, ':', d.name) - ORDER BY d.director_id SEPARATOR ';') AS directors + mr.name AS mpa_name, + (SELECT GROUP_CONCAT(DISTINCT CONCAT(g2.genre_id, ':', g2.name) ORDER BY g2.genre_id SEPARATOR ';') + FROM film_genres fg2 + JOIN genres g2 ON g2.genre_id = fg2.genre_id + WHERE fg2.film_id = f.film_id) AS genres, + (SELECT GROUP_CONCAT(DISTINCT CONCAT(d2.director_id, ':', d2.name) ORDER BY d2.director_id SEPARATOR ';') + FROM film_directors fd2 + JOIN directors d2 ON d2.director_id = fd2.director_id + WHERE fd2.film_id = f.film_id) AS directors FROM films f JOIN mpa_ratings mr ON f.mpa_id = mr.mpa_id LEFT JOIN film_genres fg ON f.film_id = fg.film_id diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/like/JdbcLikesRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/like/JdbcLikesRepository.java index 85a9b6f..f309b94 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/repository/like/JdbcLikesRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/like/JdbcLikesRepository.java @@ -14,8 +14,10 @@ public class JdbcLikesRepository implements LikesRepository { public void addLike(long userId, long filmId) { MapSqlParameterSource params = getParameterMap(userId, filmId); String addLikeToFilmSql = """ - INSERT INTO likes (user_id, film_id) - VALUES (:user_id, :film_id)"""; + MERGE INTO likes (user_id, film_id) + KEY(user_id, film_id) + VALUES (:user_id, :film_id) + """; jdbc.update(addLikeToFilmSql, params); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/review/JdbcReviewRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/review/JdbcReviewRepository.java index ab66ed5..8bbb5bc 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/repository/review/JdbcReviewRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/review/JdbcReviewRepository.java @@ -78,6 +78,7 @@ public Optional findById(long id) { MapSqlParameterSource params = new MapSqlParameterSource("review_id", id); List list = jdbc.query(BASE_SELECT_REVIEW_SQL.concat(""" WHERE r.review_id = :review_id + ORDER BY useful_rating DESC """), params, mapper); return list.isEmpty() ? Optional.empty() : Optional.of(list.getFirst()); } @@ -88,6 +89,7 @@ public Collection findAllByFilm(Long filmId, long count) { .addValue("count", count); return jdbc.query(BASE_SELECT_REVIEW_SQL.concat(""" WHERE fr.film_id = COALESCE(:film_id, fr.film_id) + ORDER BY useful_rating DESC LIMIT :count """), params, mapper); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index ac5b1c6..fc2688c 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -5,4 +5,4 @@ spring.sql.init.mode=ALWAYS spring.datasource.url=jdbc:h2:file:./db/filmorate spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa -spring.datasource.password=password \ No newline at end of file +spring.datasource.password=password From 3643fbf1c7f209c12fd15d9c562e693c275b25d5 Mon Sep 17 00:00:00 2001 From: basementdoor <119163446+basementdoor@users.noreply.github.com> Date: Mon, 20 Oct 2025 21:38:40 +0300 Subject: [PATCH 11/13] fix film recommendations after review (#27) * fix film recommendations after review * fix: count distinct likes --------- Co-authored-by: Egor Ilyin Co-authored-by: Ilia Egorov --- .../filmorate/repository/film/FilmRepository.java | 2 +- .../repository/film/JdbcFilmRepository.java | 7 ++++--- .../repository/user/JdbcUserRepository.java | 13 +++---------- .../filmorate/repository/user/UserRepository.java | 3 ++- .../recommendation/RecommendationServiceImpl.java | 9 ++++----- 5 files changed, 14 insertions(+), 20 deletions(-) diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRepository.java index 159e292..32949af 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/film/FilmRepository.java @@ -25,7 +25,7 @@ public interface FilmRepository { Collection findFilmsOfDirector(long directorId, FilmsSortBy sortFilmsBy); - Collection findFilmRecommendations(long userId, long similarUserId); + Collection findFilmRecommendations(long userId, List similarUserIds); Collection searchFilms(String query, List searchBy); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java index e2aa20b..253b22a 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java @@ -175,18 +175,19 @@ public Collection findFilmsOfDirector(long directorId, FilmsSortBy sortFil } @Override - public Collection findFilmRecommendations(long userId, long similarUserId) { + public Collection findFilmRecommendations(long userId, List similarUserIds) { String sqlRecommendations = BASE_SELECT_SQL.concat(""" LEFT JOIN likes l ON f.film_id = l.film_id - WHERE l.user_id = :similarUserId + WHERE l.user_id IN (:similarUserIds) AND f.film_id NOT IN ( SELECT film_id FROM likes WHERE user_id = :userId ) GROUP BY f.film_id + ORDER BY COUNT(DISTINCT l.user_id) DESC """); MapSqlParameterSource params = new MapSqlParameterSource() - .addValue("similarUserId", similarUserId) + .addValue("similarUserIds", similarUserIds) .addValue("userId", userId); return jdbc.query(sqlRecommendations, params, filmRowMapper); diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/user/JdbcUserRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/user/JdbcUserRepository.java index 6b6ec95..79b02ee 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/repository/user/JdbcUserRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/user/JdbcUserRepository.java @@ -115,7 +115,7 @@ WHERE u.user_id IN (SELECT user_id2 return jdbc.query(selectCommonFriendsSql, params, rowMapper); } - public Optional findSimilarFilmTasteUser(long userId) { + public List findSimilarFilmTasteUsers(long userId) { MapSqlParameterSource params = new MapSqlParameterSource() .addValue("userId", userId); @@ -128,15 +128,8 @@ public Optional findSimilarFilmTasteUser(long userId) { AND l2.user_id != :userId GROUP BY l2.user_id ORDER BY common_like_count DESC - LIMIT 1"""; + LIMIT 5"""; - Long result = jdbc.query(sql, params, rs -> { - if (rs.next()) { - return rs.getObject("other_user", Long.class); - } - return null; - }); - - return Optional.ofNullable(result); + return jdbc.query(sql, params, (rs, rowNum) -> rs.getLong("other_user")); } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/user/UserRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/user/UserRepository.java index 92f37c0..ed45ac7 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/repository/user/UserRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/user/UserRepository.java @@ -3,6 +3,7 @@ import ru.yandex.practicum.filmorate.model.User; import java.util.Collection; +import java.util.List; import java.util.Optional; public interface UserRepository { @@ -20,5 +21,5 @@ public interface UserRepository { Collection findAllCommonFriends(long userId1, long userId2); - Optional findSimilarFilmTasteUser(long userId); + List findSimilarFilmTasteUsers(long userId); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/recommendation/RecommendationServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/recommendation/RecommendationServiceImpl.java index 9ad2b04..26d9c8e 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/recommendation/RecommendationServiceImpl.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/recommendation/RecommendationServiceImpl.java @@ -11,7 +11,7 @@ import java.util.Collection; import java.util.Collections; -import java.util.Optional; +import java.util.List; @Service @Slf4j @@ -25,14 +25,13 @@ public Collection findFilmRecommendations(long userId) { userRepository.findById(userId) .orElseThrow(NotFoundException.supplier("User with id %d not found", userId)); - Optional similarUserOpt = userRepository.findSimilarFilmTasteUser(userId); + List similarUsers = userRepository.findSimilarFilmTasteUsers(userId); - if (similarUserOpt.isEmpty()) { + if (similarUsers.isEmpty()) { return Collections.emptyList(); } - long similarUserId = similarUserOpt.get(); - return filmRepository.findFilmRecommendations(userId, similarUserId) + return filmRepository.findFilmRecommendations(userId, similarUsers) .stream() .map(FilmMapper::toFilmDto) .toList(); From 7e3a471644d70ccfb939c373ca85e2da7a541740 Mon Sep 17 00:00:00 2001 From: LightInTheFire <109972737+LightInTheFire@users.noreply.github.com> Date: Mon, 20 Oct 2025 22:02:30 +0300 Subject: [PATCH 12/13] update code after review (#26) * feat: add dockerfile and docker compose * fix: removed @Validated unnecessary annotations * fix: update Dockerfile * fix: changed timestamp name in bd to created_at * chore: changed return dto's to records * fix: changed null to optional * fix: corrected likes count * fix: changed timestamp to created_at --- Dockerfile | 48 +++++++++++++++++++ docker-compose.yaml | 13 +++++ .../controller/DirectorController.java | 2 - .../filmorate/controller/FilmController.java | 17 ++----- .../filmorate/controller/FilmsSortBy.java | 10 ++-- .../filmorate/controller/SearchFilmsBy.java | 18 ++++--- .../filmorate/controller/UserController.java | 2 - .../dto/director/NewDirectorRequest.java | 3 ++ .../filmorate/dto/event/EventDto.java | 17 +++---- .../practicum/filmorate/dto/film/FilmDto.java | 27 ++++------- .../filmorate/dto/review/ReviewDto.java | 23 +++------ .../practicum/filmorate/dto/user/UserDto.java | 22 +++------ .../filmorate/mapper/EventMapper.java | 16 +++---- .../filmorate/mapper/FilmMapper.java | 26 +++++----- .../filmorate/mapper/ReviewMapper.java | 17 ++++--- .../filmorate/mapper/UserMapper.java | 13 +++-- .../repository/feed/EventRowMapper.java | 4 +- .../repository/feed/JdbcFeedRepository.java | 4 +- .../repository/film/JdbcFilmRepository.java | 6 +-- src/main/resources/schema.sql | 2 +- 20 files changed, 154 insertions(+), 136 deletions(-) create mode 100644 Dockerfile create mode 100644 docker-compose.yaml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..183ee28 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,48 @@ +FROM maven:3.9.11-amazoncorretto-21-alpine AS builder + +# Set working directory +WORKDIR /app + +COPY pom.xml ./ +COPY lombok.config ./ + +# Set up Maven local repository for caching +ENV MAVEN_OPTS="-Dmaven.repo.local=/app/.m2/repository" + +# Download dependencies (caching layer) +RUN mvn dependency:go-offline -B + +# Copy source code +COPY src ./src + +# Build the application (skip tests for faster build) +RUN mvn clean package -DskipTests -Dcheckstyle.skip=true + +# Stage 2: Layers stage +FROM amazoncorretto:21.0.8-alpine AS layers +WORKDIR /app +COPY --from=builder /app/target/*.jar app.jar +# Extract layers from the JAR for better caching in runtime +RUN java -Djarmode=layertools -jar app.jar extract + +# Stage 3: Runtime stage +FROM amazoncorretto:21.0.8-alpine + +# Set non-root user for security +RUN addgroup -g 1001 appgroup && adduser -u 1001 -G appgroup -s /bin/sh -D appuser +USER appuser + +# Set working directory +WORKDIR /app + +# Copy extracted layers from builder stage +COPY --from=layers /app/dependencies/ ./ +COPY --from=layers /app/spring-boot-loader/ ./ +COPY --from=layers /app/snapshot-dependencies/ ./ +COPY --from=layers /app/application/ ./ + +# Expose the application port +EXPOSE 8080 + +# Entry point to run the application +ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"] diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..086972f --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,13 @@ +services: + filmorate: + build: + context: . + dockerfile: Dockerfile + ports: + - "8080:8080" + volumes: + - maven-repo:/app/.m2/repository + - ./db:/app/db + +volumes: + maven-repo: {} diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/DirectorController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/DirectorController.java index b8ffea9..1cb9e1e 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/DirectorController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/DirectorController.java @@ -36,14 +36,12 @@ public DirectorDto findById(@PathVariable @Positive long id) { @PostMapping @ResponseStatus(HttpStatus.CREATED) - @Validated public DirectorDto create(@RequestBody @Valid NewDirectorRequest request) { log.trace("Create new director requested: {}", request); return directorService.create(request); } @PutMapping - @Validated public DirectorDto update(@RequestBody @Valid UpdateDirectorRequest request) { log.trace("Update director requested: {}", request); return directorService.update(request); diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java index a86e957..d82a7e3 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -44,14 +44,12 @@ public void deleteById(@PathVariable @Positive long filmId) { @PostMapping @ResponseStatus(HttpStatus.CREATED) - @Validated public FilmDto create(@RequestBody @Valid NewFilmRequest request) { log.trace("Create new film requested {}", request); return filmService.create(request); } @PutMapping - @Validated public FilmDto update(@RequestBody @Valid UpdateFilmRequest request) { log.trace("Update film requested {}", request); return filmService.update(request); @@ -76,13 +74,8 @@ public Collection searchFilms(@RequestParam @NotBlank String query, @RequestParam(defaultValue = "title") String by) { log.trace("Search films requested with query: {}, by: {}", query, by); - List parsedInput = SearchFilmsBy.parseStr(by); - - for (SearchFilmsBy searchFilmsBy : parsedInput) { - if (searchFilmsBy == null) { - throw new IllegalArgumentException("Unknown search params %s".formatted(by)); - } - } + List parsedInput = SearchFilmsBy.parseStrOrThrow(by, () -> + new IllegalArgumentException("Invalid search films requested: " + by)); return filmService.searchFilms(query, parsedInput); } @@ -106,10 +99,8 @@ public Collection findPopular(@RequestParam(defaultValue = "10") @Posit public Collection findFilmsOfDirector(@PathVariable @Positive long directorId, @RequestParam String sortBy) { - FilmsSortBy sortFilmsBy = FilmsSortBy.fromString(sortBy); - if (sortFilmsBy == null) { - throw new IllegalArgumentException("invalid sort by: %s".formatted(sortBy)); - } + FilmsSortBy sortFilmsBy = FilmsSortBy.fromString(sortBy).orElseThrow( + () -> new IllegalArgumentException("invalid sort by: %s".formatted(sortBy))); log.trace("Find films of director with id {} requested", directorId); return filmService.findFilmsOfDirector(directorId, sortFilmsBy); diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmsSortBy.java b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmsSortBy.java index 0fac786..dc2fda2 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmsSortBy.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmsSortBy.java @@ -1,14 +1,16 @@ package ru.yandex.practicum.filmorate.controller; +import java.util.Optional; + public enum FilmsSortBy { YEAR, LIKES; - public static FilmsSortBy fromString(String sortBy) { + public static Optional fromString(String sortBy) { return switch (sortBy.toLowerCase()) { - case "year" -> YEAR; - case "likes" -> LIKES; - default -> null; + case "year" -> Optional.of(YEAR); + case "likes" -> Optional.of(LIKES); + default -> Optional.empty(); }; } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/SearchFilmsBy.java b/src/main/java/ru/yandex/practicum/filmorate/controller/SearchFilmsBy.java index 2e598f8..99c1434 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/SearchFilmsBy.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/SearchFilmsBy.java @@ -2,8 +2,11 @@ import lombok.Getter; +import java.awt.*; import java.util.ArrayList; import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; @Getter public enum SearchFilmsBy { @@ -16,23 +19,24 @@ public enum SearchFilmsBy { this.value = value; } - public static List parseStr(String value) { + public static List parseStrOrThrow(String value, + Supplier exceptionSupplier) throws T { String[] splittedStr = value.trim().toLowerCase().split(","); List parsedList = new ArrayList<>(); for (String str : splittedStr) { - SearchFilmsBy searchFilmsBy = SearchFilmsBy.fromString(str); - parsedList.add(searchFilmsBy); + parsedList.add(SearchFilmsBy.fromString(str) + .orElseThrow(exceptionSupplier)); } return parsedList; } - public static SearchFilmsBy fromString(String value) { + public static Optional fromString(String value) { return switch (value) { - case "title" -> SearchFilmsBy.TITLE; - case "director" -> SearchFilmsBy.DIRECTOR; - default -> null; + case "title" -> Optional.of(TITLE); + case "director" -> Optional.of(DIRECTOR); + default -> Optional.empty(); }; } diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java index 397b773..a4ca3c7 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java @@ -46,14 +46,12 @@ public void deleteById(@PathVariable @Positive long userId) { @PostMapping @ResponseStatus(HttpStatus.CREATED) - @Validated public UserDto create(@RequestBody @Valid NewUserRequest user) { log.trace("create new user requested {}", user); return userService.create(user); } @PutMapping - @Validated public UserDto update(@RequestBody @Valid UpdateUserRequest user) { log.trace("Update user requested {}", user); return userService.update(user); diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/director/NewDirectorRequest.java b/src/main/java/ru/yandex/practicum/filmorate/dto/director/NewDirectorRequest.java index cf7c130..8f118b8 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dto/director/NewDirectorRequest.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/director/NewDirectorRequest.java @@ -1,9 +1,12 @@ package ru.yandex.practicum.filmorate.dto.director; import jakarta.validation.constraints.NotBlank; +import lombok.AccessLevel; import lombok.Data; +import lombok.experimental.FieldDefaults; @Data +@FieldDefaults(level = AccessLevel.PRIVATE) public class NewDirectorRequest { @NotBlank String name; diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/event/EventDto.java b/src/main/java/ru/yandex/practicum/filmorate/dto/event/EventDto.java index bcd9a40..306c8f6 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dto/event/EventDto.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/event/EventDto.java @@ -1,18 +1,13 @@ package ru.yandex.practicum.filmorate.dto.event; -import lombok.Builder; -import lombok.Data; import ru.yandex.practicum.filmorate.model.EventType; import ru.yandex.practicum.filmorate.model.Operation; -@Data -@Builder -public class EventDto { - private Long timestamp; - private Long userId; - private EventType eventType; - private Operation operation; - private Long eventId; - private Long entityId; +public record EventDto(Long timestamp, + Long userId, + EventType eventType, + Operation operation, + Long eventId, + Long entityId) { } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/film/FilmDto.java b/src/main/java/ru/yandex/practicum/filmorate/dto/film/FilmDto.java index 2536314..58753ed 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dto/film/FilmDto.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/film/FilmDto.java @@ -1,10 +1,5 @@ package ru.yandex.practicum.filmorate.dto.film; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Data; -import lombok.Singular; -import lombok.experimental.FieldDefaults; import ru.yandex.practicum.filmorate.dto.director.DirectorDto; import ru.yandex.practicum.filmorate.dto.genre.GenreDto; import ru.yandex.practicum.filmorate.dto.mparating.MPARatingDto; @@ -12,17 +7,13 @@ import java.time.LocalDate; import java.util.List; -@Data -@Builder -@FieldDefaults(level = AccessLevel.PRIVATE) -public class FilmDto { - Long id; - String name; - String description; - LocalDate releaseDate; - Integer duration; - MPARatingDto mpa; - @Singular - List genres; - List directors; +public record FilmDto( + Long id, + String name, + String description, + LocalDate releaseDate, + Integer duration, + MPARatingDto mpa, + List genres, + List directors) { } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/review/ReviewDto.java b/src/main/java/ru/yandex/practicum/filmorate/dto/review/ReviewDto.java index 0c61df4..62c89a2 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dto/review/ReviewDto.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/review/ReviewDto.java @@ -1,19 +1,10 @@ package ru.yandex.practicum.filmorate.dto.review; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Data; -import lombok.experimental.FieldDefaults; - -@Data -@Builder -@FieldDefaults(level = AccessLevel.PRIVATE) -public class ReviewDto { - Long reviewId; - String content; - Boolean isPositive; - Long userId; - Long filmId; - Long useful; +public record ReviewDto( + Long reviewId, + String content, + Boolean isPositive, + Long userId, + Long filmId, + Long useful) { } - diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/user/UserDto.java b/src/main/java/ru/yandex/practicum/filmorate/dto/user/UserDto.java index 531d172..edd64ff 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dto/user/UserDto.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/user/UserDto.java @@ -1,21 +1,11 @@ package ru.yandex.practicum.filmorate.dto.user; -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Data; -import lombok.experimental.FieldDefaults; - import java.time.LocalDate; -@Data -@Builder -@FieldDefaults(level = AccessLevel.PRIVATE) -public class UserDto { - @JsonProperty(access = JsonProperty.Access.READ_ONLY) - long id; - String email; - String login; - String name; - LocalDate birthday; +public record UserDto( + long id, + String email, + String login, + String name, + LocalDate birthday) { } diff --git a/src/main/java/ru/yandex/practicum/filmorate/mapper/EventMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mapper/EventMapper.java index 9ac3f6f..d02e99e 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/mapper/EventMapper.java +++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/EventMapper.java @@ -8,14 +8,12 @@ public class EventMapper { public EventDto toEventDto(Event event) { - return EventDto.builder() - .timestamp(event.getTimestamp()) - .userId(event.getUserId()) - .eventType(event.getEventType()) - .operation(event.getOperation()) - .eventId(event.getEventId()) - .entityId(event.getEntityId()) - .build(); + return new EventDto( + event.getTimestamp(), + event.getUserId(), + event.getEventType(), + event.getOperation(), + event.getEventId(), + event.getEntityId()); } } - diff --git a/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmMapper.java index b97e912..3a048bb 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmMapper.java +++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmMapper.java @@ -4,28 +4,26 @@ import ru.yandex.practicum.filmorate.dto.film.FilmDto; import ru.yandex.practicum.filmorate.dto.film.NewFilmRequest; import ru.yandex.practicum.filmorate.dto.film.UpdateFilmRequest; -import ru.yandex.practicum.filmorate.dto.genre.GenreDto; import ru.yandex.practicum.filmorate.model.Film; @UtilityClass public class FilmMapper { public FilmDto toFilmDto(Film film) { - return FilmDto.builder() - .id(film.getId()) - .description(film.getDescription()) - .name(film.getName()) - .releaseDate(film.getReleaseDate()) - .duration(film.getDuration()) - .mpa(MPARatingMapper.toMPARatingDto(film.getMpaRating())) - .genres(film.getGenres() + return new FilmDto( + film.getId(), + film.getName(), + film.getDescription(), + film.getReleaseDate(), + film.getDuration(), + MPARatingMapper.toMPARatingDto(film.getMpaRating()), + film.getGenres() .stream() - .map(genre -> new GenreDto(genre.getId(), genre.getName())) - .toList()) - .directors(film.getDirectors() + .map(GenreMapper::toGenreDto) + .toList(), + film.getDirectors() .stream() .map(DirectorMapper::toDirectorDto) - .toList()) - .build(); + .toList()); } public Film toFilm(NewFilmRequest request) { diff --git a/src/main/java/ru/yandex/practicum/filmorate/mapper/ReviewMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mapper/ReviewMapper.java index c51ba37..1911f92 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/mapper/ReviewMapper.java +++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/ReviewMapper.java @@ -9,14 +9,13 @@ @UtilityClass public class ReviewMapper { public ReviewDto toDto(Review review) { - return ReviewDto.builder() - .reviewId(review.getId()) - .content(review.getContent()) - .isPositive(review.getIsPositive()) - .userId(review.getUserId()) - .filmId(review.getFilmId()) - .useful(review.getUsefulRating()) - .build(); + return new ReviewDto( + review.getId(), + review.getContent(), + review.getIsPositive(), + review.getUserId(), + review.getFilmId(), + review.getUsefulRating()); } public Review toReview(NewReviewRequest request) { @@ -37,4 +36,4 @@ public Review updateReviewFields(Review review, UpdateReviewRequest request) { } return review; } -} \ No newline at end of file +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/mapper/UserMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mapper/UserMapper.java index 099ad58..cc1391a 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/mapper/UserMapper.java +++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/UserMapper.java @@ -10,13 +10,12 @@ @UtilityClass public class UserMapper { public UserDto toUserDto(User user) { - return UserDto.builder() - .id(user.getId()) - .email(user.getEmail()) - .login(user.getLogin()) - .name(user.getName()) - .birthday(user.getBirthday()) - .build(); + return new UserDto( + user.getId(), + user.getEmail(), + user.getLogin(), + user.getName(), + user.getBirthday()); } public User mapToUser(NewUserRequest request) { diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/feed/EventRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/repository/feed/EventRowMapper.java index de91215..68994d6 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/repository/feed/EventRowMapper.java +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/feed/EventRowMapper.java @@ -15,11 +15,11 @@ public class EventRowMapper implements RowMapper { public Event mapRow(ResultSet rs, int rowNum) throws SQLException { return Event.builder() .eventId(rs.getLong("event_id")) - .timestamp(rs.getLong("timestamp")) + .timestamp(rs.getLong("created_at")) .userId(rs.getLong("user_id")) .eventType(EventType.valueOf(rs.getString("event_type"))) .operation(Operation.valueOf(rs.getString("operation"))) .entityId(rs.getLong("entity_id")) .build(); } -} \ No newline at end of file +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/feed/JdbcFeedRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/feed/JdbcFeedRepository.java index c2b17c2..cffbeda 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/repository/feed/JdbcFeedRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/feed/JdbcFeedRepository.java @@ -17,7 +17,7 @@ public class JdbcFeedRepository implements FeedRepository { @Override public List findByUserId(long userId) { - String sql = "SELECT * FROM feed_events WHERE user_id = :user_id ORDER BY timestamp ASC"; + String sql = "SELECT * FROM feed_events WHERE user_id = :user_id ORDER BY created_at ASC"; return jdbc.query(sql, new MapSqlParameterSource("user_id", userId), eventRowMapper); @@ -33,7 +33,7 @@ public Event save(Event event) { .addValue("operation", event.getOperation().toString()) .addValue("entity_id", event.getEntityId()); String sql = """ - INSERT INTO feed_events (timestamp, user_id, event_type, operation, entity_id) + INSERT INTO feed_events (created_at, user_id, event_type, operation, entity_id) VALUES (:timestamp, :user_id, :event_type, :operation, :entity_id) """; jdbc.update(sql, params, keyHolder, new String[]{"event_id"}); diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java index 253b22a..0f3b4d7 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/film/JdbcFilmRepository.java @@ -125,7 +125,7 @@ public Collection findTopPopularFilms(int count, Integer genreId, Integer WHERE (g.genre_id = :genreId OR :genreId IS NULL) AND (YEAR(f.release_date) =:year OR :year IS NULL) GROUP BY f.film_id - ORDER BY COUNT(fl.user_id) DESC, f.film_id + ORDER BY COUNT(DISTINCT fl.user_id) DESC, f.film_id LIMIT :count"""); MapSqlParameterSource params = new MapSqlParameterSource() .addValue("count", count) @@ -147,7 +147,7 @@ WHERE f.film_id in (SELECT l1.film_id FROM likes l2 WHERE l2.user_id = :friend_id) GROUP BY f.film_id, g.genre_id - ORDER BY COUNT(fl.user_id) DESC, f.film_id + ORDER BY COUNT(DISTINCT fl.user_id) DESC, f.film_id """); MapSqlParameterSource params = new MapSqlParameterSource() .addValue("user_id", userId) @@ -160,7 +160,7 @@ ORDER BY COUNT(fl.user_id) DESC, f.film_id public Collection findFilmsOfDirector(long directorId, FilmsSortBy sortFilmsBy) { String sortBySql = switch (sortFilmsBy) { case YEAR -> "EXTRACT(YEAR from f.release_date)"; - case LIKES -> "COUNT(fl.user_id) DESC"; + case LIKES -> "COUNT(DISTINCT fl.user_id) DESC"; }; String selectFilmsOfDirectorSortedSql = BASE_SELECT_SQL.concat(""" diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 40ecaab..0dc4b91 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -105,7 +105,7 @@ CREATE TABLE IF NOT EXISTS review_likes CREATE TABLE IF NOT EXISTS feed_events ( event_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - timestamp BIGINT NOT NULL, + created_at BIGINT NOT NULL, user_id BIGINT NOT NULL, event_type ENUM('LIKE', 'REVIEW', 'FRIEND') NOT NULL, operation ENUM('REMOVE', 'ADD', 'UPDATE') NOT NULL, From 346cbbf219bb242b253c48cbe7bdc44e9caef986 Mon Sep 17 00:00:00 2001 From: Nikolay Aleksandrov Date: Tue, 21 Oct 2025 16:48:09 +0300 Subject: [PATCH 13/13] Refractor reviews (#28) * fix: refractor likes methods * fix: edited sql in updateUsefulRating method --- .../controller/ReviewController.java | 7 +- .../practicum/filmorate/model/Reaction.java | 5 ++ .../review/JdbcReviewRepository.java | 74 +++++-------------- .../repository/review/ReviewRepository.java | 9 +-- .../service/review/ReviewService.java | 7 +- .../service/review/ReviewServiceImpl.java | 28 +++---- 6 files changed, 44 insertions(+), 86 deletions(-) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/model/Reaction.java diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/ReviewController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/ReviewController.java index d0489de..a081a78 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/ReviewController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/ReviewController.java @@ -10,6 +10,7 @@ import ru.yandex.practicum.filmorate.dto.review.NewReviewRequest; import ru.yandex.practicum.filmorate.dto.review.ReviewDto; import ru.yandex.practicum.filmorate.dto.review.UpdateReviewRequest; +import ru.yandex.practicum.filmorate.model.Reaction; import ru.yandex.practicum.filmorate.service.review.ReviewService; import java.util.Collection; @@ -72,13 +73,13 @@ public void setDislike(@PathVariable @Positive long id, public void removeLike(@PathVariable @Positive long id, @PathVariable @Positive long userId) { log.trace("Remove review like by reviewId: {}, from userId: {}", id, userId); - reviewService.removeLike(id, userId); + reviewService.removeReaction(id, userId, Reaction.LIKE); } @DeleteMapping("/{id}/dislike/{userId}") public void removeDislike(@PathVariable @Positive long id, @PathVariable @Positive long userId) { log.trace("Remove review dislike by reviewId: {}, from userId: {}", id, userId); - reviewService.removeDislike(id, userId); + reviewService.removeReaction(id, userId, Reaction.DISLIKE); } -} +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Reaction.java b/src/main/java/ru/yandex/practicum/filmorate/model/Reaction.java new file mode 100644 index 0000000..a7b7603 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Reaction.java @@ -0,0 +1,5 @@ +package ru.yandex.practicum.filmorate.model; + +public enum Reaction { + LIKE, DISLIKE +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/review/JdbcReviewRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/review/JdbcReviewRepository.java index 8bbb5bc..4df544b 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/repository/review/JdbcReviewRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/review/JdbcReviewRepository.java @@ -6,6 +6,7 @@ import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.jdbc.support.GeneratedKeyHolder; import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.model.Reaction; import ru.yandex.practicum.filmorate.model.Review; import java.util.Collection; @@ -25,10 +26,6 @@ public class JdbcReviewRepository implements ReviewRepository { FROM reviews r JOIN film_reviews fr ON r.review_id = fr.review_id """; - private static final int UP_UR = 1; - private static final int DOUBLE_UP_UR = 2; - private static final int DROP_UR = -1; - private static final int DOUBLE_DROP_UR = -2; private final NamedParameterJdbcOperations jdbc; private final RowMapper mapper; @@ -95,59 +92,30 @@ public Collection findAllByFilm(Long filmId, long count) { } @Override - public void setLike(long id, long userId) { + public void setReaction(long id, long userId, Reaction reaction) { MapSqlParameterSource params = new MapSqlParameterSource("review_id", id) .addValue("user_id", userId); - Boolean isPositiveYet = isPositive(params); - jdbc.update(""" - MERGE INTO review_likes (review_id, user_id, is_positive) - KEY (review_id, user_id) - VALUES (:review_id, :user_id, TRUE) - """, params); - if (isPositiveYet == null) { - changeUsefulRating(params, UP_UR); - } else if (isPositiveYet) { - changeUsefulRating(params, DOUBLE_UP_UR); + switch (reaction) { + case LIKE -> params.addValue("is_positive", true); + case DISLIKE -> params.addValue("is_positive", false); } - } - - @Override - public void setDislike(long id, long userId) { - MapSqlParameterSource params = new MapSqlParameterSource("review_id", id) - .addValue("user_id", userId); - Boolean isPositiveYet = isPositive(params); jdbc.update(""" MERGE INTO review_likes (review_id, user_id, is_positive) KEY (review_id, user_id) - VALUES (:review_id, :user_id, FALSE) - """, params); - if (isPositiveYet == null) { - changeUsefulRating(params, DROP_UR); - } else if (isPositiveYet) { - changeUsefulRating(params, DOUBLE_DROP_UR); - } - } - - @Override - public void removeLike(long id, long userId) { - MapSqlParameterSource params = new MapSqlParameterSource("review_id", id) - .addValue("user_id", userId); - jdbc.update(""" - DELETE FROM review_likes - WHERE review_id = :review_id AND user_id = :user_id + VALUES (:review_id, :user_id, :is_positive) """, params); - changeUsefulRating(params, DROP_UR); + updateUsefulRating(params); } @Override - public void removeDislike(long id, long userId) { + public void removeReaction(long id, long userId) { MapSqlParameterSource params = new MapSqlParameterSource("review_id", id) .addValue("user_id", userId); jdbc.update(""" DELETE FROM review_likes WHERE review_id = :review_id AND user_id = :user_id """, params); - changeUsefulRating(params, UP_UR); + updateUsefulRating(params); } private MapSqlParameterSource getBaseParams(Review review) { @@ -156,21 +124,15 @@ private MapSqlParameterSource getBaseParams(Review review) { .addValue("is_Positive", review.getIsPositive()); } - private Boolean isPositive(MapSqlParameterSource params) { - List list = jdbc.query(""" - SELECT is_positive - FROM review_likes - WHERE review_id = :review_id AND user_id = :user_id - """, params, (rs, rowNum) -> rs.getBoolean("is_positive")); - return list.isEmpty() ? null : list.getFirst(); - } - - private void changeUsefulRating(MapSqlParameterSource params, int points) { - params.addValue("points", points); + private void updateUsefulRating(MapSqlParameterSource params) { jdbc.update(""" - UPDATE reviews - SET useful_rating = useful_rating + :points - WHERE review_id = :review_id + UPDATE reviews r + SET useful_rating = COALESCE(( + SELECT SUM(CASE WHEN is_positive THEN 1 ELSE -1 END) + FROM review_likes rl + WHERE rl.review_id = r.review_id), + 0) + WHERE r.review_id = :review_id; """, params); } -} +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/review/ReviewRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/review/ReviewRepository.java index e1bca95..cf91540 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/repository/review/ReviewRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/review/ReviewRepository.java @@ -1,5 +1,6 @@ package ru.yandex.practicum.filmorate.repository.review; +import ru.yandex.practicum.filmorate.model.Reaction; import ru.yandex.practicum.filmorate.model.Review; import java.util.Collection; @@ -16,11 +17,7 @@ public interface ReviewRepository { Collection findAllByFilm(Long filmId, long count); - void setLike(long id, long userId); + void setReaction(long id, long userId, Reaction reaction); - void setDislike(long id, long userId); - - void removeLike(long id, long userId); - - void removeDislike(long id, long userId); + void removeReaction(long id, long userId); } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/review/ReviewService.java b/src/main/java/ru/yandex/practicum/filmorate/service/review/ReviewService.java index 08a2212..10a346a 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/review/ReviewService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/review/ReviewService.java @@ -3,6 +3,7 @@ import ru.yandex.practicum.filmorate.dto.review.NewReviewRequest; import ru.yandex.practicum.filmorate.dto.review.ReviewDto; import ru.yandex.practicum.filmorate.dto.review.UpdateReviewRequest; +import ru.yandex.practicum.filmorate.model.Reaction; import java.util.Collection; @@ -22,7 +23,5 @@ public interface ReviewService { void setDislike(long id, long userId); - void removeLike(long id, long userId); - - void removeDislike(long id, long userId); -} + void removeReaction(long id, long userId, Reaction reaction); +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/review/ReviewServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/review/ReviewServiceImpl.java index d6e29b1..701cce3 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/review/ReviewServiceImpl.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/review/ReviewServiceImpl.java @@ -3,13 +3,12 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import ru.yandex.practicum.filmorate.dto.review.NewReviewRequest; -import ru.yandex.practicum.filmorate.dto.review.ReviewDto; -import ru.yandex.practicum.filmorate.dto.review.UpdateReviewRequest; +import ru.yandex.practicum.filmorate.dto.review.*; import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.mapper.ReviewMapper; import ru.yandex.practicum.filmorate.model.EventType; import ru.yandex.practicum.filmorate.model.Operation; +import ru.yandex.practicum.filmorate.model.Reaction; import ru.yandex.practicum.filmorate.model.Review; import ru.yandex.practicum.filmorate.repository.film.FilmRepository; import ru.yandex.practicum.filmorate.repository.review.ReviewRepository; @@ -77,7 +76,7 @@ public void setLike(long id, long userId) { getReviewOrThrow(id); throwIfUserNotFound(userId); log.info("Like to review {} has been added by user {}", id, userId); - reviewRepository.setLike(id, userId); + reviewRepository.setReaction(id, userId, Reaction.LIKE); } @Override @@ -85,23 +84,18 @@ public void setDislike(long id, long userId) { getReviewOrThrow(id); throwIfUserNotFound(userId); log.info("Dislike to review {} has been added by user {}", id, userId); - reviewRepository.setDislike(id, userId); + reviewRepository.setReaction(id, userId, Reaction.DISLIKE); } @Override - public void removeLike(long id, long userId) { + public void removeReaction(long id, long userId, Reaction reaction) { getReviewOrThrow(id); throwIfUserNotFound(userId); - log.info("Like to review {} has been removed by user {}", id, userId); - reviewRepository.removeLike(id, userId); - } - - @Override - public void removeDislike(long id, long userId) { - getReviewOrThrow(id); - throwIfUserNotFound(userId); - log.info("Dislike to review {} has been removed by user {}", id, userId); - reviewRepository.removeDislike(id, userId); + switch (reaction) { + case LIKE -> log.info("Like to review {} has been removed by user {}", id, userId); + case DISLIKE -> log.info("Dislike to review {} has been removed by user {}", id, userId); + } + reviewRepository.removeReaction(id, userId); } private Review getReviewOrThrow(long id) { @@ -118,4 +112,4 @@ private void throwIfFilmNotFound(long id) { filmRepository.findById(id) .orElseThrow(NotFoundException.supplier("Film with filmId %d not found", id)); } -} +} \ No newline at end of file