diff --git a/README.md b/README.md index fe5e661..af6f295 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,8 @@ - Хранение данных в базе H2 (файловый режим — данные переживают перезапуск), доступ через `JdbcTemplate` и `RowMapper`. - Справочники жанров и рейтингов MPA в базе, эндпоинты `/genres` и `/mpa`. -- У фильма есть рейтинг и список жанров; жанры в ответе — без дублей, по порядку id. +- У фильма есть рейтинг и список жанров; жанры в ответе — без дублей, по порядку id. +- У фильмов можно указывать режиссёров и получать список фильмов выбранного режиссёра с сортировкой по году выпуска или количеству лайков. - Односторонняя дружба: добавление/удаление друзей, список друзей, общие друзья. - Лайки фильмам: добавление/удаление, топ-N фильмов по количеству лайков. - Отзывы к фильмам: положительные и негативные, с рейтингом полезности по лайкам и дизлайкам. @@ -71,6 +72,10 @@ - `DELETE /films/{id}` — удалить фильм; связанные с ним записи удаляются каскадно. - `PUT /films/{id}/like/{userId}` — поставить лайк фильму. - `DELETE /films/{id}/like/{userId}` — убрать лайк. +- `GET /films/popular?count={count}` — топ-N фильмов по лайкам (по умолчанию 10). +- `GET /films/director/{directorId}` — получить список фильмов режиссёра. +- `GET /films/director/{directorId}?sortBy=likes` — получить список фильмов режиссёра, отсортированный по лайкам. +- `GET /films/director/{directorId}?sortBy=year` — получить список фильмов режиссёра, отсортированный по году выпуска. - `GET /films/popular?count={count}&genreId={genreId}&year={year}` — топ-N фильмов по лайкам (по умолчанию 10); жанр и год — необязательные фильтры. - `GET /films/common?userId={userId}&friendId={friendId}` — общие фильмы двух пользователей, @@ -107,6 +112,13 @@ - `GET /mpa` — получить список всех рейтингов. - `GET /mpa/{id}` — получить рейтинг по ID. +## Режиссёры (`/directors`) +- `GET /directors` — получить список всех режиссёров. +- `GET /directors/{id}` — получить режиссёра по ID. +- `POST /directors` — создать нового режиссёра. +- `PUT /directors` — обновить данные режиссёра. +- `DELETE /directors/{id}` — удалить режиссёра. + ## Схема базы данных ```mermaid @@ -118,6 +130,8 @@ erDiagram users ||--o{ film_likes : "user_id" users ||--o{ friendship : "user_id" users ||--o{ friendship : "friend_id" + films ||--o{film_director : "film_id" + directors ||--o{film_director : "director_id" users ||--o{ reviews : "user_id" films ||--o{ reviews : "film_id" reviews ||--o{ review_likes : "review_id" @@ -158,6 +172,14 @@ erDiagram BIGINT user_id PK, FK BIGINT friend_id PK, FK } + directors { + BIGINT id PK + VARCHAR name + } + film_director { + BIGINT film_id PK, FK + BIGINT director_id PK, FK + } reviews { BIGINT id PK VARCHAR content @@ -181,6 +203,8 @@ erDiagram | `film_genres` | связь «фильм — жанр», многие ко многим | | `film_likes` | лайки: какой пользователь лайкнул какой фильм | | `friendship` | дружба: одна строка — `user_id` добавил в друзья `friend_id`; подтверждённая (взаимная) дружба — две встречные строки | +| `directors` | режиссёры | +| `film_director` | связь "фильм — режиссёр", многие ко многим | | `reviews` | отзывы пользователей к фильмам; рейтинг полезности не хранится, а считается по `review_likes` | | `review_likes` | оценки отзывов: одна строка — один пользователь, лайк или дизлайк | 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..d9baa58 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/DirectorController.java @@ -0,0 +1,53 @@ +package ru.yandex.practicum.filmorate.controller; + +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.model.Director; +import ru.yandex.practicum.filmorate.service.DirectorService; +import java.util.List; + +@Slf4j +@RestController +@RequestMapping("/directors") +public class DirectorController { + + private final DirectorService directorService; + + @Autowired + public DirectorController(DirectorService directorService) { + this.directorService = directorService; + } + + @GetMapping + public List getDirectors() { + log.info("GET /directors"); + return directorService.getAllDirectors(); + } + + @GetMapping("/{id}") + public Director getDirectorById(@PathVariable Long id) { + log.info("GET /directors/{}", id); + return directorService.getDirectorById(id); + } + + @PostMapping + public Director createDirector(@Valid @RequestBody Director director) { + log.info("POST /directors, body:{}", director); + return directorService.addDirector(director); + } + + @PutMapping + public Director updateDirector(@Valid @RequestBody Director director) { + log.info("PUT /directors, body:{}", director); + return directorService.updateDirector(director); + } + + @DeleteMapping("/{id}") + public void deleteDirector(@PathVariable Long id) { + log.info("DELETE /directors/{}", id); + directorService.deleteDirector(id); + } + +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java index f3473fa..68b2969 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -77,4 +77,11 @@ public Collection getCommon(@RequestParam long userId, @RequestParam long log.info("GET /films/common?userId={}&friendId={}", userId, friendId); return filmService.getCommon(userId, friendId); } + + @GetMapping("/director/{directorId}") + public Collection getFilmsByDirector(@PathVariable long directorId, + @RequestParam (defaultValue = "likes") String sortBy) { + log.info("GET /films/director/{}?sortBy={}", directorId, sortBy); + return filmService.findByDirectorSorted(directorId, sortBy); + } } \ No newline at end of file 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..5924c72 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Director.java @@ -0,0 +1,12 @@ +package ru.yandex.practicum.filmorate.model; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +public class Director { + private Long id; + + @NotBlank(message = "Имя режиссера не может быть пустым") + 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 08a9aca..5ba8b59 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java @@ -38,6 +38,8 @@ public class Film { @JsonIgnore private Set likes = new HashSet<>(); + private Set directors = new LinkedHashSet<>(); + public void addLike(Long userId) { likes.add(userId); } @@ -45,4 +47,5 @@ public void addLike(Long userId) { public boolean removeLike(Long userId) { return likes.remove(userId); } + } diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/DirectorService.java b/src/main/java/ru/yandex/practicum/filmorate/service/DirectorService.java new file mode 100644 index 0000000..df8c8a6 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/DirectorService.java @@ -0,0 +1,39 @@ +package ru.yandex.practicum.filmorate.service; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Director; +import ru.yandex.practicum.filmorate.storage.director.DirectorStorage; + +@Service +public class DirectorService { + + private final DirectorStorage directorStorage; + + public DirectorService(@Qualifier("directorDbStorage") DirectorStorage directorStorage) { + this.directorStorage = directorStorage; + } + + public Director addDirector(Director director) { + return directorStorage.addDirector(director); + } + + public Director updateDirector(Director director) { + return directorStorage.updateDirector(director); + } + + public void deleteDirector(long id) { + directorStorage.removeDirector(id); + } + + public Director getDirectorById(long id) { + return directorStorage.findDirectorById(id) + .orElseThrow(() -> new NotFoundException("Режиссер с id " + id + " не найден")); + } + + public java.util.List getAllDirectors() { + return directorStorage.findAllDirectors(); + } + +} 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 1a21f90..3628c9b 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java @@ -6,6 +6,7 @@ import org.springframework.stereotype.Service; import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.exception.ValidationException; +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.storage.film.FilmStorage; @@ -30,16 +31,19 @@ public class FilmService { // Нужны для проверки рейтинга и жанра из запроса private final MpaService mpaService; private final GenreService genreService; + private final DirectorService directorService; @Autowired public FilmService(@Qualifier("filmDbStorage") FilmStorage filmStorage, @Qualifier("userDbStorage") UserStorage userStorage, MpaService mpaService, - GenreService genreService) { + GenreService genreService, + DirectorService directorService) { this.filmStorage = filmStorage; this.userStorage = userStorage; this.mpaService = mpaService; this.genreService = genreService; + this.directorService = directorService; } // Получение фильма по id с проверкой существования @@ -134,9 +138,27 @@ private void validateMpaAndGenres(Film film) { } } + private void validateDirectors(Film film) { + + if (film.getDirectors() == null || film.getDirectors().isEmpty()) { + return; + } + + Set knownIds = new HashSet<>(); + for (Director director : directorService.getAllDirectors()) { + knownIds.add(director.getId()); + } + for (Director director : film.getDirectors()) { + if (!knownIds.contains(director.getId())) { + throw new NotFoundException("Режиссёр с id=" + director.getId() + " не найден"); + } + } + } + public Film add(Film film) { validateReleaseDate(film); validateMpaAndGenres(film); + validateDirectors(film); return filmStorage.add(film); } @@ -144,6 +166,7 @@ public Film update(Film film) { getFilmOrThrow(film.getId()); // проверка существования — иначе 404 validateReleaseDate(film); validateMpaAndGenres(film); + validateDirectors(film); return filmStorage.update(film); } @@ -160,4 +183,9 @@ public Collection findAll() { public Film findById(long id) { return getFilmOrThrow(id); } + + public Collection findByDirectorSorted(long directorId, String sortBy) { + directorService.getDirectorById(directorId); // проверка существования режиссёра — иначе 404 + return filmStorage.findFilmByDirector(directorId, sortBy); + } } \ 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..c4bb187 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/director/DirectorDbStorage.java @@ -0,0 +1,78 @@ +package ru.yandex.practicum.filmorate.storage.director; + +import java.util.Optional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Director; +import ru.yandex.practicum.filmorate.storage.mapper.DirectorRowMapper; +import java.util.List; + +@Repository +public class DirectorDbStorage implements DirectorStorage { + + private final JdbcTemplate jdbcTemplate; + private final DirectorRowMapper directorRowMapper; + + private static final String INSERT_QUERY = "INSERT INTO directors (name) VALUES (?)"; + private static final String UPDATE_QUERY = "UPDATE directors SET name = ? WHERE id = ?"; + private static final String DELETE_QUERY = "DELETE FROM directors WHERE id = ?"; + private static final String SELECT_BY_ID_QUERY = "SELECT * FROM directors WHERE id = ?"; + private static final String SELECT_ALL_QUERY = "SELECT * FROM directors ORDER BY id"; + + @Autowired + public DirectorDbStorage(JdbcTemplate jdbcTemplate, DirectorRowMapper directorRowMapper) { + this.jdbcTemplate = jdbcTemplate; + this.directorRowMapper = directorRowMapper; + } + + @Override + public Director addDirector(Director director) { + KeyHolder keyHolder = new GeneratedKeyHolder(); + jdbcTemplate.update(connection -> { + var ps = connection.prepareStatement(INSERT_QUERY, new String[]{"id"}); + ps.setString(1, director.getName()); + return ps; + }, keyHolder); + director.setId(keyHolder.getKey().longValue()); + return director; + } + + @Override + public Director updateDirector(Director director) { + int updatedRows = jdbcTemplate.update( + UPDATE_QUERY, + director.getName(), + director.getId() + ); + + if (updatedRows == 0) { + throw new NotFoundException( + "Режиссёр с id=" + director.getId() + " не найден" + ); + } + return director; + } + + @Override + public void removeDirector(long id) { + Director director = findDirectorById(id) + .orElseThrow(() -> new NotFoundException("Режиссёр с id " + id + " не найден")); + jdbcTemplate.update(DELETE_QUERY, id); + } + + @Override + public Optional findDirectorById(long id) { + Optional director = jdbcTemplate.query(SELECT_BY_ID_QUERY, directorRowMapper, id).stream().findFirst(); + return director; + } + + @Override + public List findAllDirectors() { + List directors = jdbcTemplate.query(SELECT_ALL_QUERY, directorRowMapper); + return directors; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/director/DirectorStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/director/DirectorStorage.java new file mode 100644 index 0000000..cdcdc40 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/director/DirectorStorage.java @@ -0,0 +1,18 @@ +package ru.yandex.practicum.filmorate.storage.director; + +import ru.yandex.practicum.filmorate.model.Director; +import java.util.List; +import java.util.Optional; + +public interface DirectorStorage { + + Director addDirector(Director director); + + Director updateDirector(Director director); + + void removeDirector(long id); + + Optional findDirectorById(long id); + + List findAllDirectors(); +} 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 5280bef..787bd86 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 @@ -6,8 +6,11 @@ import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Repository; import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.exception.ValidationException; +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.storage.mapper.DirectorRowMapper; import ru.yandex.practicum.filmorate.storage.mapper.FilmRowMapper; import ru.yandex.practicum.filmorate.storage.mapper.GenreRowMapper; @@ -76,16 +79,43 @@ public class FilmDbStorage implements FilmStorage { "MERGE INTO film_likes (film_id, user_id) KEY (film_id, user_id) VALUES (?, ?)"; private static final String REMOVE_LIKE_QUERY = "DELETE FROM film_likes WHERE film_id = ? AND user_id = ?"; + private static final String INSERT_FILM_DIRECTOR_QUERY = + "INSERT INTO film_director (film_id, director_id) VALUES (?, ?)"; + private static final String DELETE_DIRECTOR_FILM_QUERY = + "DELETE FROM film_director WHERE film_id = ?"; + private static final String FIND_DIRECTORS_BY_FILM_QUERY = + "SELECT d.id, d.name FROM film_director fd " + + "JOIN directors d ON fd.director_id = d.id " + + "WHERE fd.film_id = ? " + + "ORDER BY d.id"; + private static final String FIND_DIRECTORS_BY_FILMS_QUERY = + "SELECT fd.film_id, d.id, d.name " + + "FROM film_director fd " + + "JOIN directors d ON fd.director_id = d.id " + + "WHERE fd.film_id IN (%s) " + + "ORDER BY fd.film_id, d.id"; + private static final String FIND_DIRECTORS_BY_LIKES_AND_YEAR = + "SELECT f.id, f.name, f.description, f.release_date, f.duration, " + + "f.mpa_id, m.name AS mpa_name " + + "FROM films f " + + "JOIN film_director fd ON f.id = fd.film_id " + + "LEFT JOIN mpa m ON f.mpa_id = m.id " + + "LEFT JOIN film_likes fl ON f.id = fl.film_id " + + "WHERE fd.director_id = ? " + + "GROUP BY f.id, f.name, f.description, f.release_date, " + + "f.duration, f.mpa_id, m.name "; private final JdbcTemplate jdbc; private final FilmRowMapper mapper; private final GenreRowMapper genreMapper; + private final DirectorRowMapper directorMapper; @Autowired - public FilmDbStorage(JdbcTemplate jdbc, FilmRowMapper mapper, GenreRowMapper genreMapper) { + public FilmDbStorage(JdbcTemplate jdbc, FilmRowMapper mapper, GenreRowMapper genreMapper, DirectorRowMapper directorMapper) { this.jdbc = jdbc; this.mapper = mapper; this.genreMapper = genreMapper; + this.directorMapper = directorMapper; } @Override @@ -103,6 +133,7 @@ public Film add(Film film) { Long id = keyHolder.getKeyAs(Long.class); saveGenres(id, film.getGenres()); + saveDirector(id, film.getDirectors()); // Перечитываем из базы: так в ответе будут названия рейтинга и жанров, а не только их id return findById(id).orElseThrow(); } @@ -112,6 +143,7 @@ public Film update(Film film) { jdbc.update(UPDATE_QUERY, film.getName(), film.getDescription(), Date.valueOf(film.getReleaseDate()), film.getDuration(), getMpaId(film), film.getId()); saveGenres(film.getId(), film.getGenres()); + saveDirector(film.getId(), film.getDirectors()); return findById(film.getId()).orElseThrow(); } @@ -127,6 +159,7 @@ public Film delete(long id) { public Collection findAll() { List films = jdbc.query(FIND_ALL_QUERY, mapper); loadGenresForFilms(films); + loadDirectors(films); return films; } @@ -134,6 +167,7 @@ public Collection findAll() { public Optional findById(long id) { Optional film = jdbc.query(FIND_BY_ID_QUERY, mapper, id).stream().findFirst(); film.ifPresent(this::loadGenres); + film.ifPresent(this::loadDirectors); return film; } @@ -156,6 +190,7 @@ public List findPopular(int count, Integer genreId, Integer year) { String sql = buildPopularQuery(conditions) + LIMIT_CLAUSE; List films = jdbc.query(sql, mapper, params.toArray()); loadGenresForFilms(films); + loadDirectors(films); return films; } @@ -165,6 +200,7 @@ public List findCommon(long userId, long friendId) { String sql = buildPopularQuery(List.of(LIKED_BY_USER_FILTER, LIKED_BY_USER_FILTER)); List films = jdbc.query(sql, mapper, userId, friendId); loadGenresForFilms(films); + loadDirectors(films); return films; } @@ -179,7 +215,30 @@ public boolean removeLike(long filmId, long userId) { return jdbc.update(REMOVE_LIKE_QUERY, filmId, userId) > 0; } - // Собирает запрос: чтение + WHERE по условиям + сортировка по числу лайков, без LIMIT + @Override + public List findFilmByDirector(long directorId, String sortBy) { + String orderBy; + + if ("year".equals(sortBy)) { + orderBy = "ORDER BY f.release_date ASC, f.id"; + } else if ("likes".equals(sortBy)) { + orderBy = "ORDER BY COUNT(fl.user_id) DESC, f.id"; + } else { + throw new ValidationException( + "Неизвестный тип сортировки: " + sortBy + ); + } + + List films = jdbc.query(FIND_DIRECTORS_BY_LIKES_AND_YEAR + orderBy, mapper, directorId + ); + + loadDirectors(films); + loadGenresForFilms(films); + + return films; + } + + //Собирает запрос: чтение + WHERE по условиям + сортировка по числу лайков, без LIMIT private String buildPopularQuery(List conditions) { StringBuilder sql = new StringBuilder(FIND_POPULAR_BASE_QUERY); if (!conditions.isEmpty()) { @@ -226,4 +285,47 @@ private void loadGenresForFilms(List films) { film.getGenres().add(genreMapper.mapRow(rs, rs.getRow())); }, filmsById.keySet().toArray()); } + + // Директора одного фильма + private void loadDirectors(Film film) { + film.getDirectors().addAll(jdbc.query(FIND_DIRECTORS_BY_FILM_QUERY, directorMapper, film.getId())); + } + + private void loadDirectors(List films) { + if (films.isEmpty()) { + return; // фильмов нет — запрос не нужен + } + Map filmsById = new HashMap<>(); + for (Film film : films) { + filmsById.put(film.getId(), film); + } + String placeholders = String.join( + ", ", + Collections.nCopies(filmsById.size(), "?") + ); + + String query = String.format( + FIND_DIRECTORS_BY_FILMS_QUERY, + placeholders + ); + + jdbc.query(query, rs -> { + Film film = filmsById.get(rs.getLong("film_id")); + film.getDirectors().add(directorMapper.mapRow(rs, rs.getRow()) + ); + }, filmsById.keySet().toArray()); + } + + // Заменяет директоров фильма: старые связи удаляются, новые вставляются одним пакетом + private void saveDirector(long filmId, Set directors) { + jdbc.update(DELETE_DIRECTOR_FILM_QUERY, filmId); + if (directors == null || directors.isEmpty()) { + return; + } + List rows = new ArrayList<>(); + for (Director director : directors) { + rows.add(new Object[]{filmId, director.getId()}); + } + jdbc.batchUpdate(INSERT_FILM_DIRECTOR_QUERY, rows); + } } \ No newline at end of file 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 3e5ecd3..62f4a02 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 @@ -25,4 +25,6 @@ public interface FilmStorage { void addLike(long filmId, long userId); boolean removeLike(long filmId, long userId); + + List findFilmByDirector(long directorId, String sortBy); } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/mapper/DirectorRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/storage/mapper/DirectorRowMapper.java new file mode 100644 index 0000000..c8e662f --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/mapper/DirectorRowMapper.java @@ -0,0 +1,19 @@ +package ru.yandex.practicum.filmorate.storage.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("id")); + director.setName(rs.getString("name")); + return director; + } +} diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index b2dcd2d..554a36c 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -69,3 +69,16 @@ CREATE TABLE IF NOT EXISTS review_likes ( is_useful BOOLEAN NOT NULL, PRIMARY KEY (review_id, user_id) ); + + +CREATE TABLE IF NOT EXISTS directors ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name VARCHAR(255) NOT NULL +); + +--Связь «фильм — режиссёр», многие ко многим. +CREATE TABLE IF NOT EXISTS film_director ( + film_id BIGINT NOT NULL REFERENCES films(id) ON DELETE CASCADE, + director_id BIGINT NOT NULL REFERENCES directors(id) ON DELETE CASCADE, + PRIMARY KEY (film_id, director_id) +); \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/director/DirectorDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/director/DirectorDbStorageTest.java new file mode 100644 index 0000000..d00716c --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/director/DirectorDbStorageTest.java @@ -0,0 +1,75 @@ +package ru.yandex.practicum.filmorate.storage.director; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.context.annotation.Import; +import ru.yandex.practicum.filmorate.model.Director; +import ru.yandex.practicum.filmorate.storage.mapper.DirectorRowMapper; +import java.util.List; +import static org.junit.jupiter.api.Assertions.*; + +@JdbcTest +@AutoConfigureTestDatabase +@Import({DirectorDbStorage.class, DirectorRowMapper.class}) +public class DirectorDbStorageTest { + + private final DirectorDbStorage directorDbStorage; + + @Autowired + public DirectorDbStorageTest(DirectorDbStorage directorDbStorage) { + this.directorDbStorage = directorDbStorage; + } + + private Director createDirector(String directorName) { + Director director = new Director(); + director.setName(directorName); + return director; + } + + @Test + public void addDirector() { + Director director = createDirector("name"); + directorDbStorage.addDirector(director); + assertEquals("name", director.getName()); + assertNotNull(director.getId()); + } + + @Test + public void getDirectorById() { + Director director = createDirector("director"); + Director createdDirector = directorDbStorage.addDirector(director); + Director retrievedDirector = directorDbStorage.findDirectorById(createdDirector.getId()).orElse(null); + assertEquals(createdDirector.getId(), retrievedDirector.getId()); + assertEquals(createdDirector.getName(), retrievedDirector.getName()); + } + + @Test + public void deleteDirectorById() { + Director director = createDirector("director"); + directorDbStorage.addDirector(director); + directorDbStorage.removeDirector(director.getId()); + assertTrue(directorDbStorage.findDirectorById(director.getId()).isEmpty()); + } + + @Test + public void updateDirectorById() { + Director director = createDirector("director"); + directorDbStorage.addDirector(director); + director.setName("updated name"); + directorDbStorage.updateDirector(director); + Director updatedDirector = directorDbStorage.findDirectorById(director.getId()).orElse(null); + assertEquals("updated name", updatedDirector.getName()); + } + + @Test + public void getAllDirectors() { + Director director = createDirector("director"); + Director director2 = createDirector("director2"); + directorDbStorage.addDirector(director); + directorDbStorage.addDirector(director2); + List allDirectors = directorDbStorage.findAllDirectors(); + assertEquals(2, allDirectors.size()); + } +} 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 bffa5f2..8a786fd 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 @@ -7,9 +7,12 @@ import org.springframework.context.annotation.Import; import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.model.Director; import ru.yandex.practicum.filmorate.model.Genre; import ru.yandex.practicum.filmorate.model.Mpa; import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.storage.director.DirectorDbStorage; +import ru.yandex.practicum.filmorate.storage.mapper.DirectorRowMapper; import ru.yandex.practicum.filmorate.storage.mapper.FilmRowMapper; import ru.yandex.practicum.filmorate.storage.mapper.GenreRowMapper; import ru.yandex.practicum.filmorate.storage.mapper.UserRowMapper; @@ -24,16 +27,19 @@ @JdbcTest @AutoConfigureTestDatabase @Import({FilmDbStorage.class, FilmRowMapper.class, GenreRowMapper.class, - UserDbStorage.class, UserRowMapper.class}) + UserDbStorage.class, UserRowMapper.class, + DirectorDbStorage.class, DirectorRowMapper.class}) class FilmDbStorageTest { private final FilmDbStorage filmStorage; private final UserDbStorage userStorage; + private final DirectorDbStorage directorStorage; @Autowired - FilmDbStorageTest(FilmDbStorage filmStorage, UserDbStorage userStorage) { + FilmDbStorageTest(FilmDbStorage filmStorage, UserDbStorage userStorage, DirectorDbStorage directorStorage) { this.filmStorage = filmStorage; this.userStorage = userStorage; + this.directorStorage = directorStorage; } // Фильм без рейтинга и жанров: тесты добавляют их сами, где нужно @@ -75,6 +81,12 @@ private static List genreNames(Film film) { return film.getGenres().stream().map(Genre::getName).toList(); } + private Director createDirector(String directorName) { + Director director = new Director(); + director.setName(directorName); + return director; + } + @Test void shouldAddFilmWithMpaAndGenres() { Film film = newFilm("С жанрами"); @@ -292,4 +304,48 @@ void shouldReturnEmptyCommonFilmsWhenNoSharedLikes() { assertEquals(0, filmStorage.findCommon(anna.getId(), boris.getId()).size()); } + + @Test + void shouldReturnAllFilmsByDirectorSortedByLikes() { + Director director = createDirector("Christopher Nolan"); + directorStorage.addDirector(director); + Film film1 = newFilm("Фильм 1"); + film1.getDirectors().add(director); + film1 = filmStorage.add(film1); + Film film2 = newFilm("Фильм 2"); + film2.getDirectors().add(director); + film2 = filmStorage.add(film2); + User anna = createUser("anna"); + User boris = createUser("boris"); + filmStorage.addLike(film1.getId(), anna.getId()); + filmStorage.addLike(film2.getId(), anna.getId()); + filmStorage.addLike(film2.getId(), boris.getId()); + + List allFilms = filmStorage.findFilmByDirector(director.getId(), "likes"); + assertEquals(2, allFilms.size()); + assertEquals(film2.getId(), allFilms.get(0).getId()); + assertEquals(film1.getId(), allFilms.get(1).getId()); + } + + @Test + void shouldReturnAllFilmsByDirectorSortedByYear() { + Director director = createDirector("Steven Spielberg"); + directorStorage.addDirector(director); + Film film1 = newFilm("Фильм 1"); + film1.setReleaseDate(LocalDate.of(2000, 1, 1)); + film1.getDirectors().add(director); + film1 = filmStorage.add(film1); + Film film2 = newFilm("Фильм 2"); + film2.setReleaseDate(LocalDate.of(2010, 1, 1)); + film2.getDirectors().add(director); + film2 = filmStorage.add(film2); + Film film3 = newFilm("Фильм 3"); + film3.setReleaseDate(LocalDate.of(2005, 1, 1)); + film3.getDirectors().add(director); + film3 = filmStorage.add(film3); + List allFilms = filmStorage.findFilmByDirector(director.getId(), "year"); + assertEquals(film1.getId(), allFilms.get(0).getId()); + assertEquals(film3.getId(), allFilms.get(1).getId()); + assertEquals(film2.getId(), allFilms.get(2).getId()); + } } \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/review/ReviewDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/review/ReviewDbStorageTest.java index 95d380c..28bace8 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/storage/review/ReviewDbStorageTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/review/ReviewDbStorageTest.java @@ -10,10 +10,7 @@ import ru.yandex.practicum.filmorate.model.Review; import ru.yandex.practicum.filmorate.model.User; import ru.yandex.practicum.filmorate.storage.film.FilmDbStorage; -import ru.yandex.practicum.filmorate.storage.mapper.FilmRowMapper; -import ru.yandex.practicum.filmorate.storage.mapper.GenreRowMapper; -import ru.yandex.practicum.filmorate.storage.mapper.ReviewRowMapper; -import ru.yandex.practicum.filmorate.storage.mapper.UserRowMapper; +import ru.yandex.practicum.filmorate.storage.mapper.*; import ru.yandex.practicum.filmorate.storage.user.UserDbStorage; import java.time.LocalDate; @@ -24,7 +21,7 @@ @JdbcTest @AutoConfigureTestDatabase @Import({ReviewDbStorage.class, ReviewRowMapper.class, FilmDbStorage.class, FilmRowMapper.class, - GenreRowMapper.class, UserDbStorage.class, UserRowMapper.class}) + GenreRowMapper.class, UserDbStorage.class, UserRowMapper.class, DirectorRowMapper.class}) class ReviewDbStorageTest { private final ReviewDbStorage reviewStorage;