From fe0e1a8df4260b639c75ec555bfd9dbb6926feb9 Mon Sep 17 00:00:00 2001 From: Kinza Date: Tue, 6 Oct 2026 20:28:46 +0300 Subject: [PATCH 1/3] Add genre and year filters to popular films endpoint --- .../filmorate/controller/FilmController.java | 8 ++-- .../filmorate/service/FilmService.java | 14 ++++++- .../filmorate/storage/film/FilmDbStorage.java | 41 +++++++++++++++++-- .../filmorate/storage/film/FilmStorage.java | 2 +- .../storage/film/FilmDbStorageTest.java | 4 +- 5 files changed, 57 insertions(+), 12 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 21c3dda..961959c 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -59,8 +59,10 @@ public Film removeLike(@PathVariable long id, @PathVariable long userId) { @GetMapping("/popular") public Collection getPopular( - @RequestParam(defaultValue = "10") int count) { - log.info("GET /films/popular?count={}", count); - return filmService.getPopular(count); + @RequestParam(defaultValue = "10") int count, + @RequestParam(required = false) Integer genreId, + @RequestParam(required = false) Integer year) { + log.info("GET /films/popular?count={}&genreId={}&year={}", count, genreId, year); + return filmService.getPopular(count, genreId, year); } } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java index 160de6e..beee424 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java @@ -77,14 +77,24 @@ public Film removeLike(long filmId, long userId) { return film; } - public Collection getPopular(int count) { + public Collection getPopular(int count, Integer genreId, Integer year) { // Валидация: count должен быть положительным числом if (count <= 0) { throw new ValidationException( "Количество фильмов должно быть положительным числом, получено: " + count ); } - return filmStorage.findPopular(count); + // Жанр, если указан, должен существовать — иначе 404 + if (genreId != null) { + genreService.findById(genreId); + } + // Валидация: год, если указан, должен быть положительным + if (year != null && year <= 0) { + throw new ValidationException( + "Год должен быть положительным числом, получено: " + year + ); + } + return filmStorage.findPopular(count, genreId, year); } // Валидация даты релиза — не может быть раньше дня рождения кино 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 df032fd..36e2435 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 @@ -33,11 +33,21 @@ public class FilmDbStorage implements FilmStorage { + "LEFT JOIN mpa m ON f.mpa_id = m.id "; private static final String FIND_ALL_QUERY = SELECT_FILMS + "ORDER BY f.id"; private static final String FIND_BY_ID_QUERY = SELECT_FILMS + "WHERE f.id = ?"; - private static final String FIND_POPULAR_QUERY = SELECT_FILMS - + "LEFT JOIN film_likes fl ON f.id = fl.film_id " + + // Базовый запрос для популярных фильмов: WHERE-условия добавляются динамически + private static final String FIND_POPULAR_BASE_QUERY = SELECT_FILMS + + "LEFT JOIN film_likes fl ON f.id = fl.film_id "; + // Хвост запроса популярных фильмов: группировка, сортировка и лимит + private static final String FIND_POPULAR_TAIL_QUERY = " " + "GROUP BY f.id, f.name, f.description, f.release_date, f.duration, f.mpa_id, m.name " + "ORDER BY COUNT(fl.user_id) DESC, f.id " + "LIMIT ?"; + + // Условия фильтрации популярных фильмов: по жанру и по году выпуска + private static final String FIND_POPULAR_GENRE_FILTER = + "f.id IN (SELECT film_id FROM film_genres WHERE genre_id = ?)"; + private static final String FIND_POPULAR_YEAR_FILTER = + "YEAR(f.release_date) = ?"; private static final String INSERT_QUERY = "INSERT INTO films (name, description, release_date, duration, mpa_id) VALUES (?, ?, ?, ?, ?)"; private static final String UPDATE_QUERY = @@ -124,8 +134,22 @@ public Optional findById(long id) { } @Override - public List findPopular(int count) { - List films = jdbc.query(FIND_POPULAR_QUERY, mapper, count); + public List findPopular(int count, Integer genreId, Integer year) { + List conditions = new ArrayList<>(); + List params = new ArrayList<>(); + + if (genreId != null) { + // Подзапрос вместо JOIN: иначе фильм с несколькими жанрами задвоит строки + conditions.add(FIND_POPULAR_GENRE_FILTER); + params.add(genreId); + } + if (year != null) { + conditions.add(FIND_POPULAR_YEAR_FILTER); + params.add(year); + } + params.add(count); // LIMIT всегда последний + + List films = jdbc.query(buildPopularQuery(conditions), mapper, params.toArray()); loadGenresForFilms(films); return films; } @@ -141,6 +165,15 @@ public boolean removeLike(long filmId, long userId) { return jdbc.update(REMOVE_LIKE_QUERY, filmId, userId) > 0; } + // Собирает запрос популярных фильмов: базовое чтение + WHERE по условиям + группировка с лимитом + private String buildPopularQuery(List conditions) { + StringBuilder sql = new StringBuilder(FIND_POPULAR_BASE_QUERY); + if (!conditions.isEmpty()) { + sql.append("WHERE ").append(String.join(" AND ", conditions)).append(" "); + } + return sql.append(FIND_POPULAR_TAIL_QUERY).toString(); + } + // Рейтинга может не быть — тогда в колонку mpa_id уходит NULL private Integer getMpaId(Film film) { return film.getMpa() == null ? null : film.getMpa().getId(); 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 501bf43..d1d178c 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 @@ -18,7 +18,7 @@ public interface FilmStorage { Optional findById(long id); - List findPopular(int count); + List findPopular(int count, Integer genreId, Integer year); void addLike(long filmId, long userId); diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorageTest.java index 002d03a..ab48cb3 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorageTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorageTest.java @@ -158,10 +158,10 @@ void shouldFindPopularByLikes() { filmStorage.addLike(second.getId(), boris.getId()); filmStorage.addLike(first.getId(), anna.getId()); - List popular = filmStorage.findPopular(10); + List popular = filmStorage.findPopular(10, null, null); assertEquals(List.of(second.getId(), first.getId(), third.getId()), popular.stream().map(Film::getId).toList()); - assertEquals(2, filmStorage.findPopular(2).size()); + assertEquals(2, filmStorage.findPopular(2, null, null).size()); } } \ No newline at end of file From 2f00c948920603af5caa764feb473e5e3d2568fb Mon Sep 17 00:00:00 2001 From: Kinza Date: Tue, 6 Oct 2026 20:35:02 +0300 Subject: [PATCH 2/3] Cover popular films filtering by genre and year with tests --- .../storage/film/FilmDbStorageTest.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorageTest.java index ab48cb3..b9ad5e6 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorageTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorageTest.java @@ -164,4 +164,63 @@ void shouldFindPopularByLikes() { popular.stream().map(Film::getId).toList()); assertEquals(2, filmStorage.findPopular(2, null, null).size()); } + + @Test + void shouldFilterPopularByGenre() { + Film comedy = createFilm("Комедия"); + comedy.getGenres().add(genre(1)); + filmStorage.update(comedy); + Film drama = createFilm("Драма"); + drama.getGenres().add(genre(2)); + filmStorage.update(drama); + User anna = createUser("anna"); + filmStorage.addLike(comedy.getId(), anna.getId()); + filmStorage.addLike(drama.getId(), anna.getId()); + + List popular = filmStorage.findPopular(10, 1, null); + + assertEquals(List.of(comedy.getId()), popular.stream().map(Film::getId).toList()); + } + + @Test + void shouldFilterPopularByYear() { + Film old = createFilm("Старый"); + Film fresh = newFilm("Новый"); + fresh.setReleaseDate(LocalDate.of(2020, 1, 1)); + fresh = filmStorage.add(fresh); + User anna = createUser("anna"); + filmStorage.addLike(old.getId(), anna.getId()); + filmStorage.addLike(fresh.getId(), anna.getId()); + + List popular = filmStorage.findPopular(10, null, 2000); + + assertEquals(List.of(old.getId()), popular.stream().map(Film::getId).toList()); + } + + @Test + void shouldFilterPopularByGenreAndYear() { + Film comedy2000 = newFilm("Комедия 2000"); + comedy2000.getGenres().add(genre(1)); + comedy2000 = filmStorage.add(comedy2000); + Film comedy2020 = newFilm("Комедия 2020"); + comedy2020.setReleaseDate(LocalDate.of(2020, 1, 1)); + comedy2020.getGenres().add(genre(1)); + comedy2020 = filmStorage.add(comedy2020); + User anna = createUser("anna"); + filmStorage.addLike(comedy2000.getId(), anna.getId()); + filmStorage.addLike(comedy2020.getId(), anna.getId()); + + List popular = filmStorage.findPopular(10, 1, 2000); + + assertEquals(List.of(comedy2000.getId()), popular.stream().map(Film::getId).toList()); + } + + @Test + void shouldReturnAllPopularWhenFiltersAreNull() { + Film film = createFilm("Один"); + User anna = createUser("anna"); + filmStorage.addLike(film.getId(), anna.getId()); + + assertEquals(1, filmStorage.findPopular(10, null, null).size()); + } } \ No newline at end of file From cc2ca10f7744f6c12ca7d6dab34138b1be66de9b Mon Sep 17 00:00:00 2001 From: Kinza Date: Tue, 6 Oct 2026 21:46:06 +0300 Subject: [PATCH 3/3] Trigger CI