From e07b41fd3f437a407a2f7ffd3aa92a4144ed3ec3 Mon Sep 17 00:00:00 2001 From: b1mgd Date: Thu, 24 Apr 2025 20:03:34 +0300 Subject: [PATCH 1/9] Add remove endpoint for films and users (#14) --- .gitignore | 3 +++ .../filmorate/controller/ErrorHandler.java | 7 +++++++ .../filmorate/controller/FilmController.java | 7 +++++++ .../filmorate/controller/UserController.java | 7 +++++++ .../practicum/filmorate/dal/FilmRepository.java | 5 +++++ .../practicum/filmorate/dal/UserRepository.java | 5 +++++ .../exception/InternalServerException.java | 7 +++++++ .../filmorate/service/film/FilmService.java | 15 +++++++++++++++ .../filmorate/service/user/UserService.java | 16 ++++++++++++++++ .../filmorate/storage/film/FilmDbStorage.java | 5 +++++ .../filmorate/storage/film/FilmStorage.java | 2 ++ .../filmorate/storage/user/UserDbStorage.java | 5 +++++ .../filmorate/storage/user/UserStorage.java | 2 ++ 13 files changed, 86 insertions(+) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/exception/InternalServerException.java diff --git a/.gitignore b/.gitignore index d7268a3..97fa19e 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,9 @@ target/ !**/src/main/**/target/ !**/src/test/**/target/ +### macOS ### +.DS_Store + ### STS ### .apt_generated .classpath diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/ErrorHandler.java b/src/main/java/ru/yandex/practicum/filmorate/controller/ErrorHandler.java index 86a12a3..95cfe67 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/ErrorHandler.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/ErrorHandler.java @@ -38,4 +38,11 @@ public ErrorResponse handleConstraintViolation(final ConstraintViolationExceptio log.warn("constraint violation"); return new ErrorResponse(e.getMessage()); } + + @ExceptionHandler(InternalServerException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ErrorResponse handleException(Exception e) { + log.error("Ошибка сервера: {}", e.getMessage()); + return new ErrorResponse("Внутренняя ошибка сервера: " + e.getMessage()); + } } 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 79be9ce..1ab750a 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -67,4 +67,11 @@ public List getTopFilms(@RequestParam(defaultValue = "10") @Positive(me log.info("Received GET /films/popular?count={} request", count); return filmService.getTopFilms(count); } + + @DeleteMapping("/{filmId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteFilm(@PathVariable long filmId) { + log.info("Received DELETE /films/{} request", filmId); + filmService.deleteFilm(filmId); + } } \ 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 1c16497..d5c7121 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java @@ -72,5 +72,12 @@ public Collection getFriends(@PathVariable int id) { log.info("getFriends"); return userService.getFriends(id); } + + @DeleteMapping("/{userId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteUser(@PathVariable int userId) { + log.info("Recieved DELETE /users/{} request", userId); + userService.deleteUser(userId); + } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/FilmRepository.java b/src/main/java/ru/yandex/practicum/filmorate/dal/FilmRepository.java index 4adda28..fc8612d 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/FilmRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/FilmRepository.java @@ -36,6 +36,7 @@ public class FilmRepository extends BaseRepository { "FROM genre g " + "JOIN film_genre fg ON g.id = fg.genre_id " + "WHERE fg.film_id IN (:filmIds)"; + private static final String DELETE_FILM_QUERY = "DELETE FROM Films WHERE id = ?"; private final JdbcTemplate jdbc; private final NamedParameterJdbcTemplate namedJdbcTemplate; @@ -172,6 +173,10 @@ public Film update(Film film) { return findById(film.getId()).orElseThrow(() -> new IllegalStateException("Updated film not found, id: " + film.getId())); } + public boolean deleteFilm(long id) { + return delete(DELETE_FILM_QUERY, id); + } + private void deleteGenres(long filmId) { String sql = "DELETE FROM film_genre WHERE film_id = ?"; jdbc.update(sql, filmId); diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/UserRepository.java b/src/main/java/ru/yandex/practicum/filmorate/dal/UserRepository.java index e043ce3..317b7bc 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/UserRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/UserRepository.java @@ -15,6 +15,7 @@ public class UserRepository extends BaseRepository { private static final String FIND_BY_LOGIN_QUERY = "SELECT * FROM users WHERE login = ?"; private static final String INSERT_QUERY = "INSERT INTO users(email, login, name, birthday) VALUES (?, ?, ?, ?)"; private static final String UPDATE_QUERY = "UPDATE users SET email = ?, login = ?, name = ?, birthday = ? WHERE id = ?"; + private static final String DELETE_USER_QUERY = "DELETE FROM users WHERE id = ?"; public UserRepository(JdbcTemplate jdbc, UserRowMapper mapper) { super(jdbc, mapper); @@ -60,6 +61,10 @@ public User update(User user) { return user; } + public boolean deleteUser(long id) { + return delete(DELETE_USER_QUERY, id); + } + public Set getFriends(long userId) { String sql = "SELECT friend_id FROM friendship WHERE user_id = ?"; diff --git a/src/main/java/ru/yandex/practicum/filmorate/exception/InternalServerException.java b/src/main/java/ru/yandex/practicum/filmorate/exception/InternalServerException.java new file mode 100644 index 0000000..c9eba3a --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/exception/InternalServerException.java @@ -0,0 +1,7 @@ +package ru.yandex.practicum.filmorate.exception; + +public class InternalServerException extends RuntimeException { + public InternalServerException(String message) { + super(message); + } +} 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 4f83d6e..cf96bb6 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 @@ -9,6 +9,7 @@ import ru.yandex.practicum.filmorate.dto.GenreDto; import ru.yandex.practicum.filmorate.dto.RatingDto; import ru.yandex.practicum.filmorate.exception.ConstraintViolationException; +import ru.yandex.practicum.filmorate.exception.InternalServerException; import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.mappers.FilmMapper; import ru.yandex.practicum.filmorate.mappers.GenreMapper; @@ -134,6 +135,20 @@ public RatingDto getRatingById(int id) { return RatingMapper.mapToRatingDto(rating); } + @Transactional + public void deleteFilm(long filmId) { + FilmDto filmDto = getFilmById(filmId); + log.info("Deleting film: {}", filmDto); + + boolean isDeleted = filmStorage.deleteFilm(filmId); + + if (isDeleted) { + log.info("Film deleted successfully"); + } else { + throw new InternalServerException("Film was not deleted due to internal error."); + } + } + private void validateFilm(Film film) { if (film.getReleaseDate() != null && film.getReleaseDate().isBefore(MIN_RELEASE_DATE)) { log.warn("Validation failed"); diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/user/UserService.java b/src/main/java/ru/yandex/practicum/filmorate/service/user/UserService.java index c8f052f..8f641bd 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/user/UserService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/user/UserService.java @@ -4,10 +4,12 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import ru.yandex.practicum.filmorate.dto.UserDto; import ru.yandex.practicum.filmorate.exception.EmptyFieldException; +import ru.yandex.practicum.filmorate.exception.InternalServerException; import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.mappers.UserMapper; import ru.yandex.practicum.filmorate.model.User; @@ -128,6 +130,20 @@ public UserDto getUserById(@PathVariable long id) { return UserMapper.mapToUserDto(userDbStorage.getUserById(id)); } + @Transactional + public void deleteUser(long userId) { + UserDto userDto = getUserById(userId); + log.info("Deleting user: {}", userDto); + + boolean isDeleted = userDbStorage.deleteUser(userId); + + if (isDeleted) { + log.info("User deleted successfully"); + } else { + throw new InternalServerException("User was not deleted due to internal server error"); + } + } + private boolean isExistsUser(User user) { return userDbStorage.getUsers().stream() .noneMatch(userCheck -> userCheck.getId() == user.getId()); diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java index 5cb7f56..8c93be5 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java @@ -61,6 +61,11 @@ public Film getFilmById(long filmId) { .orElseThrow(() -> new NotFoundException("Film with id " + filmId + " not found")); } + @Override + public boolean deleteFilm(long filmId) { + return filmRepository.deleteFilm(filmId); + } + public Set getLikes(long filmId) { getFilmById(filmId); return filmRepository.getLikes(filmId); diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java index 4d2f3d5..2d9ad94 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java @@ -14,4 +14,6 @@ public interface FilmStorage { Film updateFilm(@RequestBody Film film); Film getFilmById(long filmId); + + boolean deleteFilm(long filmId); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java index 3b9222c..04242c0 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java @@ -38,6 +38,11 @@ public User getUserById(long userId) { return repository.findById(userId).orElseThrow(() -> new NotFoundException("User with id " + userId + " not found")); } + @Override + public boolean deleteUser(long userId) { + return repository.deleteUser(userId); + } + public Set getFriends(long userId) { return repository.getFriends(userId); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java index eaf2924..3c85c73 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java @@ -14,4 +14,6 @@ public interface UserStorage { User updateUser(@RequestBody User user); User getUserById(long userId); + + boolean deleteUser(long userId); } From bab42a54014cb06e0950abe51c3f62be97a0fed2 Mon Sep 17 00:00:00 2001 From: Stanislav Mun <146199590+Nesailormun@users.noreply.github.com> Date: Thu, 24 Apr 2025 20:04:06 +0300 Subject: [PATCH 2/9] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=BE=D0=BD?= =?UTF-8?q?=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=C2=AB=D0=9F?= =?UTF-8?q?=D0=BE=D0=BF=D1=83=D0=BB=D1=8F=D1=80=D0=BD=D1=8B=D0=B5=20=D1=84?= =?UTF-8?q?=D0=B8=D0=BB=D1=8C=D0=BC=D1=8B=C2=BB=20(#13)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Обновлен метод getTopFilms в цепочке FilmController->FilmService->FilmDbStorage->FilmRepository согласно новой функциональности (фильтрации по genreId и year) * Обновлен метод getTopFilms в цепочке FilmController->FilmService->FilmDbStorage->FilmRepository согласно новой функциональности (фильтрации по genreId и year) * делаю коммит псÐоскольку force push не триггерит пайплайн по всей видимости * Исправил логику работы метода getTopFilms(...) (параметры строки запроса genreId and year являются необязательными) --------- Co-authored-by: nesailormun --- pom.xml | 2 ++ .../filmorate/controller/FilmController.java | 8 +++-- .../filmorate/dal/FilmRepository.java | 32 +++++++++++++++++-- .../filmorate/service/film/FilmService.java | 17 ++++------ .../filmorate/storage/film/FilmDbStorage.java | 4 +++ src/main/resources/application.properties | 8 ++++- 6 files changed, 54 insertions(+), 17 deletions(-) diff --git a/pom.xml b/pom.xml index 0c428fa..4f7f5f6 100644 --- a/pom.xml +++ b/pom.xml @@ -43,6 +43,7 @@ org.projectlombok lombok + 1.18.30 true @@ -81,6 +82,7 @@ org.projectlombok lombok + 1.18.30 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 1ab750a..3961387 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -63,9 +63,11 @@ public void deleteLikeToFilm(@PathVariable long filmId, @PathVariable long userI } @GetMapping("/popular") - public List getTopFilms(@RequestParam(defaultValue = "10") @Positive(message = "Count must be positive") int count) { - log.info("Received GET /films/popular?count={} request", count); - return filmService.getTopFilms(count); + public List getTopFilms(@RequestParam(defaultValue = "10") @Positive(message = "Count must be positive") int count, + @RequestParam(defaultValue = "-1") int genreId, + @RequestParam(defaultValue = "-1") int year) { + log.info("Received GET /films/popular?count={}&genreId={}&year={} request", count, genreId, year); + return filmService.getTopFilms(count, genreId, year); } @DeleteMapping("/{filmId}") diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/FilmRepository.java b/src/main/java/ru/yandex/practicum/filmorate/dal/FilmRepository.java index fc8612d..1984622 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/FilmRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/FilmRepository.java @@ -94,7 +94,7 @@ public Optional findById(long id) { if (films.isEmpty()) { return Optional.empty(); } else { - Film film = films.get(0); + Film film = films.getFirst(); setGenresForFilms(List.of(film)); return Optional.of(film); } @@ -211,4 +211,32 @@ public void addLike(long filmId, long userId) { public void removeLike(long filmId, long userId) { jdbc.update(REMOVE_LIKE_QUERY, filmId, userId); } -} \ No newline at end of file + + public List getTopFilms(int count, Integer genreId, Integer year) { + + StringBuilder queryBuilder = new StringBuilder("SELECT f.*, r.rating_id AS mpa_id, r.rating_name AS mpa_name " + + "FROM films AS f JOIN rating AS r on f.rating_id = r.rating_id " + + "JOIN film_genre AS fg ON f.id = fg.film_id " + + "JOIN likes AS l ON f.id = l.film_id WHERE 1=1"); + + List filterParams = new ArrayList<>(); + + if (genreId != -1) { + queryBuilder.append(" AND fg.genre_id = ?"); + filterParams.add(genreId); + } + + if (year != -1) { + queryBuilder.append(" AND EXTRACT(YEAR FROM f.release_date) = ?"); + filterParams.add(year); + } + + queryBuilder.append(" GROUP BY f.id, r.rating_id, r.rating_name ORDER BY COUNT(l.user_id) DESC LIMIT ?;"); + filterParams.add(count); + + List films = jdbc.query(queryBuilder.toString(), filmWithRatingMapper, filterParams.toArray()); + setGenresForFilms(films); + return films; + } + +} 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 cf96bb6..8ce7663 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 @@ -23,7 +23,6 @@ import java.time.LocalDate; import java.util.Collection; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -90,23 +89,19 @@ public void deleteLikeToFilm(long filmId, long userId) { log.info("Like removed successfully"); } - public List getTopFilms(int count) { - log.info("Getting top {} popular films", count); + public List getTopFilms(int count, int genreId, int year) { + log.info("Getting top {} popular films with genreId = {} and release year = {}", count, genreId, year); if (count <= 0) { throw new ValidationException("Count parameter must be positive."); } - Collection allFilms = filmStorage.getFilms(); - return allFilms.stream() + return filmStorage.getTopFilms(count, genreId, year) + .stream() .map(film -> { Set likes = filmStorage.getLikes(film.getId()); - return Map.entry(film, likes.size()); + return FilmMapper.mapToFilmDto(film, likes); }) - .sorted(Map.Entry.comparingByValue().reversed()) - .limit(count) - .map(Map.Entry::getKey) - .map(film -> FilmMapper.mapToFilmDto(film, filmStorage.getLikes(film.getId()))) - .collect(Collectors.toList()); + .toList(); } public Collection getGenres() { diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java index 8c93be5..d67ae29 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java @@ -97,6 +97,10 @@ public Rating getRatingById(int id) { .orElseThrow(() -> new NotFoundException("Rating with id " + id + " not found")); } + public List getTopFilms(int count, int genreId, int year) { + return filmRepository.getTopFilms(count, genreId, year); + } + private void validateRatingExists(Rating mpa) { if (mpa == null || mpa.getId() == 0) { throw new IllegalArgumentException("Film Rating (MPA) is required."); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 1730186..73fd17b 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,7 @@ -logging.level.org.zalando.logbook:ERROR \ No newline at end of file +logging.level.org.zalando.logbook=TRACE +logging.level.ru.yandex.practicum.filmorate=INFO +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 From 7930dd3b14b0c829d1a54ff75e652aed2b7e55c0 Mon Sep 17 00:00:00 2001 From: 1Boolean1 <124202814+1Boolean1@users.noreply.github.com> Date: Thu, 24 Apr 2025 20:25:38 +0300 Subject: [PATCH 3/9] add getCommonFilms for user and friend (#10) * add getCommonFilms for user and friend * little change * checkstyle --- .../filmorate/controller/FilmController.java | 8 ++++ .../filmorate/dal/FilmRepository.java | 19 ++++++++++ .../practicum/filmorate/dto/UserDto.java | 1 + .../filmorate/service/film/FilmService.java | 15 ++++++++ .../filmorate/storage/film/FilmDbStorage.java | 5 +++ src/main/resources/data.sql | 37 +++++++++++++++---- 6 files changed, 78 insertions(+), 7 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 3961387..8baed43 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -76,4 +76,12 @@ public void deleteFilm(@PathVariable long filmId) { log.info("Received DELETE /films/{} request", filmId); filmService.deleteFilm(filmId); } + + @GetMapping("/common") + public List getCommonFilms( + @RequestParam long userId, + @RequestParam long friendId) { + log.info("GET common films for userId={} and friendId={}", userId, friendId); + return filmService.getCommonFilms(userId, friendId); + } } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/FilmRepository.java b/src/main/java/ru/yandex/practicum/filmorate/dal/FilmRepository.java index 1984622..042be2e 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/FilmRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/FilmRepository.java @@ -30,6 +30,17 @@ public class FilmRepository extends BaseRepository { private static final String GET_LIKES_QUERY = "SELECT user_id FROM Likes WHERE film_id = ?"; private static final String ADD_LIKE_QUERY = "INSERT INTO Likes (user_id, film_id) VALUES (?, ?)"; private static final String REMOVE_LIKE_QUERY = "DELETE FROM Likes WHERE film_id = ? AND user_id = ?"; + private static final String GET_COMMON_FILMS = "SELECT f.*, r.rating_id as mpa_id, r.rating_name as mpa_name " + + "FROM Films f " + + "JOIN rating AS r ON f.rating_id = r.rating_id " + + "JOIN Likes l ON f.id = l.film_id " + + "WHERE l.film_id IN ( " + + " SELECT film_id FROM Likes WHERE user_id = ? " + + " INTERSECT " + + " SELECT film_id FROM Likes WHERE user_id = ? " + + ") " + + "GROUP BY f.id " + + "ORDER BY COUNT(l.user_id) DESC"; private static final String FIND_GENRES_FOR_FILMS_QUERY = "SELECT fg.film_id, g.id as genre_id, g.name as genre_name " + @@ -88,6 +99,14 @@ public List findAll() { return films; } + public List findCommon(long userId, long filmId) { + List films = jdbc.query(GET_COMMON_FILMS, filmWithRatingMapper, userId, filmId); + if (!films.isEmpty()) { + setGenresForFilms(films); + } + return films; + } + public Optional findById(long id) { List films = jdbc.query(FIND_BY_ID_QUERY, filmWithRatingMapper, id); diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/UserDto.java b/src/main/java/ru/yandex/practicum/filmorate/dto/UserDto.java index d59f55f..53ff743 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dto/UserDto.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/UserDto.java @@ -1,4 +1,5 @@ package ru.yandex.practicum.filmorate.dto; + import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; 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 8ce7663..020dfb3 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 @@ -130,6 +130,20 @@ public RatingDto getRatingById(int id) { return RatingMapper.mapToRatingDto(rating); } + + public List getCommonFilms(long userId, long friendId) { + userExists(userId); + userExists(friendId); + if (userId == friendId) { + throw new ConstraintViolationException("Id's can't be the same"); + } + log.info("Getting MPA common films for users: {}, {}", userId, friendId); + Collection films = filmStorage.getCommonFilms(userId, friendId); + return films.stream() + .map(film -> FilmMapper.mapToFilmDto(film, filmStorage.getLikes(film.getId()))) + .collect(Collectors.toList()); + } + @Transactional public void deleteFilm(long filmId) { FilmDto filmDto = getFilmById(filmId); @@ -142,6 +156,7 @@ public void deleteFilm(long filmId) { } else { throw new InternalServerException("Film was not deleted due to internal error."); } + } private void validateFilm(Film film) { diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java index d67ae29..7d18f16 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java @@ -97,6 +97,11 @@ public Rating getRatingById(int id) { .orElseThrow(() -> new NotFoundException("Rating with id " + id + " not found")); } + + public Collection getCommonFilms(long userId, long friendId) { + return filmRepository.findCommon(userId, friendId); + } + public List getTopFilms(int count, int genreId, int year) { return filmRepository.getTopFilms(count, genreId, year); } diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index 2b54194..c7bdbc0 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -1,18 +1,41 @@ -INSERT INTO Users (email, login, name, birthday) -VALUES ('test@user.com', 'testlogin', 'Test User Name', '1991-11-11'); - -INSERT INTO Users (email, login, name, birthday) -VALUES ('test1@user.com', 'test2login', 'Another User', '1992-11-11'); +DELETE +FROM FILMS; +DELETE +FROM LIKES; +DELETE +FROM GENRE; +DELETE +FROM RATING; +DELETE +FROM USERS; +DELETE +FROM FILM_GENRE; +DELETE +FROM FRIENDSHIP; -INSERT INTO Rating(rating_id, rating_name) +INSERT INTO RATING(rating_id, rating_name) VALUES (1, 'G'), (2, 'PG'), (3, 'PG-13'), (4, 'R'), (5, 'NC-17'); +INSERT INTO USERS (email, login, name, birthday) +VALUES ('test@user.com', 'testlogin', 'Test User Name', '1991-11-11'); + +INSERT INTO USERS ( email, login, name, birthday) +VALUES ('test1@user.com', 'test2login', 'Another User', '1992-11-11'); + +INSERT INTO FILMS (NAME, DESCRIPTION, RELEASE_DATE, DURATION, RATING_ID) +VALUES ('film1', 'its film1', '2010-10-10', '100', '1'), + ('film2', 'its film2', '2020-12-20', '200', '2'); + +INSERT INTO LIKES(user_id, film_id) +VALUES (1, 1), + (2, 1), + (2, 2); -INSERT INTO Genre (name) +INSERT INTO GENRE (name) VALUES ('Комедия'), ('Драма'), ('Мультфильм'), From 715f6a8a15cc5ffe8025cbacdfd0e4be3f184d5e Mon Sep 17 00:00:00 2001 From: 1Boolean1 <124202814+1Boolean1@users.noreply.github.com> Date: Fri, 25 Apr 2025 12:47:56 +0300 Subject: [PATCH 4/9] Add remove endpoint (#17) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add recommendations * Реализован функционал добавления-исправления режиссёра, получения списка режиссёров по id, получение списка всех режиссёров. Реализовано добавление/исправление/получение фильмов с учетом информации о режиссере. * Добавлен функционал возвращения списка фильмов режиссера отсортированных по лайкам или году выпуска * Исправить ошибки Checkstyle * Отладка тестов Postman: переименовать поле director в directors; внести правки в функционал исправления фильма в части информации о режиссере * Переименовать метод deleteFilms в deleteFilmDirectors в классе FilmRepository. Добавить проверку на корректность введённых параметров в контроллере FilmController в методе findByDirector * Add remove endpoint for films and users (#14) * Добавлена функциональность «Популярные фильмы» (#13) * Обновлен метод getTopFilms в цепочке FilmController->FilmService->FilmDbStorage->FilmRepository согласно новой функциональности (фильтрации по genreId и year) * Обновлен метод getTopFilms в цепочке FilmController->FilmService->FilmDbStorage->FilmRepository согласно новой функциональности (фильтрации по genreId и year) * делаю коммит псÐоскольку force push не триггерит пайплайн по всей видимости * Исправил логику работы метода getTopFilms(...) (параметры строки запроса genreId and year являются необязательными) --------- Co-authored-by: nesailormun * add getCommonFilms for user and friend (#10) * add getCommonFilms for user and friend * little change * checkstyle * add getCommonFilms for user and friend * Внести исправление в метод getTopFilms класса FilmRepository: join с таблицей likes заменить на left join --------- Co-authored-by: Plastinin-Igor Co-authored-by: b1mgd Co-authored-by: Stanislav Mun <146199590+Nesailormun@users.noreply.github.com> Co-authored-by: nesailormun --- .../controller/DirectorController.java | 55 ++++++ .../filmorate/controller/FilmController.java | 8 + .../filmorate/controller/UserController.java | 12 +- .../filmorate/dal/DirectorRepository.java | 48 +++++ .../filmorate/dal/FilmRepository.java | 184 ++++++++++++++++-- .../dal/mapper/DirectorRowMapper.java | 20 ++ .../filmorate/dal/mapper/FilmRowMapper.java | 1 + .../practicum/filmorate/dto/DirectorDto.java | 9 + .../practicum/filmorate/dto/FilmDto.java | 1 + .../filmorate/dto/NewFilmRequest.java | 2 + .../filmorate/dto/UpdateFilmRequest.java | 2 + .../exception/ParameterNotValidException.java | 4 + .../filmorate/mappers/DirectorMapper.java | 36 ++++ .../filmorate/mappers/FilmMapper.java | 12 ++ .../practicum/filmorate/model/Director.java | 9 + .../practicum/filmorate/model/Film.java | 1 + .../service/director/DirectorService.java | 54 +++++ .../filmorate/service/film/FilmService.java | 19 +- .../storage/director/DirectorDBStorage.java | 43 ++++ .../filmorate/storage/film/FilmDbStorage.java | 98 +++++++++- src/main/resources/application.properties | 2 +- src/main/resources/data.sql | 52 ++--- src/main/resources/schema.sql | 17 ++ 23 files changed, 640 insertions(+), 49 deletions(-) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/controller/DirectorController.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dal/DirectorRepository.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dal/mapper/DirectorRowMapper.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dto/DirectorDto.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/exception/ParameterNotValidException.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/mappers/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/service/director/DirectorService.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/director/DirectorDBStorage.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..4e9b914 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/DirectorController.java @@ -0,0 +1,55 @@ +package ru.yandex.practicum.filmorate.controller; + +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.DirectorDto; +import ru.yandex.practicum.filmorate.model.Director; +import ru.yandex.practicum.filmorate.service.director.DirectorService; + +import java.util.Collection; + +@Slf4j +@RestController +@RequestMapping("/directors") +@RequiredArgsConstructor +@Validated +public class DirectorController { + + private final DirectorService directorService; + + @GetMapping + public Collection getDirectors() { + log.info("Received GET /directors request"); + return directorService.getDirectors(); + } + + @GetMapping("/{directorId}") + public DirectorDto getDirectorById(@PathVariable long directorId) { + log.info("Received GET /directors/{} request", directorId); + return directorService.getDirectorById(directorId); + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public DirectorDto addDirector(@RequestBody Director director) { + log.info("Received POST /directors request with body: {}", director); + return directorService.addDirector(director); + } + + @PutMapping + public DirectorDto updateDirector(@RequestBody Director director) { + log.info("Received PUT /directors request with body: {}", director); + return directorService.updateDirector(director); + } + + @DeleteMapping("/{directorId}") + public void deleteDirector(@PathVariable long directorId) { + log.info("Received DELETE /directors/{} request", directorId); + directorService.deleteDirector(directorId); + + } + +} 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 8baed43..8a9c7ab 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -84,4 +84,12 @@ public List getCommonFilms( log.info("GET common films for userId={} and friendId={}", userId, friendId); return filmService.getCommonFilms(userId, friendId); } + + @GetMapping("/director/{directorId}") + public List findByDirector(@PathVariable long directorId, + @RequestParam(name = "sortBy") String sortMode) { + log.info("Received GET /films/director/{}?sortBy={} request", directorId, sortMode); + return filmService.findByDirector(directorId, sortMode); + } + } \ 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 d5c7121..9e5f8e5 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java @@ -5,8 +5,10 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.dto.FilmDto; import ru.yandex.practicum.filmorate.dto.UserDto; import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.service.film.FilmService; import ru.yandex.practicum.filmorate.service.user.UserService; import ru.yandex.practicum.filmorate.storage.user.UserDbStorage; @@ -17,10 +19,12 @@ @RequestMapping("/users") public class UserController { private final UserService userService; + private final FilmService filmService; @Autowired - public UserController(UserDbStorage userDbStorage) { + public UserController(UserDbStorage userDbStorage, FilmService filmService) { userService = new UserService(userDbStorage); + this.filmService = filmService; } @GetMapping @@ -79,5 +83,11 @@ public void deleteUser(@PathVariable int userId) { log.info("Recieved DELETE /users/{} request", userId); userService.deleteUser(userId); } + + @GetMapping("/{id}/recommendations") + public Collection getRecommendations(@PathVariable long id) { + log.info("get recommendations"); + return filmService.getRecommendations(id); + } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/DirectorRepository.java b/src/main/java/ru/yandex/practicum/filmorate/dal/DirectorRepository.java new file mode 100644 index 0000000..f18d332 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/DirectorRepository.java @@ -0,0 +1,48 @@ +package ru.yandex.practicum.filmorate.dal; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.model.Director; + +import java.util.List; +import java.util.Optional; + +@Repository +public class DirectorRepository extends BaseRepository { + + private static final String FIND_ALL_QUERY = "SELECT * FROM DIRECTORS d"; + private static final String FIND_BY_ID_QUERY = "SELECT * FROM DIRECTORS d WHERE d.director_id = ?"; + private static final String INSERT_QUERY = "INSERT INTO DIRECTORS (NAME) VALUES(?)"; + private static final String UPDATE_QUERY = "UPDATE DIRECTORS SET NAME = ? WHERE DIRECTOR_ID = ?"; + private static final String DELETE_QUERY = "DELETE FROM DIRECTORS WHERE DIRECTOR_ID = ?"; + + public DirectorRepository(JdbcTemplate jdbc, + RowMapper mapper) { + super(jdbc, mapper); + } + + public List findAll() { + return findMany(FIND_ALL_QUERY); + } + + public Optional findById(long id) { + return findOne(FIND_BY_ID_QUERY, id); + } + + public Director save(Director director) { + Long id = (Long) insert(INSERT_QUERY, director.getName()); + director.setId(id); + return director; + } + + public Director update(Director director) { + update(UPDATE_QUERY, director.getName(), director.getId()); + return director; + } + + public void delete(long id) { + update(DELETE_QUERY, id); + } + +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/FilmRepository.java b/src/main/java/ru/yandex/practicum/filmorate/dal/FilmRepository.java index 042be2e..e41c41e 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/FilmRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/FilmRepository.java @@ -1,6 +1,8 @@ package ru.yandex.practicum.filmorate.dal; +import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; +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.NamedParameterJdbcTemplate; @@ -8,12 +10,15 @@ import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Repository; import ru.yandex.practicum.filmorate.dal.mapper.FilmRowMapper; +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.Rating; import java.sql.Date; import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; import java.util.*; import java.util.stream.Collectors; @@ -31,24 +36,55 @@ public class FilmRepository extends BaseRepository { private static final String ADD_LIKE_QUERY = "INSERT INTO Likes (user_id, film_id) VALUES (?, ?)"; private static final String REMOVE_LIKE_QUERY = "DELETE FROM Likes WHERE film_id = ? AND user_id = ?"; private static final String GET_COMMON_FILMS = "SELECT f.*, r.rating_id as mpa_id, r.rating_name as mpa_name " + - "FROM Films f " + - "JOIN rating AS r ON f.rating_id = r.rating_id " + - "JOIN Likes l ON f.id = l.film_id " + - "WHERE l.film_id IN ( " + - " SELECT film_id FROM Likes WHERE user_id = ? " + - " INTERSECT " + - " SELECT film_id FROM Likes WHERE user_id = ? " + - ") " + - "GROUP BY f.id " + - "ORDER BY COUNT(l.user_id) DESC"; + "FROM Films f " + + "JOIN rating AS r ON f.rating_id = r.rating_id " + + "JOIN Likes l ON f.id = l.film_id " + + "WHERE l.film_id IN ( " + + " SELECT film_id FROM Likes WHERE user_id = ? " + + " INTERSECT " + + " SELECT film_id FROM Likes WHERE user_id = ? " + + ") " + + "GROUP BY f.id " + + "ORDER BY COUNT(l.user_id) DESC"; private static final String FIND_GENRES_FOR_FILMS_QUERY = "SELECT fg.film_id, g.id as genre_id, g.name as genre_name " + - "FROM genre g " + - "JOIN film_genre fg ON g.id = fg.genre_id " + - "WHERE fg.film_id IN (:filmIds)"; + "FROM genre g " + + "JOIN film_genre fg ON g.id = fg.genre_id " + + "WHERE fg.film_id IN (:filmIds)"; private static final String DELETE_FILM_QUERY = "DELETE FROM Films WHERE id = ?"; + private static final String FIND_DIRECTOR_FOR_FILMS_QUERY = """ + SELECT fd.FILM_ID, + d.DIRECTOR_ID, + d.NAME as DIRECTOR_NAME + FROM FILM_DIRECTORS fd + JOIN DIRECTORS d ON (fd.DIRECTOR_ID = d.DIRECTOR_ID) + WHERE fd.film_id in (:filmIds) + """; + + private static final String FIND_BY_DIRECTOR_SORT_BY_LIKES = """ + SELECT f.*, + r.rating_id as mpa_id, + r.rating_name as mpa_name + FROM films AS f + JOIN rating AS r ON f.rating_id = r.rating_id + JOIN FILM_DIRECTORS fd ON f.ID = fd.FILM_ID + WHERE fd.DIRECTOR_ID = ? + ORDER BY (SELECT COUNT(1) FROM LIKES l WHERE l.FILM_ID = f.ID) DESC + """; + + private static final String FIND_BY_DIRECTOR_SORT_BY_YEAR = """ + SELECT f.*, + r.rating_id as mpa_id, + r.rating_name as mpa_name + FROM films AS f + JOIN rating AS r ON f.rating_id = r.rating_id + JOIN FILM_DIRECTORS fd ON f.ID = fd.FILM_ID + WHERE fd.DIRECTOR_ID = ? + ORDER BY f.RELEASE_DATE + """; + private final JdbcTemplate jdbc; private final NamedParameterJdbcTemplate namedJdbcTemplate; private FilmRowMapper filmMapper; @@ -63,6 +99,16 @@ private static class FilmGenreRelation { } } + private static class FilmDirectorRelation { + final long filmId; + final Director director; + + FilmDirectorRelation(long filmId, Director director) { + this.filmId = filmId; + this.director = director; + } + } + private final RowMapper filmGenreRelationRowMapper = (rs, rowNum) -> { Genre genre = new Genre(); genre.setId(rs.getInt("genre_id")); @@ -70,6 +116,13 @@ private static class FilmGenreRelation { return new FilmGenreRelation(rs.getLong("film_id"), genre); }; + private final RowMapper filmDirectorRelationRowMapper = (rs, rowNum) -> { + Director director = new Director(); + director.setId(rs.getLong("director_id")); + director.setName(rs.getString("director_name")); + return new FilmDirectorRelation(rs.getLong("film_id"), director); + }; + public FilmRepository(JdbcTemplate jdbc, NamedParameterJdbcTemplate namedJdbcTemplate, FilmRowMapper filmMapper) { @@ -95,6 +148,7 @@ public List findAll() { if (!films.isEmpty()) { setGenresForFilms(films); + setDirectorForFilm(films); } return films; } @@ -107,6 +161,7 @@ public List findCommon(long userId, long filmId) { return films; } + public Optional findById(long id) { List films = jdbc.query(FIND_BY_ID_QUERY, filmWithRatingMapper, id); @@ -115,6 +170,7 @@ public Optional findById(long id) { } else { Film film = films.getFirst(); setGenresForFilms(List.of(film)); + setDirectorForFilm(List.of(film)); return Optional.of(film); } } @@ -168,7 +224,7 @@ public Film save(Film film) { film.setId(id); saveGenres(film); - + saveDirector(film); return findById(id).orElseThrow(() -> new IllegalStateException("Saved film not found, id: " + id)); } @@ -189,6 +245,9 @@ public Film update(Film film) { deleteGenres(film.getId()); saveGenres(film); + deleteFilms(film.getId()); + saveDirector(film); + return findById(film.getId()).orElseThrow(() -> new IllegalStateException("Updated film not found, id: " + film.getId())); } @@ -196,6 +255,25 @@ public boolean deleteFilm(long id) { return delete(DELETE_FILM_QUERY, id); } + public Map> getAllLikesGroupedByUser() { + String sql = "SELECT * FROM Likes"; + + return jdbc.query(sql, new ResultSetExtractor>>() { + @Override + public Map> extractData(ResultSet rs) throws SQLException, DataAccessException { + Map> userLikes = new HashMap<>(); + while (rs.next()) { + long userId = rs.getLong("user_id"); + long filmId = rs.getLong("film_id"); + + List filmIds = userLikes.computeIfAbsent(userId, k -> new ArrayList<>()); + filmIds.add(filmId); + } + return userLikes; + } + }); + } + private void deleteGenres(long filmId) { String sql = "DELETE FROM film_genre WHERE film_id = ?"; jdbc.update(sql, filmId); @@ -234,9 +312,10 @@ public void removeLike(long filmId, long userId) { public List getTopFilms(int count, Integer genreId, Integer year) { StringBuilder queryBuilder = new StringBuilder("SELECT f.*, r.rating_id AS mpa_id, r.rating_name AS mpa_name " + - "FROM films AS f JOIN rating AS r on f.rating_id = r.rating_id " + - "JOIN film_genre AS fg ON f.id = fg.film_id " + - "JOIN likes AS l ON f.id = l.film_id WHERE 1=1"); + "FROM films AS f JOIN rating AS r on f.rating_id = r.rating_id " + + "JOIN film_genre AS fg ON f.id = fg.film_id " + + "LEFT JOIN likes AS l ON f.id = l.film_id WHERE 1=1" + ); List filterParams = new ArrayList<>(); @@ -258,4 +337,75 @@ public List getTopFilms(int count, Integer genreId, Integer year) { return films; } + public List findByDirector(long directorId, String sortMode) { + String sql = ""; + if (sortMode.equals("likes")) { + sql = FIND_BY_DIRECTOR_SORT_BY_LIKES; + } else if (sortMode.equals("year")) { + sql = FIND_BY_DIRECTOR_SORT_BY_YEAR; + } + + List films = jdbc.query(sql, filmWithRatingMapper, directorId); + + if (!films.isEmpty()) { + setGenresForFilms(films); + setDirectorForFilm(films); + } + return films; + } + + private void saveDirector(Film film) { + if (film.getDirectors() == null || film.getDirectors().isEmpty()) { + return; + } + String sql = "INSERT INTO FILM_DIRECTORS (FILM_ID, DIRECTOR_ID) VALUES(?, ?)"; + + List batchArgs = film.getDirectors().stream() + .filter(Objects::nonNull) + .filter(director -> director.getId() > 0) + .distinct() + .map(director -> new Object[]{film.getId(), director.getId()}) + .collect(Collectors.toList()); + + if (!batchArgs.isEmpty()) { + jdbc.batchUpdate(sql, batchArgs); + } + } + + private void setDirectorForFilm(List films) { + List filmIds = films.stream() + .map(Film::getId) + .collect(Collectors.toList()); + + if (filmIds.isEmpty()) { + return; + } + + MapSqlParameterSource parameters = new MapSqlParameterSource(); + parameters.addValue("filmIds", filmIds); + + + List relations = namedJdbcTemplate.query( + FIND_DIRECTOR_FOR_FILMS_QUERY, + parameters, + filmDirectorRelationRowMapper + ); + + Map> directorsByFilmId = relations.stream() + .collect(groupingBy( + relation -> relation.filmId, + mapping(relation -> relation.director, toList()) + )); + + films.forEach(film -> + film.setDirectors(directorsByFilmId.getOrDefault(film.getId(), Collections.emptyList())) + ); + } + + private void deleteFilms(long filmId) { + String sql = "DELETE FROM FILM_DIRECTORS WHERE film_id = ?"; + jdbc.update(sql, filmId); + } + } + diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/mapper/DirectorRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/dal/mapper/DirectorRowMapper.java new file mode 100644 index 0000000..e306b93 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/mapper/DirectorRowMapper.java @@ -0,0 +1,20 @@ +package ru.yandex.practicum.filmorate.dal.mapper; + +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 { + Director director = new Director(); + director.setId(rs.getLong("director_id")); + director.setName(rs.getString("name")); + + return director; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/mapper/FilmRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/dal/mapper/FilmRowMapper.java index dcda2df..4c442a1 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/mapper/FilmRowMapper.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/mapper/FilmRowMapper.java @@ -27,6 +27,7 @@ public Film mapRow(ResultSet rs, int rowNum) throws SQLException { film.setMpa(mpa); film.setGenres(new ArrayList<>()); + film.setDirectors(new ArrayList<>()); return film; } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/DirectorDto.java b/src/main/java/ru/yandex/practicum/filmorate/dto/DirectorDto.java new file mode 100644 index 0000000..e617f0f --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/DirectorDto.java @@ -0,0 +1,9 @@ +package ru.yandex.practicum.filmorate.dto; + +import lombok.Data; + +@Data +public class DirectorDto { + private Long id; + private String name; +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/FilmDto.java b/src/main/java/ru/yandex/practicum/filmorate/dto/FilmDto.java index ccfb1ba..b9fe18d 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dto/FilmDto.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/FilmDto.java @@ -18,4 +18,5 @@ public class FilmDto { private RatingDto mpa; private List genres = new ArrayList<>(); private Set likes = new HashSet<>(); + private List directors = new ArrayList<>(); } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/NewFilmRequest.java b/src/main/java/ru/yandex/practicum/filmorate/dto/NewFilmRequest.java index 9a18bba..1280050 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dto/NewFilmRequest.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/NewFilmRequest.java @@ -1,6 +1,7 @@ package ru.yandex.practicum.filmorate.dto; import lombok.Data; +import ru.yandex.practicum.filmorate.model.Director; import ru.yandex.practicum.filmorate.model.Rating; @Data @@ -10,4 +11,5 @@ public class NewFilmRequest { private String releaseDate; private String duration; private Rating ratingId; + private Director directors; } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/UpdateFilmRequest.java b/src/main/java/ru/yandex/practicum/filmorate/dto/UpdateFilmRequest.java index d9fda56..44f24c0 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dto/UpdateFilmRequest.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/UpdateFilmRequest.java @@ -1,6 +1,7 @@ package ru.yandex.practicum.filmorate.dto; import lombok.Data; +import ru.yandex.practicum.filmorate.model.Director; import ru.yandex.practicum.filmorate.model.Rating; @Data @@ -10,6 +11,7 @@ public class UpdateFilmRequest { private String releaseDate; private String duration; private Rating ratingId; + private Director directors; public boolean hasName() { return !(name == null || name.isBlank()); diff --git a/src/main/java/ru/yandex/practicum/filmorate/exception/ParameterNotValidException.java b/src/main/java/ru/yandex/practicum/filmorate/exception/ParameterNotValidException.java new file mode 100644 index 0000000..19750f4 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/exception/ParameterNotValidException.java @@ -0,0 +1,4 @@ +package ru.yandex.practicum.filmorate.exception; + +public class ParameterNotValidException { +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/mappers/DirectorMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mappers/DirectorMapper.java new file mode 100644 index 0000000..2e34790 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/mappers/DirectorMapper.java @@ -0,0 +1,36 @@ +package ru.yandex.practicum.filmorate.mappers; + +import lombok.experimental.UtilityClass; +import ru.yandex.practicum.filmorate.dto.DirectorDto; +import ru.yandex.practicum.filmorate.model.Director; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +@UtilityClass +public class DirectorMapper { + public static Director mapToDirector(Director request) { + Director director = new Director(); + director.setId(request.getId()); + director.setName(request.getName()); + return director; + } + + public static DirectorDto mapToDirectorDto(Director director) { + DirectorDto directorDto = new DirectorDto(); + directorDto.setId(director.getId()); + directorDto.setName(director.getName()); + return directorDto; + } + + public static List mapToDirectorDtoList(List directors) { + if (directors == null || directors.isEmpty()) { + return Collections.emptyList(); + } + return directors.stream() + .map(DirectorMapper::mapToDirectorDto) + .collect(Collectors.toList()); + } + +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/mappers/FilmMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mappers/FilmMapper.java index 66c3506..e52c360 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/mappers/FilmMapper.java +++ b/src/main/java/ru/yandex/practicum/filmorate/mappers/FilmMapper.java @@ -2,6 +2,7 @@ import lombok.experimental.UtilityClass; import ru.yandex.practicum.filmorate.dto.FilmDto; +import ru.yandex.practicum.filmorate.model.Director; import ru.yandex.practicum.filmorate.model.Film; import ru.yandex.practicum.filmorate.model.Genre; @@ -33,6 +34,16 @@ public static Film mapToFilm(Film requestFilm) { film.setGenres(new ArrayList<>()); } + if (requestFilm.getDirectors() != null) { + List directorsWithIdOnly = requestFilm.getDirectors() + .stream() + .filter(d -> d != null && d.getId() != 0) + .collect(Collectors.toList()); + film.setDirectors(directorsWithIdOnly); + } else { + film.setDirectors(new ArrayList<>()); + } + return film; } @@ -48,6 +59,7 @@ public static FilmDto mapToFilmDto(Film film, Set likes) { dto.setMpa(RatingMapper.mapToRatingDto(film.getMpa())); dto.setGenres(GenreMapper.mapToGenreDtoList(film.getGenres())); dto.setLikes(Optional.ofNullable(likes).orElse(Collections.emptySet())); + dto.setDirectors(DirectorMapper.mapToDirectorDtoList(film.getDirectors())); return dto; } 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..e4e8ef4 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Director.java @@ -0,0 +1,9 @@ +package ru.yandex.practicum.filmorate.model; + +import lombok.Data; + +@Data +public class Director { + private Long id; + private 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 79bf0c2..ca8c868 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java @@ -23,4 +23,5 @@ public class Film { @NotNull(message = "Rating cannot be null") private Rating mpa; private List genres = new ArrayList<>(); + private List directors = new ArrayList<>(); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/director/DirectorService.java b/src/main/java/ru/yandex/practicum/filmorate/service/director/DirectorService.java new file mode 100644 index 0000000..35aebbf --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/director/DirectorService.java @@ -0,0 +1,54 @@ +package ru.yandex.practicum.filmorate.service.director; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.dto.DirectorDto; +import ru.yandex.practicum.filmorate.mappers.DirectorMapper; +import ru.yandex.practicum.filmorate.model.Director; +import ru.yandex.practicum.filmorate.storage.director.DirectorDBStorage; + +import java.util.Collection; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +public class DirectorService { + + private final DirectorDBStorage directorStorage; + + public Collection getDirectors() { + log.info("Getting all directors"); + Collection directors = directorStorage.getDirectors(); + return directors.stream() + .map(DirectorMapper::mapToDirectorDto) + .collect(Collectors.toList()); + } + + public DirectorDto getDirectorById(long directorId) { + log.info("Getting director by id: {}", directorId); + Director director = directorStorage.getDirectorById(directorId); + return DirectorMapper.mapToDirectorDto(director); + } + + public DirectorDto addDirector(Director director) { + log.info("Adding new director: {}", director.getName()); + Director newDirector = directorStorage.addDirector(director); + log.info("Director {} added with id: {}", director.getName(), director.getId()); + return DirectorMapper.mapToDirectorDto(director); + } + + public DirectorDto updateDirector(Director director) { + log.info("Updating director with id: {}", director.getId()); + Director updateDirector = directorStorage.updateDirector(director); + log.info("Director {} with id: {} updated", updateDirector.getName(), updateDirector.getId()); + return DirectorMapper.mapToDirectorDto(updateDirector); + } + + public void deleteDirector(long directorId) { + log.info("Deleting director by id: {}", directorId); + directorStorage.deleteDirector(directorId); + } + +} 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 020dfb3..d5929f4 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 @@ -94,7 +94,6 @@ public List getTopFilms(int count, int genreId, int year) { if (count <= 0) { throw new ValidationException("Count parameter must be positive."); } - return filmStorage.getTopFilms(count, genreId, year) .stream() .map(film -> { @@ -159,6 +158,15 @@ public void deleteFilm(long filmId) { } + public List getRecommendations(long id) { + userExists(id); + log.info("Getting recommendations films for user with id = {}", id); + Collection films = filmStorage.getRecommendations(id); + return films.stream() + .map(film -> FilmMapper.mapToFilmDto(film, filmStorage.getLikes(film.getId()))) + .collect(Collectors.toList()); + } + private void validateFilm(Film film) { if (film.getReleaseDate() != null && film.getReleaseDate().isBefore(MIN_RELEASE_DATE)) { log.warn("Validation failed"); @@ -173,4 +181,13 @@ private void userExists(long userId) { private void filmExists(long filmId) { filmStorage.getFilmById(filmId); } + + public List findByDirector(long directorId, String sortMode) { + log.info("Getting films by director with id: {} and sort mode {}", directorId, sortMode); + Collection films = filmStorage.findByDirector(directorId, sortMode); + return films.stream() + .map(film -> FilmMapper.mapToFilmDto(film, filmStorage.getLikes(film.getId()))) + .collect(Collectors.toList()); + } + } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/director/DirectorDBStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/director/DirectorDBStorage.java new file mode 100644 index 0000000..750de3a --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/director/DirectorDBStorage.java @@ -0,0 +1,43 @@ +package ru.yandex.practicum.filmorate.storage.director; + +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.dal.DirectorRepository; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Director; + +import java.util.Collection; + +@Repository +public class DirectorDBStorage { + private final DirectorRepository directorRepository; + + public DirectorDBStorage(DirectorRepository directorRepository) { + this.directorRepository = directorRepository; + } + + public Director addDirector(Director director) { + return directorRepository.save(director); + } + + public Director updateDirector(Director director) { + directorRepository.findById(director.getId()) + .orElseThrow(() -> new NotFoundException("Director with id " + director.getId() + " not found")); + return directorRepository.update(director); + } + + public Collection getDirectors() { + return directorRepository.findAll(); + } + + public Director getDirectorById(long directorId) { + return directorRepository.findById(directorId) + .orElseThrow(() -> new NotFoundException("Director with id " + directorId + " not found")); + } + + public void deleteDirector(long directorId) { + directorRepository.findById(directorId) + .orElseThrow(() -> new NotFoundException("Director with id " + directorId + " not found")); + directorRepository.delete(directorId); + } +} + diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java index 7d18f16..cd8de11 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java @@ -9,9 +9,8 @@ import ru.yandex.practicum.filmorate.model.Genre; import ru.yandex.practicum.filmorate.model.Rating; -import java.util.Collection; -import java.util.List; -import java.util.Set; +import java.util.*; +import java.util.stream.Collectors; @Repository public class FilmDbStorage implements FilmStorage { @@ -123,4 +122,97 @@ private void validateGenresExist(List genres) { } } } + + public Collection getRecommendations(long targetUserId) { + Optional mostSimilarUserOpt = findUserWithMostCommonLikes(targetUserId); + + if (mostSimilarUserOpt.isEmpty()) { + return Collections.emptyList(); + } + long mostSimilarUserId = mostSimilarUserOpt.get(); + + Map> allUserLikes = filmRepository.getAllLikesGroupedByUser(); + + Set filmsLikedByTarget = new HashSet<>(allUserLikes.getOrDefault(targetUserId, Collections.emptyList())); + + List filmsLikedBySimilar = allUserLikes.getOrDefault(mostSimilarUserId, Collections.emptyList()); + + List recommendedFilmIds = filmsLikedBySimilar.stream() + .filter(filmId -> !filmsLikedByTarget.contains(filmId)) + .collect(Collectors.toList()); + + if (recommendedFilmIds.isEmpty()) { + return Collections.emptyList(); + } + + return recommendedFilmIds.stream() + .map(filmId -> { + try { + return getFilmById(filmId); + } catch (NotFoundException e) { + return null; + } + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + public Optional findUserWithMostCommonLikes(long id) { + Map crossesFilms = findCrosses(id); + + if (crossesFilms == null || crossesFilms.isEmpty()) { + return Optional.empty(); + } + + Optional> maxEntry = crossesFilms.entrySet() + .stream() + .max(Map.Entry.comparingByValue()); + + return maxEntry.map(Map.Entry::getKey); + } + + + private Map findCrosses(long targetUserId) { + Map> allUserLikes = filmRepository.getAllLikesGroupedByUser(); + + List filmsLikedByTargetUser = allUserLikes.get(targetUserId); + + if (filmsLikedByTargetUser == null || filmsLikedByTargetUser.isEmpty()) { + return Collections.emptyMap(); + } + + Set targetLikedSet = new HashSet<>(filmsLikedByTargetUser); + + Map commonLikesCountMap = new HashMap<>(); + + for (Map.Entry> entry : allUserLikes.entrySet()) { + long otherUserId = entry.getKey(); + List filmsLikedByOtherUser = entry.getValue(); + + if (otherUserId == targetUserId) { + continue; + } + + if (filmsLikedByOtherUser == null || filmsLikedByOtherUser.isEmpty()) { + continue; + } + + int commonCount = 0; + for (Long filmLikedByOther : filmsLikedByOtherUser) { + if (targetLikedSet.contains(filmLikedByOther)) { + commonCount++; + } + } + + if (commonCount > 0) { + commonLikesCountMap.put(otherUserId, commonCount); + } + } + return commonLikesCountMap; + } + + public List findByDirector(long directorId, String sortMode) { + return filmRepository.findByDirector(directorId, sortMode); + } + } \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 73fd17b..2776f53 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 logging.level.ru.yandex.practicum.filmorate=INFO spring.sql.init.mode=always spring.datasource.url=jdbc:h2:file:./db/filmorate diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index c7bdbc0..ec1f723 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -1,42 +1,42 @@ -DELETE -FROM FILMS; -DELETE -FROM LIKES; -DELETE -FROM GENRE; -DELETE -FROM RATING; -DELETE -FROM USERS; -DELETE -FROM FILM_GENRE; -DELETE -FROM FRIENDSHIP; +delete +from FILMS; +delete +from LIKES; +delete +from GENRE; +delete +from RATING; +delete +from USERS; +delete +from FILM_GENRE; +delete +from FRIENDSHIP; -INSERT INTO RATING(rating_id, rating_name) -VALUES (1, 'G'), +insert into RATING(rating_id, rating_name) +values (1, 'G'), (2, 'PG'), (3, 'PG-13'), (4, 'R'), (5, 'NC-17'); -INSERT INTO USERS (email, login, name, birthday) -VALUES ('test@user.com', 'testlogin', 'Test User Name', '1991-11-11'); +insert into USERS (email, login, name, birthday) +values ('test@user.com', 'testlogin', 'Test User Name', '1991-11-11'); -INSERT INTO USERS ( email, login, name, birthday) -VALUES ('test1@user.com', 'test2login', 'Another User', '1992-11-11'); +insert into USERS ( email, login, name, birthday) +values ('test1@user.com', 'test2login', 'Another User', '1992-11-11'); -INSERT INTO FILMS (NAME, DESCRIPTION, RELEASE_DATE, DURATION, RATING_ID) -VALUES ('film1', 'its film1', '2010-10-10', '100', '1'), +insert into FILMS (NAME, DESCRIPTION, RELEASE_DATE, DURATION, RATING_ID) +values ('film1', 'its film1', '2010-10-10', '100', '1'), ('film2', 'its film2', '2020-12-20', '200', '2'); -INSERT INTO LIKES(user_id, film_id) -VALUES (1, 1), +insert into LIKES(user_id, film_id) +values (1, 1), (2, 1), (2, 2); -INSERT INTO GENRE (name) -VALUES ('Комедия'), +insert into GENRE (name) +values ('Комедия'), ('Драма'), ('Мультфильм'), ('Триллер'), diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index c290170..289841c 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -55,4 +55,21 @@ CREATE TABLE IF NOT EXISTS Film_genre PRIMARY KEY (film_id, genre_id), FOREIGN KEY (film_id) REFERENCES Films (id) ON DELETE CASCADE, FOREIGN KEY (genre_id) REFERENCES Genre (id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS directors +( + director_id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY, + name varchar(255) NOT NULL, + CONSTRAINT directors_pk PRIMARY KEY (director_id) +); + +CREATE TABLE IF NOT EXISTS film_directors +( + id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY, + film_id BIGINT NOT NULL, + director_id BIGINT NOT NULL, + CONSTRAINT film_directors_pk PRIMARY KEY (id), + FOREIGN KEY (film_id) REFERENCES Films (id) ON DELETE CASCADE, + FOREIGN KEY (director_id) REFERENCES directors (director_id) ON DELETE CASCADE ); \ No newline at end of file From d39c94ea60a420ecba3fac7cecd2ba95d2f7ad77 Mon Sep 17 00:00:00 2001 From: Plastinin-Igor Date: Fri, 25 Apr 2025 15:56:08 +0300 Subject: [PATCH 5/9] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D1=82=D1=8C=20=D0=A4=D1=83=D0=BD=D0=BA=D1=86=D0=B8?= =?UTF-8?q?=D0=BE=D0=BD=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20?= =?UTF-8?q?=D0=9F=D0=BE=D0=B8=D1=81=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../filmorate/controller/FilmController.java | 6 +++ .../filmorate/dal/FilmRepository.java | 47 +++++++++++++++++++ .../exception/ParameterNotValidException.java | 9 +++- .../filmorate/service/film/FilmService.java | 8 ++++ .../filmorate/storage/film/FilmDbStorage.java | 3 ++ 5 files changed, 72 insertions(+), 1 deletion(-) 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 8a9c7ab..14084c9 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -92,4 +92,10 @@ public List findByDirector(@PathVariable long directorId, return filmService.findByDirector(directorId, sortMode); } + @GetMapping("/search") + public List searchFilms(@RequestParam String query, + @RequestParam String by) { + log.info("GET /films/search?query={}&by={}", query, by); + return filmService.searchFilms(query, by); + } } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/FilmRepository.java b/src/main/java/ru/yandex/practicum/filmorate/dal/FilmRepository.java index e41c41e..5dabd74 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/FilmRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/FilmRepository.java @@ -10,6 +10,7 @@ import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Repository; import ru.yandex.practicum.filmorate.dal.mapper.FilmRowMapper; +import ru.yandex.practicum.filmorate.exception.ParameterNotValidException; import ru.yandex.practicum.filmorate.model.Director; import ru.yandex.practicum.filmorate.model.Film; import ru.yandex.practicum.filmorate.model.Genre; @@ -85,6 +86,17 @@ ORDER BY (SELECT COUNT(1) FROM LIKES l WHERE l.FILM_ID = f.ID) DESC ORDER BY f.RELEASE_DATE """; + private static final String FIND_BY_TITLE_OR_DIRECTOR = """ + SELECT f.*, + r.rating_id as mpa_id, + r.rating_name as mpa_name + FROM films AS f + JOIN rating AS r ON f.rating_id = r.rating_id + LEFT JOIN FILM_DIRECTORS fd ON f.ID = fd.FILM_ID + LEFT JOIN DIRECTORS d ON fd.DIRECTOR_ID = d.DIRECTOR_ID + WHERE + """; + private final JdbcTemplate jdbc; private final NamedParameterJdbcTemplate namedJdbcTemplate; private FilmRowMapper filmMapper; @@ -407,5 +419,40 @@ private void deleteFilms(long filmId) { jdbc.update(sql, filmId); } + public List searchFilms(String searchText, String searchBy) { + // searchBy может принимать значения director (поиск по режиссёру), title (поиск по названию), + // либо оба значения через запятую при поиске одновременно и по режиссеру и по названию. + // Поэтому поместим эти параметры в массив и в цикле сделаем условия для запроса + String[] searchByList = searchBy.split(","); + Arrays.sort(searchByList); + StringBuilder queryBuilder = new StringBuilder(FIND_BY_TITLE_OR_DIRECTOR); + + if (searchByList.length > 2 || searchByList.length == 0) { + throw new ParameterNotValidException("by", "The parameter must contain one or two values: " + + "director and(or) title"); + } + + for (int i = 0; i < searchByList.length; i++) { + if (searchByList[i].equals("director") && searchByList.length == 1) { + queryBuilder.append("upper(d.NAME) LIKE upper('%" + searchText + "%')"); + } else if (searchByList[i].equals("director") && searchByList.length == 2) { + queryBuilder.append("upper(d.NAME) LIKE upper('%" + searchText + "%') OR"); + } + if (searchByList[i].equals("title")) { + queryBuilder.append(" upper(f.NAME) LIKE upper('%" + searchText + "%')"); + } + } + queryBuilder.append("\nORDER BY (SELECT count(1) FROM LIKES l WHERE l.FILM_ID = f.ID) DESC"); + + List films = jdbc.query(queryBuilder.toString(), filmWithRatingMapper); + + if (!films.isEmpty()) { + setGenresForFilms(films); + setDirectorForFilm(films); + } + + return films; + } + } diff --git a/src/main/java/ru/yandex/practicum/filmorate/exception/ParameterNotValidException.java b/src/main/java/ru/yandex/practicum/filmorate/exception/ParameterNotValidException.java index 19750f4..eb87526 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/exception/ParameterNotValidException.java +++ b/src/main/java/ru/yandex/practicum/filmorate/exception/ParameterNotValidException.java @@ -1,4 +1,11 @@ package ru.yandex.practicum.filmorate.exception; -public class ParameterNotValidException { +public class ParameterNotValidException extends IllegalArgumentException { + private final String parameter; + private final String reason; + + public ParameterNotValidException(String parameter, String reason) { + this.parameter = parameter; + this.reason = reason; + } } 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 d5929f4..f8ec588 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 @@ -190,4 +190,12 @@ public List findByDirector(long directorId, String sortMode) { .collect(Collectors.toList()); } + public List searchFilms(String searchText, String searchBy) { + Collection films = filmStorage.searchFilms(searchText, searchBy); + log.info("Getting films by {} and search text {}", searchBy, searchText); + return films.stream() + .map(film -> FilmMapper.mapToFilmDto(film, filmStorage.getLikes(film.getId()))) + .collect(Collectors.toList()); + } + } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java index cd8de11..5263f61 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java @@ -215,4 +215,7 @@ public List findByDirector(long directorId, String sortMode) { return filmRepository.findByDirector(directorId, sortMode); } + public List searchFilms(String searchText, String searchBy) { + return filmRepository.searchFilms(searchText, searchBy); + } } \ No newline at end of file From e7bdd27bb7e74d54835490bf17f50b8622b06209 Mon Sep 17 00:00:00 2001 From: Plastinin-Igor Date: Fri, 25 Apr 2025 16:02:06 +0300 Subject: [PATCH 6/9] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D1=83=20Checkstyl?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/ru/yandex/practicum/filmorate/dal/FilmRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/FilmRepository.java b/src/main/java/ru/yandex/practicum/filmorate/dal/FilmRepository.java index 5dabd74..5b42050 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/FilmRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/FilmRepository.java @@ -94,7 +94,7 @@ ORDER BY (SELECT COUNT(1) FROM LIKES l WHERE l.FILM_ID = f.ID) DESC JOIN rating AS r ON f.rating_id = r.rating_id LEFT JOIN FILM_DIRECTORS fd ON f.ID = fd.FILM_ID LEFT JOIN DIRECTORS d ON fd.DIRECTOR_ID = d.DIRECTOR_ID - WHERE + WHERE """; private final JdbcTemplate jdbc; From 8daf78c05970bb93eb6d5d0bef497fe94eccdb89 Mon Sep 17 00:00:00 2001 From: nesailormun Date: Fri, 25 Apr 2025 16:14:19 +0300 Subject: [PATCH 7/9] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=BE=D0=BD?= =?UTF-8?q?=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D1=83=D1=87=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D1=8B=20=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D0=B9=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=BF=D1=80=D0=B8=20=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D0=B5=20=D1=81=20=D0=B4=D1=80=D1=83=D0=B7=D1=8C=D1=8F?= =?UTF-8?q?=D0=BC=D0=B8=20=D0=B8=20=D0=BB=D0=B0=D0=B9=D0=BA=D0=B0=D0=BC?= =?UTF-8?q?=D0=B8.=20=D0=9E=D0=B6=D0=B8=D0=B4=D0=B0=D1=8E=20=D1=80=D0=B5?= =?UTF-8?q?=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D0=B8=20=D1=84=D1=83?= =?UTF-8?q?=D0=BD=D0=BA=D1=86=D0=B8=D0=BE=D0=BD=D0=B0=D0=BB=D1=8C=D0=BD?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D0=B8=20review.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../filmorate/controller/FeedController.java | 26 +++++++++++++ .../filmorate/controller/UserController.java | 5 ++- .../filmorate/dal/FeedRepository.java | 31 +++++++++++++++ .../filmorate/dal/FilmRepository.java | 32 +++++++-------- .../filmorate/dal/mapper/EventRowMapper.java | 25 ++++++++++++ .../practicum/filmorate/model/Event.java | 14 +++++++ .../practicum/filmorate/model/EventType.java | 7 ++++ .../practicum/filmorate/model/Operation.java | 7 ++++ .../filmorate/service/feed/FeedService.java | 39 +++++++++++++++++++ .../filmorate/service/film/FilmService.java | 8 ++-- .../filmorate/service/user/UserService.java | 12 +++++- src/main/resources/schema.sql | 11 ++++++ 12 files changed, 194 insertions(+), 23 deletions(-) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/controller/FeedController.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dal/FeedRepository.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dal/mapper/EventRowMapper.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/service/feed/FeedService.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..1c06dd6 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FeedController.java @@ -0,0 +1,26 @@ +package ru.yandex.practicum.filmorate.controller; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import ru.yandex.practicum.filmorate.model.Event; +import ru.yandex.practicum.filmorate.service.feed.FeedService; + +import java.util.List; + +@RestController +@RequestMapping("/users") +public class FeedController { + + private final FeedService feedService; + + public FeedController(FeedService feedService) { + this.feedService = feedService; + } + + @GetMapping("/{id}/feed") + public List getUserFeed(@PathVariable("id") long userId) { + return feedService.getUsersEventFeed(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 9e5f8e5..3da7c93 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java @@ -8,6 +8,7 @@ import ru.yandex.practicum.filmorate.dto.FilmDto; import ru.yandex.practicum.filmorate.dto.UserDto; import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.service.feed.FeedService; import ru.yandex.practicum.filmorate.service.film.FilmService; import ru.yandex.practicum.filmorate.service.user.UserService; import ru.yandex.practicum.filmorate.storage.user.UserDbStorage; @@ -22,8 +23,8 @@ public class UserController { private final FilmService filmService; @Autowired - public UserController(UserDbStorage userDbStorage, FilmService filmService) { - userService = new UserService(userDbStorage); + public UserController(UserDbStorage userDbStorage, FilmService filmService, FeedService feedService) { + userService = new UserService(userDbStorage, feedService); this.filmService = filmService; } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/FeedRepository.java b/src/main/java/ru/yandex/practicum/filmorate/dal/FeedRepository.java new file mode 100644 index 0000000..bb6bbce --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/FeedRepository.java @@ -0,0 +1,31 @@ +package ru.yandex.practicum.filmorate.dal; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.dal.mapper.EventRowMapper; +import ru.yandex.practicum.filmorate.model.Event; + +import java.util.List; + +@Repository +public class FeedRepository { + + private final JdbcTemplate jdbc; + private final EventRowMapper eventRowMapper; + + public FeedRepository(JdbcTemplate jdbc, EventRowMapper eventRowMapper) { + this.jdbc = jdbc; + this.eventRowMapper = eventRowMapper; + } + + public List getUsersEventFeed(long userId) { + String sqlQuery = "SELECT * FROM event_feed WHERE user_id = ? ORDER BY timestamp DESC;"; + return jdbc.query(sqlQuery, eventRowMapper, userId); + } + + public void saveEvent(Event event) { + String sqlQuery = "INSERT INTO event_feed (timestamp, user_id, event_type, operation, entity_id) VALUES (?, ?, ?, ?, ?);"; + jdbc.update(sqlQuery, event.getTimestamp(), event.getUserId(), event.getEventType().name(), + event.getOperation().name(), event.getEntityId()); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/FilmRepository.java b/src/main/java/ru/yandex/practicum/filmorate/dal/FilmRepository.java index e41c41e..dc3f6be 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/FilmRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/FilmRepository.java @@ -36,22 +36,22 @@ public class FilmRepository extends BaseRepository { private static final String ADD_LIKE_QUERY = "INSERT INTO Likes (user_id, film_id) VALUES (?, ?)"; private static final String REMOVE_LIKE_QUERY = "DELETE FROM Likes WHERE film_id = ? AND user_id = ?"; private static final String GET_COMMON_FILMS = "SELECT f.*, r.rating_id as mpa_id, r.rating_name as mpa_name " + - "FROM Films f " + - "JOIN rating AS r ON f.rating_id = r.rating_id " + - "JOIN Likes l ON f.id = l.film_id " + - "WHERE l.film_id IN ( " + - " SELECT film_id FROM Likes WHERE user_id = ? " + - " INTERSECT " + - " SELECT film_id FROM Likes WHERE user_id = ? " + - ") " + - "GROUP BY f.id " + - "ORDER BY COUNT(l.user_id) DESC"; + "FROM Films f " + + "JOIN rating AS r ON f.rating_id = r.rating_id " + + "JOIN Likes l ON f.id = l.film_id " + + "WHERE l.film_id IN ( " + + " SELECT film_id FROM Likes WHERE user_id = ? " + + " INTERSECT " + + " SELECT film_id FROM Likes WHERE user_id = ? " + + ") " + + "GROUP BY f.id " + + "ORDER BY COUNT(l.user_id) DESC"; private static final String FIND_GENRES_FOR_FILMS_QUERY = "SELECT fg.film_id, g.id as genre_id, g.name as genre_name " + - "FROM genre g " + - "JOIN film_genre fg ON g.id = fg.genre_id " + - "WHERE fg.film_id IN (:filmIds)"; + "FROM genre g " + + "JOIN film_genre fg ON g.id = fg.genre_id " + + "WHERE fg.film_id IN (:filmIds)"; private static final String DELETE_FILM_QUERY = "DELETE FROM Films WHERE id = ?"; private static final String FIND_DIRECTOR_FOR_FILMS_QUERY = """ @@ -312,9 +312,9 @@ public void removeLike(long filmId, long userId) { public List getTopFilms(int count, Integer genreId, Integer year) { StringBuilder queryBuilder = new StringBuilder("SELECT f.*, r.rating_id AS mpa_id, r.rating_name AS mpa_name " + - "FROM films AS f JOIN rating AS r on f.rating_id = r.rating_id " + - "JOIN film_genre AS fg ON f.id = fg.film_id " + - "LEFT JOIN likes AS l ON f.id = l.film_id WHERE 1=1" + "FROM films AS f JOIN rating AS r on f.rating_id = r.rating_id " + + "JOIN film_genre AS fg ON f.id = fg.film_id " + + "LEFT JOIN likes AS l ON f.id = l.film_id WHERE 1=1" ); List filterParams = new ArrayList<>(); diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/mapper/EventRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/dal/mapper/EventRowMapper.java new file mode 100644 index 0000000..f644c80 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/mapper/EventRowMapper.java @@ -0,0 +1,25 @@ +package ru.yandex.practicum.filmorate.dal.mapper; + +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 resultSet, int rowNum) throws SQLException { + Event event = new Event(); + event.setId(resultSet.getLong("event_id")); + event.setTimestamp(resultSet.getLong("timestamp")); + event.setUserId(resultSet.getLong("user_id")); + event.setEventType(EventType.valueOf(resultSet.getString(("event_type")))); + event.setOperation(Operation.valueOf(resultSet.getString("operation"))); + event.setEntityId(resultSet.getLong("entity_id")); + return event; + } +} 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..f43476d --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Event.java @@ -0,0 +1,14 @@ +package ru.yandex.practicum.filmorate.model; + +import lombok.Data; + +@Data +public class Event { + private long id; + private long timestamp; + private long userId; + private EventType eventType; + private Operation operation; + private 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..5de71e3 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/EventType.java @@ -0,0 +1,7 @@ +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..ca7ce05 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Operation.java @@ -0,0 +1,7 @@ +package ru.yandex.practicum.filmorate.model; + +public enum Operation { + REMOVE, + ADD, + UPDATE +} 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..a16bfdb --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/feed/FeedService.java @@ -0,0 +1,39 @@ +package ru.yandex.practicum.filmorate.service.feed; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.dal.FeedRepository; +import ru.yandex.practicum.filmorate.model.Event; +import ru.yandex.practicum.filmorate.model.EventType; +import ru.yandex.practicum.filmorate.model.Operation; + +import java.util.List; + +@Service +@Slf4j +public class FeedService { + + private final FeedRepository feedRepository; + + @Autowired + public FeedService(FeedRepository feedRepository) { + this.feedRepository = feedRepository; + } + + public List getUsersEventFeed(long userId) { + log.info("Обработка запроса на получение ленты событий пользователя с userId = {}", userId); + return feedRepository.getUsersEventFeed(userId); + } + + public void logEvent(long userId, EventType eventType, Operation op, long entityId) { + log.info("Сохранение события = {}, операция = {}, инициатор = {}, субъект = {}", eventType, op, userId, entityId); + Event event = new Event(); + event.setTimestamp(System.currentTimeMillis()); + event.setUserId(userId); + event.setEventType(eventType); + event.setOperation(op); + event.setEntityId(entityId); + feedRepository.saveEvent(event); + } +} 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 d5929f4..71b0b33 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 @@ -14,9 +14,8 @@ import ru.yandex.practicum.filmorate.mappers.FilmMapper; import ru.yandex.practicum.filmorate.mappers.GenreMapper; import ru.yandex.practicum.filmorate.mappers.RatingMapper; -import ru.yandex.practicum.filmorate.model.Film; -import ru.yandex.practicum.filmorate.model.Genre; -import ru.yandex.practicum.filmorate.model.Rating; +import ru.yandex.practicum.filmorate.model.*; +import ru.yandex.practicum.filmorate.service.feed.FeedService; import ru.yandex.practicum.filmorate.storage.film.FilmDbStorage; import ru.yandex.practicum.filmorate.storage.user.UserDbStorage; @@ -35,6 +34,7 @@ public class FilmService { private final FilmDbStorage filmStorage; private final UserDbStorage userStorage; + private final FeedService feedService; public Collection getFilms() { log.info("Getting all films"); @@ -78,6 +78,7 @@ public void addLikeToFilm(long filmId, long userId) { userExists(userId); filmStorage.addLike(filmId, userId); log.info("Like added successfully"); + feedService.logEvent(userId, EventType.LIKE, Operation.ADD, filmId); } @Transactional @@ -87,6 +88,7 @@ public void deleteLikeToFilm(long filmId, long userId) { userExists(userId); filmStorage.removeLike(filmId, userId); log.info("Like removed successfully"); + feedService.logEvent(userId, EventType.LIKE, Operation.REMOVE, filmId); } public List getTopFilms(int count, int genreId, int year) { diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/user/UserService.java b/src/main/java/ru/yandex/practicum/filmorate/service/user/UserService.java index 8f641bd..4077bff 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/user/UserService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/user/UserService.java @@ -12,7 +12,10 @@ import ru.yandex.practicum.filmorate.exception.InternalServerException; import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.mappers.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.service.feed.FeedService; import ru.yandex.practicum.filmorate.storage.user.UserDbStorage; import java.util.ArrayList; @@ -25,10 +28,12 @@ @Service public class UserService { private final UserDbStorage userDbStorage; + private final FeedService feedService; - @Autowired // Внедрение зависимостей через конструктор - public UserService(UserDbStorage userDbStorage) { + @Autowired + public UserService(UserDbStorage userDbStorage, FeedService feedService) { this.userDbStorage = userDbStorage; + this.feedService = feedService; } public void addFriend(long firstUserId, long secondUserId) { @@ -39,6 +44,7 @@ public void addFriend(long firstUserId, long secondUserId) { if (!userDbStorage.getFriends(firstUserId).contains(secondUserId)) { log.debug("add friends"); userDbStorage.addFriend(firstUserId, secondUserId); + feedService.logEvent(firstUserId, EventType.FRIEND, Operation.ADD, secondUserId); } else if (userDbStorage.getFriends(firstUserId).contains(secondUserId)) { log.warn("the friendship was already created"); throw new RuntimeException("the friendship was already created"); @@ -56,7 +62,9 @@ public void deleteFriend(long firstUserId, long secondUserId) { if (userDbStorage.getFriends(firstUserId).contains(secondUserId)) { log.debug("delete friends"); userDbStorage.deleteFriend(firstUserId, secondUserId); + feedService.logEvent(firstUserId, EventType.FRIEND, Operation.REMOVE, secondUserId); } + } public List getFriends(long userId) { diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 289841c..07a62fa 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -72,4 +72,15 @@ CREATE TABLE IF NOT EXISTS film_directors CONSTRAINT film_directors_pk PRIMARY KEY (id), FOREIGN KEY (film_id) REFERENCES Films (id) ON DELETE CASCADE, FOREIGN KEY (director_id) REFERENCES directors (director_id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS event_feed +( + event_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + timestamp BIGINT NOT NULL, + user_id BIGINT NOT NULL, + event_type VARCHAR(20) NOT NULL, + operation VARCHAR(20) NOT NULL, + entity_id BIGINT NOT NULL, + FOREIGN KEY (user_id) REFERENCES Users (id) ON DELETE CASCADE ); \ No newline at end of file From 40778cf0870985bf7a7d090dd2cc9581273e0e78 Mon Sep 17 00:00:00 2001 From: b1mgd Date: Mon, 28 Apr 2025 14:24:59 +0300 Subject: [PATCH 8/9] Add review endpoints --- pom.xml | 19 ++ .../controller/ReviewController.java | 92 +++++++++ .../filmorate/dal/ReviewRepository.java | 133 +++++++++++++ .../filmorate/dal/mapper/ReviewRowMapper.java | 26 +++ .../filmorate/dto/NewReviewRequest.java | 19 ++ .../practicum/filmorate/dto/ReviewDto.java | 13 ++ .../filmorate/dto/UpdateReviewRequest.java | 24 +++ .../filmorate/mappers/ReviewMapper.java | 48 +++++ .../practicum/filmorate/model/Review.java | 13 ++ .../service/review/ReviewService.java | 174 ++++++++++++++++++ .../filmorate/storage/film/FilmDbStorage.java | 2 + .../storage/review/ReviewStorage.java | 27 +++ .../filmorate/storage/user/UserDbStorage.java | 2 +- src/main/resources/application.properties | 6 +- src/main/resources/data.sql | 15 -- src/main/resources/schema.sql | 52 +++++- 16 files changed, 641 insertions(+), 24 deletions(-) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/controller/ReviewController.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dal/ReviewRepository.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dal/mapper/ReviewRowMapper.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dto/NewReviewRequest.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dto/ReviewDto.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dto/UpdateReviewRequest.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/mappers/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/service/review/ReviewService.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/review/ReviewStorage.java diff --git a/pom.xml b/pom.xml index 4f7f5f6..14702dc 100644 --- a/pom.xml +++ b/pom.xml @@ -99,6 +99,25 @@ + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.3.1 + + + + check + + compile + + + + checkstyle.xml + true + + + 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..f4fa2dd --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/ReviewController.java @@ -0,0 +1,92 @@ +package ru.yandex.practicum.filmorate.controller; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.dto.NewReviewRequest; +import ru.yandex.practicum.filmorate.dto.ReviewDto; +import ru.yandex.practicum.filmorate.dto.UpdateReviewRequest; +import ru.yandex.practicum.filmorate.service.review.ReviewService; + +import java.util.List; + +@Slf4j +@RestController +@RequestMapping("/reviews") +public class ReviewController { + private final ReviewService reviewService; + + public ReviewController(ReviewService reviewService) { + this.reviewService = reviewService; + } + + @GetMapping + public List getFilmReviews(@RequestParam(defaultValue = "0") long filmId, + @RequestParam(defaultValue = "10") + @Positive(message = "Count must be positive") long count) { + log.info("Received GET {} reviews for film with ID = {}", count, filmId); + return reviewService.getFilmReviews(filmId, count); + } + + @GetMapping("{reviewId}") + @ResponseStatus(HttpStatus.OK) + public ReviewDto getReviewById(@PathVariable int reviewId) { + log.info("Received GET review with ID = {}", reviewId); + return reviewService.getReviewById(reviewId); + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public ReviewDto addReview(@RequestBody @Valid NewReviewRequest request) { + log.info("Received POST new review: {}", request); + return reviewService.addReview(request); + } + + @PutMapping + @ResponseStatus(HttpStatus.OK) + public ReviewDto updateReview(@RequestBody @Valid UpdateReviewRequest request) { + log.info("Received UPDATE review: {}", request); + return reviewService.updateReview(request); + } + + @DeleteMapping("/{reviewId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteReview(@PathVariable long reviewId) { + log.info("Received DELETE review with ID = {}", reviewId); + reviewService.deleteReview(reviewId); + } + + @PutMapping("/{reviewId}/like/{userId}") + @ResponseStatus(HttpStatus.OK) + public void likeReview(@PathVariable long reviewId, + @PathVariable long userId) { + log.info("Received LIKE review with reviewId = {} by user with userId = {}", reviewId, userId); + reviewService.likeReview(reviewId, userId); + } + + @PutMapping("/{reviewId}/dislike/{userId}") + @ResponseStatus(HttpStatus.OK) + public void dislikeReview(@PathVariable long reviewId, + @PathVariable long userId) { + log.info("Received DISLIKE review with reviewId = {} by user with userId = {}", reviewId, userId); + reviewService.dislikeReview(reviewId, userId); + } + + @DeleteMapping("/{reviewId}/like/{userId}") + @ResponseStatus(HttpStatus.OK) + public void removeLike(@PathVariable long reviewId, + @PathVariable long userId) { + log.info("Received REMOVE LIKE from review with reviewId = {} by user with userId = {}", reviewId, userId); + reviewService.removeLike(reviewId, userId); + } + + @DeleteMapping("/{reviewId}/dislike/{userId}") + @ResponseStatus(HttpStatus.OK) + public void removeDislike(@PathVariable long reviewId, + @PathVariable long userId) { + log.info("Received REMOVE DISLIKE from review with reviewId = {} by user with userId = {}", reviewId, userId); + reviewService.removeDislike(reviewId, userId); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/ReviewRepository.java b/src/main/java/ru/yandex/practicum/filmorate/dal/ReviewRepository.java new file mode 100644 index 0000000..2f506ec --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/ReviewRepository.java @@ -0,0 +1,133 @@ +package ru.yandex.practicum.filmorate.dal; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.dal.mapper.ReviewRowMapper; +import ru.yandex.practicum.filmorate.model.Review; +import ru.yandex.practicum.filmorate.storage.review.ReviewStorage; + +import java.util.List; +import java.util.Optional; + +/* +Не стал оформлять ReviewDbStorage, так как в остальной программе такие классы лишь делигируют вызовы классу-репозиторию. +В моем понимании ReviewDbStorage по заданию предыдущего спринта и есть ReviewRepository и т.п. + */ + +@Qualifier("reviewRepository") +@Repository +public class ReviewRepository extends BaseRepository implements ReviewStorage { + + private static final String GET_FILM_REVIEWS = + "SELECT r.review_id, " + + "r.content, " + + "r.is_positive, " + + "r.user_id, " + + "r.film_id, " + + "(SELECT COALESCE(SUM(CASE WHEN rl.is_like THEN 1 ELSE -1 END), 0) " + + "FROM review_likes rl " + + "WHERE rl.review_id = r.review_id) AS useful " + + "FROM reviews r " + + "WHERE r.film_id = ? " + + "ORDER BY useful DESC, r.review_id ASC " + + "LIMIT ?"; + private static final String GET_REVIEW_BY_REVIEW_ID = + "SELECT r.review_id, " + + "r.content, " + + "r.is_positive, " + + "r.user_id, " + + "r.film_id, " + + "(SELECT COALESCE(SUM(CASE WHEN rl.is_like THEN 1 ELSE -1 END), 0) " + + "FROM review_likes rl " + + "WHERE rl.review_id = r.review_id) AS useful " + + "FROM reviews r " + + "WHERE r.review_id = ?"; + private static final String GET_ALL_REVIEWS = + "SELECT r.review_id, " + + "r.content, " + + "r.is_positive, " + + "r.user_id, " + + "r.film_id, " + + "(SELECT COALESCE(SUM(CASE WHEN rl.is_like THEN 1 ELSE -1 END), 0) " + + "FROM review_likes rl " + + "WHERE rl.review_id = r.review_id) AS useful " + + "FROM reviews r " + + "ORDER BY useful DESC, r.review_id ASC " + + "LIMIT ?"; + + private static final String POST_REVIEW = "INSERT INTO reviews (content, is_positive, user_id, film_id) " + + "VALUES (?, ?, ?, ?)"; + + private static final String PUT_REVIEW = "UPDATE reviews " + + "SET content = ?, is_positive = ?, user_id = ?, film_id = ? WHERE review_id = ?"; + + private static final String DELETE_REVIEW = "DELETE FROM reviews WHERE review_id = ?"; + + private static final String RATE_REVIEW = "MERGE INTO review_likes (review_id, user_id, is_like) VALUES (?, ?, ?)"; + private static final String REMOVE_RATE = "DELETE FROM review_likes WHERE review_id = ? AND user_id = ?"; + + private static final String CHECK_RATING = "SELECT is_like FROM review_likes WHERE review_id = ? AND user_id = ?"; + + public ReviewRepository(JdbcTemplate jdbc, ReviewRowMapper mapper) { + super(jdbc, mapper); + } + + @Override + public List getFilmReviews(long filmId, long count) { + return findMany(GET_FILM_REVIEWS, filmId, count); + } + + @Override + public List getAllReviews(long count) { + return findMany(GET_ALL_REVIEWS, count); + } + + @Override + public Optional getReviewById(long reviewId) { + return findOne(GET_REVIEW_BY_REVIEW_ID, reviewId); + } + + @Override + public Review addReview(Review newReview) { + long id = insert( + POST_REVIEW, + newReview.getContent(), + newReview.getIsPositive(), + newReview.getUserId(), + newReview.getFilmId() + ); + newReview.setReviewId(id); + + return newReview; + } + + @Override + public boolean deleteReview(long reviewId) { + return delete(DELETE_REVIEW, reviewId); + } + + @Override + public boolean likeReview(long reviewId, long userId) { + int rowsAdded = jdbc.update(RATE_REVIEW, reviewId, userId, true); + return rowsAdded > 0; + } + + @Override + public boolean removeLike(long reviewId, long userId) { + int rowsRemoved = jdbc.update(REMOVE_RATE, reviewId, userId); + return rowsRemoved > 0; + } + + @Override + public boolean dislikeReview(long reviewId, long userId) { + int rowsAdded = jdbc.update(RATE_REVIEW, reviewId, userId, false); + return rowsAdded > 0; + } + + @Override + public boolean removeDislike(long reviewId, long userId) { + int rowsRemoved = jdbc.update(REMOVE_RATE, reviewId, userId); + return rowsRemoved > 0; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/mapper/ReviewRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/dal/mapper/ReviewRowMapper.java new file mode 100644 index 0000000..3a3ef42 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/mapper/ReviewRowMapper.java @@ -0,0 +1,26 @@ +package ru.yandex.practicum.filmorate.dal.mapper; + +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 resultSet, int rowNum) throws SQLException { + Review review = new Review(); + + review.setReviewId(resultSet.getLong("review_id")); + review.setContent(resultSet.getString("content")); + review.setIsPositive(resultSet.getBoolean("is_positive")); + review.setUserId(resultSet.getLong("user_id")); + review.setFilmId(resultSet.getLong("film_id")); + review.setUseful(resultSet.getInt("useful")); + + return review; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/NewReviewRequest.java b/src/main/java/ru/yandex/practicum/filmorate/dto/NewReviewRequest.java new file mode 100644 index 0000000..18e5a6d --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/NewReviewRequest.java @@ -0,0 +1,19 @@ +package ru.yandex.practicum.filmorate.dto; + +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +public class NewReviewRequest { + @NotNull + private String content; + @NotNull + private Boolean isPositive; + /* + POST тесты в postman требуют возавращать 404 при неверных значения userId и filmId. + В это время стандартный Spring-обработчик вернет 400 (аннотация @Positive) + Реализовал обработку вручную в ReviewService с выбрасыванием исключения + */ + private Long userId; + private Long filmId; +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/ReviewDto.java b/src/main/java/ru/yandex/practicum/filmorate/dto/ReviewDto.java new file mode 100644 index 0000000..7c25e6e --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/ReviewDto.java @@ -0,0 +1,13 @@ +package ru.yandex.practicum.filmorate.dto; + +import lombok.Data; + +@Data +public class ReviewDto { + private Long reviewId; + private String content; + private Boolean isPositive; + private Long userId; + private Long filmId; + private Integer useful; +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/UpdateReviewRequest.java b/src/main/java/ru/yandex/practicum/filmorate/dto/UpdateReviewRequest.java new file mode 100644 index 0000000..259166b --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/UpdateReviewRequest.java @@ -0,0 +1,24 @@ +package ru.yandex.practicum.filmorate.dto; + +import jakarta.validation.constraints.Positive; +import lombok.Data; + +@Data +public class UpdateReviewRequest { + @Positive + private Long reviewId; + private String content; + private Boolean isPositive; + @Positive + private Long userId; + @Positive + private Long filmId; + + public boolean hasContent() { + return content != null && !content.isBlank(); + } + + public boolean hasRating() { + return isPositive != null; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/mappers/ReviewMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mappers/ReviewMapper.java new file mode 100644 index 0000000..6b0f550 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/mappers/ReviewMapper.java @@ -0,0 +1,48 @@ +package ru.yandex.practicum.filmorate.mappers; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import ru.yandex.practicum.filmorate.dto.NewReviewRequest; +import ru.yandex.practicum.filmorate.dto.ReviewDto; +import ru.yandex.practicum.filmorate.dto.UpdateReviewRequest; +import ru.yandex.practicum.filmorate.model.Review; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class ReviewMapper { + + public static Review mapToReview(NewReviewRequest request) { + Review review = new Review(); + + review.setContent(request.getContent()); + review.setIsPositive(request.getIsPositive()); + review.setUserId(request.getUserId()); + review.setFilmId(request.getFilmId()); + + return review; + } + + public static ReviewDto mapToReviewDto(Review review) { + ReviewDto reviewDto = new ReviewDto(); + + reviewDto.setReviewId(review.getReviewId()); + reviewDto.setContent(review.getContent()); + reviewDto.setIsPositive(review.getIsPositive()); + reviewDto.setUserId(review.getUserId()); + reviewDto.setFilmId(review.getFilmId()); + reviewDto.setUseful(review.getUseful()); + + return reviewDto; + } + + public static Review updateReviewFields(Review review, UpdateReviewRequest request) { + if (request.hasContent()) { + review.setContent(request.getContent()); + } + + if (request.hasRating()) { + review.setIsPositive(request.getIsPositive()); + } + + return review; + } +} 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..4391ef1 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Review.java @@ -0,0 +1,13 @@ +package ru.yandex.practicum.filmorate.model; + +import lombok.Data; + +@Data +public class Review { + private Long reviewId; + private String content; + private Boolean isPositive; + private Long userId; + private Long filmId; + private Integer useful; +} 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 new file mode 100644 index 0000000..b3af550 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/review/ReviewService.java @@ -0,0 +1,174 @@ +package ru.yandex.practicum.filmorate.service.review; + +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.yandex.practicum.filmorate.dto.NewReviewRequest; +import ru.yandex.practicum.filmorate.dto.ReviewDto; +import ru.yandex.practicum.filmorate.dto.UpdateReviewRequest; +import ru.yandex.practicum.filmorate.exception.InternalServerException; +import ru.yandex.practicum.filmorate.exception.MethodArgumentNotValidException; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.mappers.ReviewMapper; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.model.Review; +import ru.yandex.practicum.filmorate.storage.film.FilmStorage; +import ru.yandex.practicum.filmorate.storage.review.ReviewStorage; +import ru.yandex.practicum.filmorate.storage.user.UserStorage; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Slf4j +@Service +public class ReviewService { + private final ReviewStorage reviewStorage; + private final FilmStorage filmStorage; + private final UserStorage userStorage; + + public ReviewService(@Qualifier("reviewRepository") ReviewStorage reviewStorage, + @Qualifier("filmDbStorage") FilmStorage filmStorage, + @Qualifier("userDbStorage") UserStorage userStorage) { + this.reviewStorage = reviewStorage; + this.filmStorage = filmStorage; + this.userStorage = userStorage; + } + + public List getFilmReviews(long filmId, long count) { + if (count <= 0) { + throw new MethodArgumentNotValidException("Count must be greater than 0"); + } + + if (filmId == 0) { + log.info("Showing {} reviews for all films", count); + return reviewStorage.getAllReviews(count) + .stream() + .map(ReviewMapper::mapToReviewDto) + .collect(Collectors.toList()); + } + + Film film = filmStorage.getFilmById(filmId); + log.info("Showing {} reviews for film: {}", count, film); + + return reviewStorage.getFilmReviews(filmId, count) + .stream() + .map(ReviewMapper::mapToReviewDto) + .collect(Collectors.toList()); + } + + public ReviewDto getReviewById(long reviewId) { + Optional optionalReview = reviewStorage.getReviewById(reviewId); + return optionalReview.map(review -> { + ReviewDto reviewDto = ReviewMapper.mapToReviewDto(review); + log.info("Found review: {}", reviewDto); + return reviewDto; + }) + .orElseThrow(() -> new NotFoundException("Review with ID " + reviewId + " not found")); + } + + @Transactional + public ReviewDto addReview(@Valid NewReviewRequest request) { + /* + Ручная обработка из-за специфичных postman тестов + */ + if (request.getUserId() <= 0) { + throw new NotFoundException("User id must be greater than 0"); + } + + if (request.getFilmId() <= 0) { + throw new NotFoundException("Film id must be greater than 0"); + } + + Review review = ReviewMapper.mapToReview(request); + ReviewDto reviewDto = ReviewMapper.mapToReviewDto(reviewStorage.addReview(review)); + log.info("Review successfully added"); + return reviewDto; + } + + @Transactional + public ReviewDto updateReview(@Valid UpdateReviewRequest request) { + Review updatedReview = reviewStorage.getReviewById(request.getReviewId()) + .map(review -> ReviewMapper.updateReviewFields(review, request)) + .orElseThrow(() -> new NotFoundException("Review with ID " + request.getReviewId() + " not found")); + + return ReviewMapper.mapToReviewDto(updatedReview); + } + + @Transactional + public void deleteReview(long reviewId) { + ReviewDto reviewDto = getReviewById(reviewId); + log.info("Deleting review: {}", reviewDto); + + boolean isDeleted = reviewStorage.deleteReview(reviewId); + + if (isDeleted) { + log.info("Review deleted successfully"); + } else { + throw new InternalServerException("Review was not deleted due to internal server error"); + } + } + + @Transactional + public void likeReview(long reviewId, long userId) { + checkReviewAndUser(reviewId, userId); + log.info("User with userId = {} wants to like review with reviewId = : {}", userId, reviewId); + + boolean isSuccessful = reviewStorage.likeReview(reviewId, userId); + + if (isSuccessful) { + log.info("User liked review successfully"); + } else { + throw new InternalServerException("Failed to like review due to internal server error"); + } + } + + @Transactional + public void removeLike(long reviewId, long userId) { + checkReviewAndUser(reviewId, userId); + log.info("User with userId = {} wants to remove like from review with reviewId = {}", userId, reviewId); + + boolean isSuccessful = reviewStorage.removeLike(reviewId, userId); + + if (isSuccessful) { + log.info("Like removed successfully"); + } else { + throw new InternalServerException("Failed to remove like due to internal server error"); + } + } + + @Transactional + public void dislikeReview(long reviewId, long userId) { + checkReviewAndUser(reviewId, userId); + log.info("User with userId = {} wants to dislike review with reviewId = {}", userId, reviewId); + + boolean isSuccessful = reviewStorage.dislikeReview(reviewId, userId); + + if (isSuccessful) { + log.info("User disliked review successfully"); + } else { + throw new InternalServerException("Failed to dislike review due to internal server error"); + } + } + + @Transactional + public void removeDislike(long reviewId, long userId) { + checkReviewAndUser(reviewId, userId); + log.info("User with userId = {} wants to remove dislike from review with reviewId = {}", userId, reviewId); + + boolean isSuccessful = reviewStorage.removeDislike(reviewId, userId); + + if (isSuccessful) { + log.info("Dislike removed successfully"); + } else { + throw new InternalServerException("Failed to remove dislike due to internal server error"); + } + } + + private void checkReviewAndUser(long reviewId, long userId) { + getReviewById(reviewId); + userStorage.getUserById(userId); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java index cd8de11..981d5c3 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java @@ -1,5 +1,6 @@ package ru.yandex.practicum.filmorate.storage.film; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Repository; import ru.yandex.practicum.filmorate.dal.FilmRepository; import ru.yandex.practicum.filmorate.dal.GenreRepository; @@ -12,6 +13,7 @@ import java.util.*; import java.util.stream.Collectors; +@Qualifier("filmDbStorage") @Repository public class FilmDbStorage implements FilmStorage { diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/review/ReviewStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/review/ReviewStorage.java new file mode 100644 index 0000000..610f18d --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/review/ReviewStorage.java @@ -0,0 +1,27 @@ +package ru.yandex.practicum.filmorate.storage.review; + +import ru.yandex.practicum.filmorate.model.Review; + +import java.util.List; +import java.util.Optional; + +public interface ReviewStorage { + + List getFilmReviews(long filmId, long count); + + List getAllReviews(long count); + + Optional getReviewById(long reviewId); + + Review addReview(Review newReview); + + boolean deleteReview(long reviewId); + + boolean likeReview(long reviewId, long likeId); + + boolean removeLike(long reviewId, long likeId); + + boolean dislikeReview(long reviewId, long likeId); + + boolean removeDislike(long reviewId, long dislikeId); +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java index 04242c0..e84b076 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java @@ -9,7 +9,7 @@ import java.util.Collection; import java.util.Set; -@Qualifier +@Qualifier("userDbStorage") @Repository public class UserDbStorage implements UserStorage { private final UserRepository repository; diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 2776f53..8da35bf 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,7 +1,11 @@ +logging.level.root=INFO logging.level.org.zalando.logbook=INFO logging.level.ru.yandex.practicum.filmorate=INFO 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 + +spring.main.banner-mode=off \ No newline at end of file diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index ec1f723..c04552a 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -1,18 +1,3 @@ -delete -from FILMS; -delete -from LIKES; -delete -from GENRE; -delete -from RATING; -delete -from USERS; -delete -from FILM_GENRE; -delete -from FRIENDSHIP; - insert into RATING(rating_id, rating_name) values (1, 'G'), (2, 'PG'), diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 289841c..b55905c 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -1,3 +1,15 @@ +DROP TABLE IF EXISTS Film_genre; +DROP TABLE IF EXISTS Likes; +DROP TABLE IF EXISTS Friendship; +DROP TABLE IF EXISTS Genre; +DROP TABLE IF EXISTS film_directors; +DROP TABLE IF EXISTS review_likes; +DROP TABLE IF EXISTS reviews; +DROP TABLE IF EXISTS Films; +DROP TABLE IF EXISTS Rating; +DROP TABLE IF EXISTS Users; +DROP TABLE IF EXISTS directors; + CREATE TABLE IF NOT EXISTS Genre ( id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, @@ -59,17 +71,43 @@ CREATE TABLE IF NOT EXISTS Film_genre CREATE TABLE IF NOT EXISTS directors ( - director_id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY, - name varchar(255) NOT NULL, - CONSTRAINT directors_pk PRIMARY KEY (director_id) + director_id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY, + name varchar(255) NOT NULL, + CONSTRAINT directors_pk PRIMARY KEY (director_id) ); CREATE TABLE IF NOT EXISTS film_directors ( - id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY, - film_id BIGINT NOT NULL, - director_id BIGINT NOT NULL, - CONSTRAINT film_directors_pk PRIMARY KEY (id), + id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY, + film_id BIGINT NOT NULL, + director_id BIGINT NOT NULL, + CONSTRAINT film_directors_pk PRIMARY KEY (id), FOREIGN KEY (film_id) REFERENCES Films (id) ON DELETE CASCADE, FOREIGN KEY (director_id) REFERENCES directors (director_id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS reviews +( + review_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + content TEXT, + is_positive BOOLEAN NOT NULL, + user_id BIGINT NOT NULL, + film_id BIGINT NOT NULL, + CONSTRAINT reviews_user_id_fk + FOREIGN KEY (user_id) REFERENCES Users ON DELETE CASCADE, + CONSTRAINT reviews_film_id_fk + FOREIGN KEY (film_id) REFERENCES Films ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS review_likes +( + review_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + is_like BOOLEAN NOT NULL, + CONSTRAINT review_likes_pk + PRIMARY KEY (review_id, user_id), + CONSTRAINT review_likes_review_id_fk + FOREIGN KEY (review_id) REFERENCES reviews ON DELETE CASCADE, + CONSTRAINT review_likes_user_id_fk + FOREIGN KEY (user_id) REFERENCES Users ON DELETE CASCADE ); \ No newline at end of file From 96e4576e0708d3694dc919c8e70fcac45a853ba0 Mon Sep 17 00:00:00 2001 From: nesailormun Date: Mon, 28 Apr 2025 16:09:35 +0300 Subject: [PATCH 9/9] =?UTF-8?q?=D0=94=D0=BE=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=B0=D0=BD=D0=B0=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=BE?= =?UTF-8?q?=D0=BD=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20"?= =?UTF-8?q?=D0=9B=D0=B5=D0=BD=D1=82=D0=B0=20=D0=BD=D0=BE=D0=B2=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B5=D0=B9"=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=20=D0=B4?= =?UTF-8?q?=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=84?= =?UTF-8?q?=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=BE=D0=BD=D0=B0=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D0=BE=D1=81=D1=82=D0=B8=20"=D0=9E=D1=82=D0=B7=D1=8B?= =?UTF-8?q?=D0=B2=D1=8B"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../filmorate/dal/FeedRepository.java | 2 +- .../filmorate/service/feed/FeedService.java | 3 +++ .../filmorate/service/film/FilmService.java | 1 + .../service/review/ReviewService.java | 19 +++++++++++++------ .../filmorate/service/user/UserService.java | 1 - src/main/resources/schema.sql | 1 + 6 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/FeedRepository.java b/src/main/java/ru/yandex/practicum/filmorate/dal/FeedRepository.java index bb6bbce..96e866d 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/FeedRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/FeedRepository.java @@ -19,7 +19,7 @@ public FeedRepository(JdbcTemplate jdbc, EventRowMapper eventRowMapper) { } public List getUsersEventFeed(long userId) { - String sqlQuery = "SELECT * FROM event_feed WHERE user_id = ? ORDER BY timestamp DESC;"; + String sqlQuery = "SELECT * FROM event_feed WHERE user_id = ?;"; return jdbc.query(sqlQuery, eventRowMapper, userId); } 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 index a16bfdb..0f35c67 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/feed/FeedService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/feed/FeedService.java @@ -3,6 +3,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import ru.yandex.practicum.filmorate.dal.FeedRepository; import ru.yandex.practicum.filmorate.model.Event; import ru.yandex.practicum.filmorate.model.EventType; @@ -21,11 +22,13 @@ public FeedService(FeedRepository feedRepository) { this.feedRepository = feedRepository; } + @Transactional public List getUsersEventFeed(long userId) { log.info("Обработка запроса на получение ленты событий пользователя с userId = {}", userId); return feedRepository.getUsersEventFeed(userId); } + @Transactional public void logEvent(long userId, EventType eventType, Operation op, long entityId) { log.info("Сохранение события = {}, операция = {}, инициатор = {}, субъект = {}", eventType, op, userId, entityId); Event event = new Event(); 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 65c9f4d..ad49815 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 @@ -91,6 +91,7 @@ public void deleteLikeToFilm(long filmId, long userId) { feedService.logEvent(userId, EventType.LIKE, Operation.REMOVE, filmId); } + @Transactional public List getTopFilms(int count, int genreId, int year) { log.info("Getting top {} popular films with genreId = {} and release year = {}", count, genreId, year); if (count <= 0) { 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 b3af550..ab74aa3 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 @@ -12,8 +12,11 @@ import ru.yandex.practicum.filmorate.exception.MethodArgumentNotValidException; import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.mappers.ReviewMapper; +import ru.yandex.practicum.filmorate.model.EventType; import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.model.Operation; import ru.yandex.practicum.filmorate.model.Review; +import ru.yandex.practicum.filmorate.service.feed.FeedService; import ru.yandex.practicum.filmorate.storage.film.FilmStorage; import ru.yandex.practicum.filmorate.storage.review.ReviewStorage; import ru.yandex.practicum.filmorate.storage.user.UserStorage; @@ -28,13 +31,16 @@ public class ReviewService { private final ReviewStorage reviewStorage; private final FilmStorage filmStorage; private final UserStorage userStorage; + private final FeedService feedService; public ReviewService(@Qualifier("reviewRepository") ReviewStorage reviewStorage, @Qualifier("filmDbStorage") FilmStorage filmStorage, - @Qualifier("userDbStorage") UserStorage userStorage) { + @Qualifier("userDbStorage") UserStorage userStorage, + FeedService feedService) { this.reviewStorage = reviewStorage; this.filmStorage = filmStorage; this.userStorage = userStorage; + this.feedService = feedService; } public List getFilmReviews(long filmId, long count) { @@ -62,9 +68,9 @@ public List getFilmReviews(long filmId, long count) { public ReviewDto getReviewById(long reviewId) { Optional optionalReview = reviewStorage.getReviewById(reviewId); return optionalReview.map(review -> { - ReviewDto reviewDto = ReviewMapper.mapToReviewDto(review); - log.info("Found review: {}", reviewDto); - return reviewDto; + ReviewDto reviewDto = ReviewMapper.mapToReviewDto(review); + log.info("Found review: {}", reviewDto); + return reviewDto; }) .orElseThrow(() -> new NotFoundException("Review with ID " + reviewId + " not found")); } @@ -84,6 +90,7 @@ public ReviewDto addReview(@Valid NewReviewRequest request) { Review review = ReviewMapper.mapToReview(request); ReviewDto reviewDto = ReviewMapper.mapToReviewDto(reviewStorage.addReview(review)); + feedService.logEvent(review.getUserId(), EventType.REVIEW, Operation.ADD, review.getReviewId()); log.info("Review successfully added"); return reviewDto; } @@ -93,7 +100,7 @@ public ReviewDto updateReview(@Valid UpdateReviewRequest request) { Review updatedReview = reviewStorage.getReviewById(request.getReviewId()) .map(review -> ReviewMapper.updateReviewFields(review, request)) .orElseThrow(() -> new NotFoundException("Review with ID " + request.getReviewId() + " not found")); - + feedService.logEvent(updatedReview.getUserId(), EventType.REVIEW, Operation.UPDATE, updatedReview.getReviewId()); return ReviewMapper.mapToReviewDto(updatedReview); } @@ -103,8 +110,8 @@ public void deleteReview(long reviewId) { log.info("Deleting review: {}", reviewDto); boolean isDeleted = reviewStorage.deleteReview(reviewId); - if (isDeleted) { + feedService.logEvent(reviewDto.getUserId(), EventType.REVIEW, Operation.REMOVE, reviewId); log.info("Review deleted successfully"); } else { throw new InternalServerException("Review was not deleted due to internal server error"); diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/user/UserService.java b/src/main/java/ru/yandex/practicum/filmorate/service/user/UserService.java index 4077bff..13639f9 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/user/UserService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/user/UserService.java @@ -64,7 +64,6 @@ public void deleteFriend(long firstUserId, long secondUserId) { userDbStorage.deleteFriend(firstUserId, secondUserId); feedService.logEvent(firstUserId, EventType.FRIEND, Operation.REMOVE, secondUserId); } - } public List getFriends(long userId) { diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 220f0c0..c09d08b 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -1,3 +1,4 @@ +DROP TABLE IF EXISTS event_feed; DROP TABLE IF EXISTS Film_genre; DROP TABLE IF EXISTS Likes; DROP TABLE IF EXISTS Friendship;