diff --git a/src/main/java/ru/yandex/practicum/filmorate/Enum/EventType.java b/src/main/java/ru/yandex/practicum/filmorate/Enum/EventType.java new file mode 100644 index 0000000..c31602b --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/Enum/EventType.java @@ -0,0 +1,7 @@ +package ru.yandex.practicum.filmorate.Enum; + +public enum EventType { + LIKE, + FRIEND, + REVIEW +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/Enum/OperationType.java b/src/main/java/ru/yandex/practicum/filmorate/Enum/OperationType.java new file mode 100644 index 0000000..3163dbc --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/Enum/OperationType.java @@ -0,0 +1,7 @@ +package ru.yandex.practicum.filmorate.Enum; + +public enum OperationType { + ADD, + UPDATE, + REMOVE +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/StartDate.java b/src/main/java/ru/yandex/practicum/filmorate/annotation/StartDate.java similarity index 92% rename from src/main/java/ru/yandex/practicum/filmorate/model/StartDate.java rename to src/main/java/ru/yandex/practicum/filmorate/annotation/StartDate.java index 28b94d7..c680e6e 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/StartDate.java +++ b/src/main/java/ru/yandex/practicum/filmorate/annotation/StartDate.java @@ -1,4 +1,4 @@ -package ru.yandex.practicum.filmorate.model; +package ru.yandex.practicum.filmorate.annotation; import jakarta.validation.Constraint; import jakarta.validation.Payload; diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/StartDateValidator.java b/src/main/java/ru/yandex/practicum/filmorate/annotation/StartDateValidator.java similarity index 90% rename from src/main/java/ru/yandex/practicum/filmorate/model/StartDateValidator.java rename to src/main/java/ru/yandex/practicum/filmorate/annotation/StartDateValidator.java index 2ed4a1c..61193d6 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/StartDateValidator.java +++ b/src/main/java/ru/yandex/practicum/filmorate/annotation/StartDateValidator.java @@ -1,4 +1,4 @@ -package ru.yandex.practicum.filmorate.model; +package ru.yandex.practicum.filmorate.annotation; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; 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..f458cfa --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/DirectorController.java @@ -0,0 +1,50 @@ +package ru.yandex.practicum.filmorate.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.model.Director; +import ru.yandex.practicum.filmorate.service.DirectorServiceImpl; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/directors") +@Validated +public class DirectorController { + + private final DirectorServiceImpl directorService; + + @PostMapping + @ResponseStatus(HttpStatus.OK) + public Director create(@Valid @RequestBody Director director) { + return directorService.create(director); + } + + @GetMapping + @ResponseStatus(HttpStatus.OK) + public List getAll() { + return directorService.getAll(); + } + + @GetMapping("/{id}") + @ResponseStatus(HttpStatus.OK) + public Director getById(@PathVariable("id") long id) { + return directorService.getById(id); + } + + @PutMapping + @ResponseStatus(HttpStatus.OK) + public Director updateDirector(@Valid @RequestBody Director director) { + return directorService.update(director); + } + + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.OK) + public void deleteDirector(@PathVariable("id") long id) { + directorService.delete(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 ddbda8f..471a419 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -25,7 +25,7 @@ public Collection findAll() { @GetMapping("/{filmId}") public Film get(@PathVariable long filmId) { - return filmService.getById(filmId); + return filmService.getFilmById(filmId); } @PostMapping @@ -40,6 +40,12 @@ public Film update(@Valid @RequestBody Film newFilm) { return filmService.update(newFilm); } + @DeleteMapping("/{filmId}") + @ResponseStatus(HttpStatus.OK) + public void deleteFilm(@PathVariable("filmId") long filmId) { + filmService.deleteFilm(filmId); + } + @PutMapping("/{filmId}/like/{userId}") @ResponseStatus(HttpStatus.OK) public void addLike(@PathVariable("filmId") long filmId, @PathVariable("userId") long userId) { @@ -54,7 +60,28 @@ public void deleteLike(@PathVariable("id") long id, @PathVariable("userId") long @GetMapping("/popular") @ResponseStatus(HttpStatus.OK) - public Collection getPopularFilms(@RequestParam(value = "count", defaultValue = "10") final Integer count) { - return filmService.getPopularFilms(count); + public Collection getPopularFilms(@RequestParam(value = "count", defaultValue = "10") Integer count, + @RequestParam(value = "genreId", required = false) Long genreId, + @RequestParam(value = "year", required = false) Integer year) { + return filmService.getPopularFilms(count, genreId, year); + } + + @GetMapping("/director/{directorId}") + @ResponseStatus(HttpStatus.OK) + public Collection getDirectorFilms(@RequestParam(value = "sortBy") final String sort, @PathVariable("directorId") long directorId) { + return filmService.getDirectorFilms(directorId, sort); + } + + @GetMapping("/common") + @ResponseStatus(HttpStatus.OK) + public Collection getCommonFilms(@RequestParam(value = "userId") Long userId, + @RequestParam(value = "friendId") Long friendId) { + return filmService.getCommonFilms(userId, friendId); + } + + @GetMapping("/search") + @ResponseStatus(HttpStatus.OK) + public Collection getSearch(@RequestParam(value = "query", defaultValue = "defaultSearch") final String query, @RequestParam(value = "by", defaultValue = "defaultSearch") final String by) { + return filmService.getSearch(query, by); } } 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..af952ac --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/ReviewController.java @@ -0,0 +1,76 @@ +package ru.yandex.practicum.filmorate.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.model.Review; +import ru.yandex.practicum.filmorate.service.ReviewServiceImpl; + +import java.util.List; +import java.util.Optional; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/reviews") +@Validated +public class ReviewController { + private final ReviewServiceImpl reviewService; + + @GetMapping("/{id}") + @ResponseStatus(HttpStatus.OK) + public Optional getReviewById(@PathVariable long id) { + return reviewService.getReviewById(id); + } + + @GetMapping + @ResponseStatus(HttpStatus.OK) + public List getAllReviewsByFilmId(@RequestParam(value = "filmId", defaultValue = "-1") long filmId, + @RequestParam(value = "count", defaultValue = "10") long count) { + return reviewService.getAllReviewsByFilmId(filmId, count); + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public Review create(@Valid @RequestBody Review review) { + return reviewService.create(review); + } + + @PutMapping + @ResponseStatus(HttpStatus.OK) + public Review update(@Valid @RequestBody Review review) { + return reviewService.update(review); + } + + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.OK) + public void deleteReview(@PathVariable long id) { + reviewService.deleteReview(id); + } + + @PutMapping("/{reviewId}/like/{userId}") + @ResponseStatus(HttpStatus.OK) + public void addLike(@PathVariable long reviewId, @PathVariable long userId) { + reviewService.addLike(reviewId, userId); + } + + @PutMapping("/{reviewId}/dislike/{userId}") + @ResponseStatus(HttpStatus.OK) + public void addDislike(@PathVariable long reviewId, @PathVariable long userId) { + reviewService.addDislike(reviewId, userId); + } + + @DeleteMapping("/{reviewId}/like/{userId}") + @ResponseStatus(HttpStatus.OK) + public void deleteLike(@PathVariable long reviewId, @PathVariable long userId) { + reviewService.deleteReaction(reviewId, userId); + } + + @DeleteMapping("/{reviewId}/dislike/{userId}") + @ResponseStatus(HttpStatus.OK) + public void deleteDislike(@PathVariable long reviewId, @PathVariable long userId) { + reviewService.deleteReaction(reviewId, userId); + } + +} 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 7ef3e32..53ccbcd 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java @@ -6,10 +6,14 @@ import org.springframework.http.HttpStatus; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.model.Activity; +import ru.yandex.practicum.filmorate.model.Film; import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.service.RecommendationService; import ru.yandex.practicum.filmorate.service.UserServiceImpl; import java.util.Collection; +import java.util.List; @RestController @RequiredArgsConstructor @@ -18,6 +22,13 @@ @Validated public class UserController { private final UserServiceImpl userService; + private final RecommendationService recommendationService; + + @GetMapping("/{userId}/feed") + @ResponseStatus(HttpStatus.OK) + public List getActivityByUserId(@PathVariable long userId) { + return userService.getActivityById(userId); + } @GetMapping @ResponseStatus(HttpStatus.OK) @@ -43,6 +54,12 @@ public User update(@Valid @RequestBody User newUser) { return userService.update(newUser); } + @DeleteMapping("/{userId}") + @ResponseStatus(HttpStatus.OK) + public void deleteUser(@PathVariable("userId") long userId) { + userService.deleteUser(userId); + } + @PutMapping("/{userId}/friends/{friendId}") @ResponseStatus(HttpStatus.OK) public void addFriend(@PathVariable("userId") long userId, @PathVariable("friendId") long friendId) { @@ -66,4 +83,10 @@ public Collection getAllFriends(@PathVariable("userId") long userId) { public Collection getCommonFriends(@PathVariable("userId") long userId, @PathVariable("otherId") long otherId) { return userService.getCommonFriends(userId, otherId); } + + @GetMapping("/{userId}/recommendations") + @ResponseStatus(HttpStatus.OK) + public List getUserRecommendations(@PathVariable("userId") long userId) { + return recommendationService.getRecommendations(userId); + } } \ No newline at end of file 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..a680eb9 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/DirectorRepository.java @@ -0,0 +1,18 @@ +package ru.yandex.practicum.filmorate.dal; + +import ru.yandex.practicum.filmorate.model.Director; + +import java.util.List; +import java.util.Optional; + +public interface DirectorRepository { + Director create(Director director); + + List getAll(); + + Optional getById(Long directorId); + + Director update(Director director); + + void delete(Long 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 6ba8f3d..7aa5995 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/FilmRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/FilmRepository.java @@ -1,8 +1,10 @@ package ru.yandex.practicum.filmorate.dal; +import ru.yandex.practicum.filmorate.model.Director; import ru.yandex.practicum.filmorate.model.Film; import ru.yandex.practicum.filmorate.model.Genre; +import java.util.Collection; import java.util.LinkedHashSet; import java.util.List; import java.util.Optional; @@ -16,13 +18,31 @@ public interface FilmRepository { Film update(Film film); + void deleteFilm(long filmId); + + LinkedHashSet getFilmDirectors(Film film); + + void connectGenres(Collection films); + + void connectDirectors(Collection films); + void addLike(long filmId, long userId); void deleteLike(long filmId, long userId); void setFilmGenres(Film film); + void setFilmDirectors(Film film); + LinkedHashSet getFilmGenres(Film film); - List getPopularFilms(int count); + List getPopularFilms(int count, Long genreId, Integer year); + + List getDirectorFilmsByYear(long id); + + List getDirectorFilmsByLikes(long id); + + List getSearch(String query, String searchBy); + + List getCommonFilms(long userId, long friendId); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/JdbcDirectorRepository.java b/src/main/java/ru/yandex/practicum/filmorate/dal/JdbcDirectorRepository.java new file mode 100644 index 0000000..7ca9f54 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/JdbcDirectorRepository.java @@ -0,0 +1,69 @@ +package ru.yandex.practicum.filmorate.dal; + +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.dal.mappers.DirectorRowMapper; +import ru.yandex.practicum.filmorate.model.Director; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +@Repository +@RequiredArgsConstructor +public class JdbcDirectorRepository implements DirectorRepository { + private final NamedParameterJdbcOperations jdbc; + private final DirectorRowMapper mapper; + + private static final String CREATE_DIRECTOR_QUERY = "INSERT INTO directors (director_name) VALUES(:director_name)"; + private static final String GET_ALL_DIRECTORS_QUERY = "SELECT * FROM directors ORDER BY director_id"; + private static final String GET_BY_ID_QUERY = "SELECT * FROM directors WHERE director_id = :director_id"; + private static final String UPDATE_DIRECTOR_QUERY = "UPDATE directors SET director_name=:director_name WHERE director_id=:director_id"; + private static final String DELETE_DIRECTOR_QUERY = "DELETE FROM directors WHERE director_id = :director_id"; + + @Override + public Director create(Director director) { + GeneratedKeyHolder keyHolder = new GeneratedKeyHolder(); + MapSqlParameterSource params = new MapSqlParameterSource(); + + params.addValue("director_name", director.getName()); + jdbc.update(CREATE_DIRECTOR_QUERY, params, keyHolder); + director.setId(keyHolder.getKeyAs(Long.class)); + return director; + } + + @Override + public List getAll() { + return jdbc.query(GET_ALL_DIRECTORS_QUERY, mapper); + } + + @Override + public Optional getById(Long directorId) { + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("director_id", directorId); + try (Stream stream = jdbc.queryForStream(GET_BY_ID_QUERY, params, mapper)) { + return stream.findAny(); + } + + } + + @Override + public Director update(Director director) { + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("director_name", director.getName()); + params.addValue("director_id", director.getId()); + jdbc.update(UPDATE_DIRECTOR_QUERY, params); + return director; + } + + @Override + public void delete(Long id) { + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("director_id", id); + jdbc.update(DELETE_DIRECTOR_QUERY, params); + + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/JdbcFilmRepository.java b/src/main/java/ru/yandex/practicum/filmorate/dal/JdbcFilmRepository.java index baa5a73..9dbfff9 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/JdbcFilmRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/JdbcFilmRepository.java @@ -1,30 +1,44 @@ package ru.yandex.practicum.filmorate.dal; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.jdbc.support.GeneratedKeyHolder; import org.springframework.jdbc.support.rowset.SqlRowSet; import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.Enum.EventType; +import ru.yandex.practicum.filmorate.Enum.OperationType; +import ru.yandex.practicum.filmorate.dal.mappers.DirectorRowMapper; import ru.yandex.practicum.filmorate.dal.mappers.FilmRowMapper; import ru.yandex.practicum.filmorate.dal.mappers.GenreRowMapper; +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 java.time.Instant; import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; +@Slf4j @Repository @RequiredArgsConstructor public class JdbcFilmRepository implements FilmRepository { private final NamedParameterJdbcOperations jdbc; private final FilmRowMapper mapper; private final GenreRowMapper genreRowMapper; + private final DirectorRowMapper directorRowMapper; private static final String CREATE_FILM_QUERY = "INSERT INTO films (name, description, release_date, duration, mpa_id) VALUES(:name,:description,:release_date,:duration,:mpa_id)"; + private static final String SET_DIRECTORS_QUERY = "INSERT INTO FILM_DIRECTORS (FILM_ID, DIRECTOR_ID) VALUES(:film_id, :director_id)"; + private static final String GET_DIRECTORS_QUERY = "SELECT d.DIRECTOR_ID, d.DIRECTOR_NAME FROM FILM_DIRECTORS fd JOIN DIRECTORS d ON fd.DIRECTOR_ID = d.DIRECTOR_ID WHERE fd.film_id = :film_id"; private static final String CLEAN_GENRES_QUERY = "DELETE FROM film_genres WHERE film_id=:film_id"; + private static final String CLEAN_DIRECTORS_QUERY = "DELETE FROM film_directors WHERE film_id=:film_id"; private static final String UPDATE_FILM_QUERY = "UPDATE films SET name=:name, description=:description, release_date=:release_date, duration=:duration, mpa_id=:mpa_id WHERE film_id=:film_id"; + private static final String DELETE_FILM_QUERY = "DELETE FROM films WHERE film_id=:film_id"; private static final String GET_BY_ID_QUERY = """ SELECT f.*, r.mpa_name, COUNT(l.*) AS likes_count FROM films f JOIN mpa r ON f.mpa_id = r.mpa_id @@ -43,15 +57,6 @@ public class JdbcFilmRepository implements FilmRepository { private static final String GET_GENRE_QUERY = "SELECT g.genre_id, g.genre_name FROM film_genres fg JOIN genres g ON fg.genre_id = g.genre_id WHERE fg.film_id = :film_id"; private static final String ADD_LIKE_QUERY = "INSERT INTO likes (user_id, film_id) VALUES(:user_id, :film_id)"; private static final String DELETE_LIKE_QUERY = "DELETE FROM likes WHERE film_id=:film_id AND user_id=:user_id;"; - private static final String GET_POPULAR_FILMS_QUERY = """ - SELECT f.*, r.mpa_name, COUNT(l.user_id) AS likes_count - FROM films f - JOIN likes l ON l.film_id = f.film_id - JOIN mpa r ON r.mpa_id = f.mpa_id - GROUP BY f.film_id - ORDER BY COUNT(l.user_id) DESC - LIMIT :limit - """; private static final String SELECT_GENRES_BY_FILM_IDS_QUERY = """ SELECT * @@ -59,6 +64,67 @@ ORDER BY COUNT(l.user_id) DESC LEFT JOIN genres ON fg.genre_id = genres.genre_id WHERE fg.film_id IN (:film_ids) """; + private static final String SELECT_DIRECTORS_BY_FILM_IDS_QUERY = """ + SELECT * + FROM FILM_DIRECTORS fd + LEFT JOIN DIRECTORS d ON fd.DIRECTOR_ID = d.DIRECTOR_ID + WHERE fd.film_id IN (:film_ids) + """; + private static final String GET_DIRECTOR_FILMS_BY_YEAR = """ + SELECT f.*,r.MPA_NAME,COUNT(l.user_id) AS likes_count + FROM FILMS f + JOIN FILM_DIRECTORS fd ON fd.FILM_ID = f.FILM_ID + JOIN mpa r ON f.mpa_id = r.mpa_id + LEFT JOIN likes l ON l.film_id = f.film_id + WHERE fd.DIRECTOR_ID = :director_id + GROUP BY f.FILM_ID + ORDER BY f.RELEASE_DATE + """; + private static final String GET_DIRECTOR_FILMS_BY_LIKES = """ + SELECT f.*,r.MPA_NAME,COUNT(l.user_id) AS likes_count + FROM FILMS f + JOIN FILM_DIRECTORS fd ON fd.FILM_ID = f.FILM_ID + JOIN mpa r ON f.mpa_id = r.mpa_id + LEFT JOIN likes l ON l.film_id = f.film_id + WHERE fd.DIRECTOR_ID = :director_id + GROUP BY f.FILM_ID + ORDER BY likes_count DESC + """; + + private static final String GET_COMMON_FILMS_QUERY = """ + SELECT f.*, r.mpa_name, COUNT(l1.user_id) AS likes_count + FROM films f + JOIN mpa r ON r.mpa_id = f.mpa_id + JOIN likes l1 ON l1.film_id = f.film_id AND l1.user_id = :userId + JOIN likes l2 ON l2.film_id = f.film_id AND l2.user_id = :friendId + GROUP BY f.film_id, r.mpa_name + ORDER BY f.name; + """; + private static final String GET_SEARCH = """ + SELECT f.*, r.mpa_name, COUNT(l.user_id) AS likes_count + FROM films f + LEFT JOIN likes l ON l.film_id = f.film_id + JOIN mpa r ON r.mpa_id = f.mpa_id + LEFT JOIN FILM_DIRECTORS fd ON fd.FILM_ID = f.FILM_ID + LEFT JOIN DIRECTORS d ON d.DIRECTOR_ID = fd.DIRECTOR_ID + WHERE LOWER(f.NAME) LIKE LOWER(:film_name) + OR LOWER(d.DIRECTOR_NAME) LIKE LOWER(:director_name) + GROUP BY f.film_id, r.mpa_name + ORDER BY likes_count DESC + """; + + private static final String ACTIVITY_GENERAL = + "INSERT INTO activity (userId, entityId, eventType, operation, timestamp) VALUES(:userId, :entityId, '"; + + private static final String ACTIVITY_FILM_LIKE = ACTIVITY_GENERAL + + EventType.LIKE + "','" + OperationType.ADD + "', " + instantOfMilliSecond() + ")"; + + private static final String ACTIVITY_FILM_LIKE_DELETE = ACTIVITY_GENERAL + + EventType.LIKE + "','" + OperationType.REMOVE + "'," + instantOfMilliSecond() + ")"; + + private static long instantOfMilliSecond() { + return Instant.now().toEpochMilli(); + } @Override public Film create(Film film) { @@ -77,6 +143,9 @@ public Film create(Film film) { if (film.getGenres() != null) { setFilmGenres(film); } + if (film.getDirectors() != null) { + setFilmDirectors(film); + } return film; } @@ -92,16 +161,26 @@ public Film update(Film film) { params.addValue("film_id", film.getId()); jdbc.update(CLEAN_GENRES_QUERY, params, keyHolder); + jdbc.update(CLEAN_DIRECTORS_QUERY, params, keyHolder); if (film.getGenres() != null) { setFilmGenres(film); } - film.setGenres(new LinkedHashSet<>()); + if (film.getDirectors() != null) { + setFilmDirectors(film); + } jdbc.update(UPDATE_FILM_QUERY, params, keyHolder); return film; } + @Override + public void deleteFilm(long filmId) { + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("film_id", filmId); + jdbc.update(DELETE_FILM_QUERY, params); + } + @Override public Optional getFilmById(long id) { MapSqlParameterSource params = new MapSqlParameterSource(); @@ -109,6 +188,7 @@ public Optional getFilmById(long id) { try (Stream stream = jdbc.queryForStream(GET_BY_ID_QUERY, params, mapper)) { Optional optionalFilm = stream.findAny(); optionalFilm.ifPresent(film -> film.setGenres(getFilmGenres(film))); + optionalFilm.ifPresent(film -> film.setDirectors(getFilmDirectors(film))); return optionalFilm; } } @@ -116,7 +196,7 @@ public Optional getFilmById(long id) { @Override public List getAllFilms() { List films = jdbc.query(GET_FILMS_QUERY, mapper); - + connectDirectors(films); connectGenres(films); return films; } @@ -136,6 +216,21 @@ public void setFilmGenres(Film film) { jdbc.batchUpdate(SET_GENRE_QUERY, paramsList, keyHolder); } + @Override + public void setFilmDirectors(Film film) { + GeneratedKeyHolder keyHolder = new GeneratedKeyHolder(); + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("film_id", film.getId()); + jdbc.update(CLEAN_DIRECTORS_QUERY, params); + MapSqlParameterSource[] paramsList = film.getDirectors().stream() + .map(director -> new MapSqlParameterSource() + .addValue("director_id", director.getId()) + .addValue("film_id", film.getId())) + .toArray(MapSqlParameterSource[]::new); + + jdbc.batchUpdate(SET_DIRECTORS_QUERY, paramsList, keyHolder); + } + @Override public LinkedHashSet getFilmGenres(Film film) { MapSqlParameterSource params = new MapSqlParameterSource(); @@ -146,7 +241,18 @@ public LinkedHashSet getFilmGenres(Film film) { return filmGenres; } - private void connectGenres(Collection films) { + @Override + public LinkedHashSet getFilmDirectors(Film film) { + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("film_id", film.getId()); + List directors = jdbc.query(GET_DIRECTORS_QUERY, params, directorRowMapper); + LinkedHashSet filmDirectors = new LinkedHashSet<>(directors); + film.setDirectors(filmDirectors); + return filmDirectors; + } + + @Override + public void connectGenres(Collection films) { List filmIds = films.stream().map(Film::getId).toList(); MapSqlParameterSource params = new MapSqlParameterSource("film_ids", filmIds); SqlRowSet rs = jdbc.queryForRowSet(SELECT_GENRES_BY_FILM_IDS_QUERY, params); @@ -161,13 +267,37 @@ private void connectGenres(Collection films) { } } + @Override + public void connectDirectors(Collection films) { + List filmIds = films.stream().map(Film::getId).toList(); + MapSqlParameterSource params = new MapSqlParameterSource("film_ids", filmIds); + SqlRowSet rs = jdbc.queryForRowSet(SELECT_DIRECTORS_BY_FILM_IDS_QUERY, params); + Map filmsMap = films.stream() + .collect(Collectors.toMap(Film::getId, film -> film)); + + while (rs.next()) { + long filmId = rs.getLong("film_id"); + Director director = new Director(rs.getLong("director_id"), rs.getString("director_name")); + filmsMap.get(filmId).getDirectors().add(director); + } + } + @Override public void addLike(long filmId, long userId) { - GeneratedKeyHolder keyHolder = new GeneratedKeyHolder(); - MapSqlParameterSource params = new MapSqlParameterSource(); - params.addValue("user_id", userId); - params.addValue("film_id", filmId); - jdbc.update(ADD_LIKE_QUERY, params, keyHolder); + + MapSqlParameterSource paramsActivity = new MapSqlParameterSource(); + paramsActivity.addValue("userId", userId); + paramsActivity.addValue("entityId", filmId); + + jdbc.update(ACTIVITY_FILM_LIKE, paramsActivity); + try { + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("user_id", userId); + params.addValue("film_id", filmId); + jdbc.update(ADD_LIKE_QUERY, params); + } catch (DataIntegrityViolationException ex) { + log.debug("Лайк уже существует: userId={}, filmId={}", userId, filmId); + } } @Override @@ -177,15 +307,110 @@ public void deleteLike(long filmId, long userId) { params.addValue("user_id", userId); params.addValue("film_id", filmId); jdbc.update(DELETE_LIKE_QUERY, params, keyHolder); + + MapSqlParameterSource paramsActivity = new MapSqlParameterSource(); + paramsActivity.addValue("userId", userId); + paramsActivity.addValue("entityId", filmId); + + jdbc.update(ACTIVITY_FILM_LIKE_DELETE, paramsActivity); } @Override - public List getPopularFilms(int count) { + public List getPopularFilms(int count, Long genreId, Integer year) { + List popularFilms; MapSqlParameterSource params = new MapSqlParameterSource(); - params.addValue("limit", count); - List popularFilms = jdbc.query(GET_POPULAR_FILMS_QUERY, params, mapper); + params.addValue("count", count); + if (genreId != null) params.addValue("genreId", genreId); + if (year != null) params.addValue("year", year); + final String GET_POPULAR_FILMS_PREFIX_QUERY = """ + SELECT f.*, r.mpa_name, COUNT(l.user_id) AS likes_count + FROM films f + LEFT JOIN likes l ON l.film_id = f.film_id + LEFT JOIN film_genres fg ON fg.film_id = f.film_id + JOIN mpa r ON r.mpa_id = f.mpa_id + WHERE f.film_id IS NOT NULL + """; + final String GET_POPULAR_FILMS_POSTFIX_QUERY = """ + GROUP BY f.film_id, r.mpa_name + ORDER BY likes_count DESC + LIMIT :count + """; + final String GENRE_SQL = "AND fg.genre_id=:genreId"; + final String YEAR_SQL = "AND EXTRACT(YEAR FROM f.release_date)=:year"; + String sql; + if (genreId == null && year == null) { + sql = GET_POPULAR_FILMS_PREFIX_QUERY + " " + GET_POPULAR_FILMS_POSTFIX_QUERY; + } else if (genreId != null && year == null) { + sql = GET_POPULAR_FILMS_PREFIX_QUERY + " " + GENRE_SQL + " " + GET_POPULAR_FILMS_POSTFIX_QUERY; + } else if (genreId == null && year != null) { + sql = GET_POPULAR_FILMS_PREFIX_QUERY + " " + YEAR_SQL + " " + GET_POPULAR_FILMS_POSTFIX_QUERY; + } else { + sql = GET_POPULAR_FILMS_PREFIX_QUERY + " " + GENRE_SQL + " " + YEAR_SQL + " " + GET_POPULAR_FILMS_POSTFIX_QUERY; + } + popularFilms = jdbc.query(sql, params, mapper); connectGenres(popularFilms); + connectDirectors(popularFilms); return popularFilms; } + + @Override + public List getDirectorFilmsByYear(long id) { + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("director_id", id); + List directorFilmsByYear = jdbc.query(GET_DIRECTOR_FILMS_BY_YEAR, params, mapper); + connectGenres(directorFilmsByYear); + connectDirectors(directorFilmsByYear); + return directorFilmsByYear; + } + + @Override + public List getDirectorFilmsByLikes(long id) { + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("director_id", id); + List directorFilmsByYear = jdbc.query(GET_DIRECTOR_FILMS_BY_LIKES, params, mapper); + connectGenres(directorFilmsByYear); + connectDirectors(directorFilmsByYear); + return directorFilmsByYear; + } + + @Override + public List getCommonFilms(long userId, long friendId) { + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("userId", userId); + params.addValue("friendId", friendId); + List commonFilms = jdbc.query(GET_COMMON_FILMS_QUERY, params, mapper); + + connectGenres(commonFilms); + connectDirectors(commonFilms); + return commonFilms; + } + + @Override + public List getSearch(String query, String searchBy) { + MapSqlParameterSource params = new MapSqlParameterSource(); + switch (searchBy) { + case "director" -> { + params.addValue("film_name", ""); + params.addValue("director_name", "%" + query + "%"); + } + case "title" -> { + params.addValue("film_name", "%" + query + "%"); + params.addValue("director_name", ""); + } + case "director,title", "title,director" -> { + params.addValue("film_name", "%" + query + "%"); + params.addValue("director_name", "%" + query + "%"); + } + case "defaultSearch" -> { + params.addValue("film_name", "%%"); + params.addValue("director_name", "%%"); + } + default -> throw new ValidationException("некорректный запрос"); + } + List searchedFilms = jdbc.query(GET_SEARCH, params, mapper); + connectGenres(searchedFilms); + connectDirectors(searchedFilms); + return searchedFilms; + } } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/JdbcReviewRepository.java b/src/main/java/ru/yandex/practicum/filmorate/dal/JdbcReviewRepository.java new file mode 100644 index 0000000..9a08399 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/JdbcReviewRepository.java @@ -0,0 +1,221 @@ +package ru.yandex.practicum.filmorate.dal; + +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.Enum.EventType; +import ru.yandex.practicum.filmorate.Enum.OperationType; +import ru.yandex.practicum.filmorate.dal.mappers.ReviewRowMapper; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Review; + +import java.time.Instant; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Repository +@RequiredArgsConstructor +public class JdbcReviewRepository implements ReviewRepository { + private final NamedParameterJdbcOperations jdbc; + private final ReviewRowMapper reviewMapper; + + private static final String GET_BY_ID_REVIEW = """ + SELECT + r.reviewId, + r.content, + r.isPositive, + r.userId, + r.filmId, + COALESCE(SUM(rl.reaction_type), 0) AS useful + FROM reviews r + LEFT JOIN reviews_likes rl ON r.reviewId = rl.reviewId + WHERE r.reviewId = :reviewId + GROUP BY r.reviewId, r.content, r.isPositive, r.userId, r.filmId; + """; + + private static final String GET_ALL_REVIEW = """ + SELECT + r.reviewId, + r.content, + r.isPositive, + r.userId, + r.filmId, + COALESCE(SUM(rl.reaction_type), 0) AS useful + FROM reviews r + LEFT JOIN reviews_likes rl ON r.reviewId = rl.reviewId + GROUP BY r.reviewId, r.content, r.isPositive, r.userId, r.filmId + LIMIT :count; + """; + private static final String GET_ALL_REVIEW_BY_FILM = """ + SELECT + r.reviewId, + r.content, + r.isPositive, + r.userId, + r.filmId, + COALESCE(SUM(rl.reaction_type), 0) AS useful + FROM reviews r + LEFT JOIN reviews_likes rl ON r.reviewId = rl.reviewId + WHERE r.filmId = :filmId + GROUP BY r.reviewId, r.content, r.isPositive, r.userId, r.filmId + LIMIT :count; + """; + + private static final String CREATE_REVIEW = """ + INSERT INTO reviews (content, isPositive, userId, filmId, useful) + VALUES(:content,:isPositive,:userId,:filmId,:useful) + """; + private static final String UPDATE_REVIEW = """ + UPDATE reviews + SET content=:content, isPositive=:isPositive, useful=:useful + WHERE reviewId=:reviewId + """; + private static final String DELETE_REVIEW = """ + DELETE FROM reviews + WHERE reviewId=:reviewId + """; + private static final String ADD_LIKE_REVIEW = """ + INSERT INTO reviews_likes (reviewId, userId, reaction_type) + VALUES(:reviewId, :userId, 1) + """; + private static final String ADD_DISLIKE_REVIEW = """ + INSERT INTO reviews_likes (reviewId, userId, reaction_type) + VALUES(:reviewId, :userId, -1) + """; + private static final String DELETE_LIKE_REVIEW = """ + DELETE FROM reviews_likes + WHERE reviewId=:reviewId AND userId=:userId + """; + + private static final String ACTIVITY_GENERAL = + "INSERT INTO activity (userId, entityId, eventType, operation, timestamp) VALUES(:userId, :entityId, '"; + + private static final String ACTIVITY_REVIEW_CREATE = ACTIVITY_GENERAL + + EventType.REVIEW + "','" + OperationType.ADD + "', " + instantOfMilliSecond() + ")"; + + private static final String ACTIVITY_REVIEW_UPDATE = ACTIVITY_GENERAL + + EventType.REVIEW + "','" + OperationType.UPDATE + "', " + instantOfMilliSecond() + ")"; + + private static final String ACTIVITY_REVIEW_DELETE = ACTIVITY_GENERAL + + EventType.REVIEW + "','" + OperationType.REMOVE + "', " + instantOfMilliSecond() + ")"; + + + private static long instantOfMilliSecond() { + return Instant.now().toEpochMilli(); + } + + @Override + public Optional getReviewById(long reviewId) { + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("reviewId", reviewId); + try (Stream stream = jdbc.queryForStream(GET_BY_ID_REVIEW, params, reviewMapper)) { + return stream.findAny(); + } + } + + @Override + public List getAllReviewsByFilmId(long filmId, long count) { + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("count", count); + if (filmId == -1) { + return jdbc.query(GET_ALL_REVIEW, params, reviewMapper).stream() + .sorted(Comparator.comparingLong(Review::getUseful).reversed()) + .collect(Collectors.toList()); + } + params.addValue("filmId", filmId); + return jdbc.query(GET_ALL_REVIEW_BY_FILM, params, reviewMapper).stream() + .sorted(Comparator.comparingLong(Review::getUseful).reversed()) + .collect(Collectors.toList()); + } + + @Override + public Review create(Review review) { + GeneratedKeyHolder keyHolder = new GeneratedKeyHolder(); + MapSqlParameterSource params = new MapSqlParameterSource(); + + params.addValue("content", review.getContent()); + params.addValue("isPositive", review.getIsPositive()); + params.addValue("userId", review.getUserId()); + params.addValue("filmId", review.getFilmId()); + params.addValue("useful", 0); + + jdbc.update(CREATE_REVIEW, params, keyHolder); + review.setReviewId(keyHolder.getKeyAs(Long.class)); + + MapSqlParameterSource paramsActivity = new MapSqlParameterSource(); + paramsActivity.addValue("userId", review.getUserId()); + paramsActivity.addValue("entityId", review.getReviewId()); + + jdbc.update(ACTIVITY_REVIEW_CREATE, paramsActivity); + + return review; + } + + @Override + public Review update(Review review) { + GeneratedKeyHolder keyHolder = new GeneratedKeyHolder(); + MapSqlParameterSource params = new MapSqlParameterSource(); + + params.addValue("content", review.getContent()); + params.addValue("isPositive", review.getIsPositive()); + params.addValue("useful", review.getUseful()); + params.addValue("reviewId", review.getReviewId()); + jdbc.update(UPDATE_REVIEW, params, keyHolder); + + Review reviewUpdated = getReviewById(review.getReviewId()) + .orElseThrow(() -> new NotFoundException("Review not found")); + MapSqlParameterSource paramsActivity = new MapSqlParameterSource(); + paramsActivity.addValue("userId", reviewUpdated.getUserId()); + paramsActivity.addValue("entityId", reviewUpdated.getReviewId()); + jdbc.update(ACTIVITY_REVIEW_UPDATE, paramsActivity); + + return reviewUpdated; + } + + @Override + public void deleteReview(long reviewId) { + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("reviewId", reviewId); + + MapSqlParameterSource paramsActivity = new MapSqlParameterSource(); + Review review = getReviewById(reviewId) + .orElseThrow(() -> new NotFoundException("Review not found")); + paramsActivity.addValue("userId", review.getUserId()); + paramsActivity.addValue("entityId", reviewId); + + jdbc.update(ACTIVITY_REVIEW_DELETE, paramsActivity); + jdbc.update(DELETE_REVIEW, params); + } + + @Override + public void addLike(long reviewId, long userId) { + deleteReaction(reviewId, userId); + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("reviewId", reviewId); + params.addValue("userId", userId); + jdbc.update(ADD_LIKE_REVIEW, params); + } + + @Override + public void addDislike(long reviewId, long userId) { + deleteReaction(reviewId, userId); + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("reviewId", reviewId); + params.addValue("userId", userId); + jdbc.update(ADD_DISLIKE_REVIEW, params); + } + + @Override + public void deleteReaction(long reviewId, long userId) { + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("reviewId", reviewId); + params.addValue("userId", userId); + jdbc.update(DELETE_LIKE_REVIEW, params); + } + +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/JdbcUserRepository.java b/src/main/java/ru/yandex/practicum/filmorate/dal/JdbcUserRepository.java index d12b04f..881c63d 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/JdbcUserRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/JdbcUserRepository.java @@ -6,8 +6,13 @@ import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.jdbc.support.GeneratedKeyHolder; import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.Enum.EventType; +import ru.yandex.practicum.filmorate.Enum.OperationType; +import ru.yandex.practicum.filmorate.dal.mappers.ActivityRowMapper; +import ru.yandex.practicum.filmorate.model.Activity; import ru.yandex.practicum.filmorate.model.User; +import java.time.Instant; import java.util.*; import java.util.stream.Stream; @@ -16,17 +21,47 @@ public class JdbcUserRepository implements UserRepository { private final NamedParameterJdbcOperations jdbc; private final RowMapper mapper; + private final ActivityRowMapper activityRowMapper; private static final String CREATE_USER_QUERY = "INSERT INTO users (login, email, name, birthday) VALUES(:login,:email,:name,:birthday)"; private static final String UPDATE_USER_QUERY = "UPDATE users SET login=:login, email=:email, name=:name, birthday=:birthday WHERE user_id=:user_id"; - private static final String GET_USER_BY_ID_QUERY = "SELECT * FROM users WHERE user_id = :user_id"; + private static final String DELETE_USER_QUERY = "DELETE FROM users WHERE user_id=:user_id"; + private static final String GET_USER_BY_ID_QUERY = "SELECT * FROM users WHERE user_id=:user_id"; private static final String GET_ALL_USERS_QUERY = "SELECT * FROM users"; private static final String ADD_FRIEND_QUERY = "INSERT INTO friends(user_id, friend_id) VALUES(:user_id,:friend_id)"; private static final String DELETE_FRIEND_QUERY = "DELETE FROM friends WHERE user_id=:user_id AND friend_id=:friend_id"; - private static final String GET_USER_FRIENDS_QUERY = "SELECT * FROM users WHERE user_id IN (SELECT friend_id FROM friends WHERE user_id = :user_id)"; + private static final String GET_USER_FRIENDS_QUERY = "SELECT * FROM users WHERE user_id IN (SELECT friend_id FROM friends WHERE user_id=:user_id)"; private static final String FIND_COMMON_FRIENDS = "SELECT u.* FROM users u JOIN friends f1 ON u.user_id = f1.friend_id " + "JOIN friends f2 ON u.user_id = f2.friend_id WHERE f1.user_id = :user_id AND f2.user_id = :other_id"; private static final String FIND_USERS_BY_IDS_QUERY = "SELECT * FROM users WHERE user_id IN (:ids)"; + private static final String GET_USER_LIKED_FILMS_QUERY = """ + SELECT f.film_id + FROM likes l + WHERE l.user_id =:user_id + """; + private static final String GET_ALL_USERS_LIKES = "SELECT user_id, film_id FROM likes"; + + private static final String ACTIVITY_GENERAL = + "INSERT INTO activity (userId, entityId, eventType, operation, timestamp) VALUES(:userId, :entityId, '"; + + private static final String ACTIVITY_FRIEND_ADD = ACTIVITY_GENERAL + + EventType.FRIEND + "','" + OperationType.ADD + "'," + instantOfMilliSecond() + ")"; + + private static final String ACTIVITY_FRIEND_DELETE = ACTIVITY_GENERAL + + EventType.FRIEND + "','" + OperationType.REMOVE + "', " + instantOfMilliSecond() + ")"; + + private static final String GET_ACTIVITY_BY_USER_ID = "SELECT * FROM activity WHERE userId = :userId ORDER BY eventId ASC"; + + private static long instantOfMilliSecond() { + return Instant.now().toEpochMilli(); + } + + @Override + public List getActivityById(long userId) { + MapSqlParameterSource paramsActivity = new MapSqlParameterSource(); + paramsActivity.addValue("userId", userId); + return jdbc.query(GET_ACTIVITY_BY_USER_ID, paramsActivity, activityRowMapper); + } @Override public User create(User user) { @@ -43,7 +78,6 @@ public User create(User user) { @Override public User update(User user) { - GeneratedKeyHolder keyHolder = new GeneratedKeyHolder(); MapSqlParameterSource params = new MapSqlParameterSource(); params.addValue("login", user.getLogin()); params.addValue("email", user.getEmail()); @@ -54,6 +88,13 @@ public User update(User user) { return user; } + @Override + public void deleteUser(long userId) { + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("user_id", userId); + jdbc.update(DELETE_USER_QUERY, params); + } + @Override public Optional getUserById(long userId) { MapSqlParameterSource params = new MapSqlParameterSource(); @@ -66,7 +107,6 @@ public Optional getUserById(long userId) { @Override public List getAllUsers() { return jdbc.query(GET_ALL_USERS_QUERY, mapper); - } @Override @@ -76,6 +116,11 @@ public void addFriend(long userId, long friendId) { params.addValue("user_id", userId); params.addValue("friend_id", friendId); jdbc.update(ADD_FRIEND_QUERY, params, keyHolder); + + MapSqlParameterSource paramsActivity = new MapSqlParameterSource(); + paramsActivity.addValue("userId", userId); + paramsActivity.addValue("entityId", friendId); + jdbc.update(ACTIVITY_FRIEND_ADD, paramsActivity); } @Override @@ -85,6 +130,11 @@ public void deleteFriend(long userId, long friendId) { params.addValue("user_id", userId); params.addValue("friend_id", friendId); jdbc.update(DELETE_FRIEND_QUERY, params, keyHolder); + + MapSqlParameterSource paramsActivity = new MapSqlParameterSource(); + paramsActivity.addValue("userId", userId); + paramsActivity.addValue("entityId", friendId); + jdbc.update(ACTIVITY_FRIEND_DELETE, paramsActivity); } @Override @@ -108,4 +158,24 @@ public List getUsersByIds(List ids) { params.addValue("ids", ids); return jdbc.query(FIND_USERS_BY_IDS_QUERY, params, mapper); } + + @Override + public Set getUserLikedFilms(long userId) { + MapSqlParameterSource params = new MapSqlParameterSource("user_id", userId); + List result = jdbc.queryForList(GET_USER_LIKED_FILMS_QUERY, params, Long.class); + return new HashSet<>(result); + } + + @Override + public Map> getAllUserLikes() { + List> rows = jdbc.queryForList(GET_ALL_USERS_LIKES, new MapSqlParameterSource()); + + Map> userLikes = new HashMap<>(); + for (Map row : rows) { + Long userId = ((Number) row.get("user_id")).longValue(); + Long filmId = ((Number) row.get("film_id")).longValue(); + userLikes.computeIfAbsent(userId, k -> new HashSet<>()).add(filmId); + } + return userLikes; + } } \ No newline at end of file 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..e298759 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/ReviewRepository.java @@ -0,0 +1,25 @@ +package ru.yandex.practicum.filmorate.dal; + +import ru.yandex.practicum.filmorate.model.Review; + +import java.util.List; +import java.util.Optional; + +public interface ReviewRepository { + Optional getReviewById(long reviewId); + + List getAllReviewsByFilmId(long filmId, long count); + + Review create(Review review); + + Review update(Review review); + + void deleteReview(long reviewId); + + void addLike(long reviewId, long userId); + + void addDislike(long reviewId, long userId); + + void deleteReaction(long reviewId, long userId); + +} 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 34b78b9..12481ac 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/UserRepository.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/UserRepository.java @@ -1,11 +1,16 @@ package ru.yandex.practicum.filmorate.dal; +import ru.yandex.practicum.filmorate.model.Activity; import ru.yandex.practicum.filmorate.model.User; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.Set; public interface UserRepository { + List getActivityById(long activityId); + Optional getUserById(long userId); List getUsersByIds(List userIds); @@ -16,6 +21,8 @@ public interface UserRepository { User update(User user); + void deleteUser(long userId); + void addFriend(long userId, long friendId); void deleteFriend(long userId, long friendId); @@ -23,4 +30,8 @@ public interface UserRepository { List getUserFriends(long userId); List getCommonFriends(long userId, long otherId); + + Set getUserLikedFilms(long userId); + + Map> getAllUserLikes(); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/mappers/ActivityRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/dal/mappers/ActivityRowMapper.java new file mode 100644 index 0000000..d32ad12 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/mappers/ActivityRowMapper.java @@ -0,0 +1,23 @@ +package ru.yandex.practicum.filmorate.dal.mappers; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.Activity; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@Component +public class ActivityRowMapper implements RowMapper { + @Override + public Activity mapRow(ResultSet rs, int rowNum) throws SQLException { + Activity activity = new Activity(); + activity.setEventId(rs.getLong("eventId")); + activity.setUserId(rs.getLong("userId")); + activity.setEntityId(rs.getLong("entityId")); + activity.setEventType(rs.getString("eventType")); + activity.setOperation(rs.getString("operation")); + activity.setTimestamp(rs.getLong("timestamp")); + return activity; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/mappers/DirectorRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/dal/mappers/DirectorRowMapper.java new file mode 100644 index 0000000..0046fbd --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/mappers/DirectorRowMapper.java @@ -0,0 +1,19 @@ +package ru.yandex.practicum.filmorate.dal.mappers; + +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 { + + public Director mapRow(ResultSet resultSet, int rowNum) throws SQLException { + Director director = new Director(); + director.setId(resultSet.getLong("director_id")); + director.setName(resultSet.getString("director_name")); + return director; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/mappers/FilmRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/dal/mappers/FilmRowMapper.java index 0a5d1dc..864660b 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/mappers/FilmRowMapper.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/mappers/FilmRowMapper.java @@ -24,6 +24,7 @@ public Film mapRow(ResultSet resultSet, int rowNum) throws SQLException { film.setDuration(resultSet.getInt("duration")); film.setMpa(mpa); film.setGenres(new LinkedHashSet<>()); + film.setDirectors(new LinkedHashSet<>()); film.setLikesCount(resultSet.getInt("likes_count")); return film; } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/mappers/ReviewRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/dal/mappers/ReviewRowMapper.java new file mode 100644 index 0000000..244607e --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/mappers/ReviewRowMapper.java @@ -0,0 +1,23 @@ +package ru.yandex.practicum.filmorate.dal.mappers; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.Review; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@Component +public class ReviewRowMapper implements RowMapper { + @Override + public Review mapRow(ResultSet rs, int rowNum) throws SQLException { + Review review = new Review(); + review.setReviewId(rs.getLong("reviewId")); + review.setContent(rs.getString("content")); + review.setIsPositive(rs.getBoolean("isPositive")); + review.setUserId(rs.getLong("userId")); + review.setFilmId(rs.getLong("filmId")); + review.setUseful(rs.getLong("useful")); + return review; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/exception/ErrorHandler.java b/src/main/java/ru/yandex/practicum/filmorate/exception/ErrorHandler.java index da47eeb..8d3a617 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/exception/ErrorHandler.java +++ b/src/main/java/ru/yandex/practicum/filmorate/exception/ErrorHandler.java @@ -1,10 +1,13 @@ package ru.yandex.practicum.filmorate.exception; +import jakarta.validation.ConstraintViolationException; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; -import org.springframework.web.bind.annotation.ExceptionHandler; @RestControllerAdvice @Slf4j @@ -38,4 +41,28 @@ public ErrorResponse handleServerError(final InternalServerException e) { e.getMessage() ); } + + @ResponseBody + @ExceptionHandler(ConstraintViolationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse onConstraintValidationException(ConstraintViolationException e) { + log.error("Параметры не прошли валидацию Spring {}", e.getMessage()); + return new ErrorResponse( + "Объект не прошёл валидацию Spring ", + e.getMessage() + ); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ResponseBody + public ErrorResponse onMethodArgumentNotValidException(MethodArgumentNotValidException e) { + log.error("Объект не прошёл валидацию Spring {}", e.getMessage()); + return new ErrorResponse( + "Объект не прошёл валидацию Spring ", + e.getMessage() + ); + } + + } diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Activity.java b/src/main/java/ru/yandex/practicum/filmorate/model/Activity.java new file mode 100644 index 0000000..a6e54f6 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Activity.java @@ -0,0 +1,23 @@ +package ru.yandex.practicum.filmorate.model; + +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Activity { + private long eventId; + @NotNull + private long userId; + @NotNull + private long entityId; + @NotNull + private String eventType; + @NotNull + private String operation; + @NotNull + private long timestamp; +} 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..03d29e6 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Director.java @@ -0,0 +1,15 @@ +package ru.yandex.practicum.filmorate.model; + +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +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 e05690f..cbcffd8 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java @@ -2,6 +2,7 @@ import jakarta.validation.constraints.*; import lombok.*; +import ru.yandex.practicum.filmorate.annotation.StartDate; import java.time.LocalDate; import java.util.LinkedHashSet; @@ -21,6 +22,7 @@ public class Film { private Integer duration; private LinkedHashSet genres; + private LinkedHashSet directors; @NotNull private Mpa mpa; int likesCount; 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..02519f5 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Review.java @@ -0,0 +1,25 @@ +package ru.yandex.practicum.filmorate.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Review { + private long reviewId; + @NotBlank(message = "Отзыв не может быть пустым") + private String content; + @NotNull + @JsonProperty("isPositive") + private Boolean isPositive; + @NotNull + private Long userId; + @NotNull + private Long filmId; + private long useful; +} 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..aa069ce --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/DirectorService.java @@ -0,0 +1,17 @@ +package ru.yandex.practicum.filmorate.service; + +import ru.yandex.practicum.filmorate.model.Director; + +import java.util.List; + +public interface DirectorService { + Director create(Director director); + + List getAll(); + + Director getById(Long directorId); + + Director update(Director director); + + void delete(Long id); +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/DirectorServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/DirectorServiceImpl.java new file mode 100644 index 0000000..dc324ef --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/DirectorServiceImpl.java @@ -0,0 +1,59 @@ +package ru.yandex.practicum.filmorate.service; + + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.dal.JdbcDirectorRepository; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.exception.ValidationException; +import ru.yandex.practicum.filmorate.model.Director; + +import java.util.List; + + +@Service +@Slf4j +@RequiredArgsConstructor +public class DirectorServiceImpl implements DirectorService { + + private final JdbcDirectorRepository jdbcDirectorRepository; + + @Override + public Director create(Director director) { + log.info("Создан новый режисёр"); + return jdbcDirectorRepository.create(director); + } + + @Override + public List getAll() { + return jdbcDirectorRepository.getAll(); + } + + @Override + public Director getById(Long directorId) { + return jdbcDirectorRepository.getById(directorId) + .orElseThrow(() -> new NotFoundException("Некорректный id = " + directorId)); + } + + @Override + public Director update(Director director) { + if (director.getId() == -1) { + log.error("Id должен быть указан"); + throw new ValidationException("Id должен быть указан"); + } + if (jdbcDirectorRepository.getAll().stream().anyMatch((oldDirector) -> oldDirector.getId() == director.getId())) { + log.info("Пользователь обновлён"); + return jdbcDirectorRepository.update(director); + } + throw new NotFoundException("Режиссёр с id = " + director.getId() + " не найден"); + } + + @Override + public void delete(Long id) { + getById(id); + jdbcDirectorRepository.delete(id); + log.info("Режиссёр удалён"); + } +} + 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 89b6f7d..27343ee 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java @@ -5,7 +5,7 @@ import java.util.List; public interface FilmService { - Film getById(long filmId); + Film getFilmById(long filmId); List getFilms(); @@ -13,9 +13,17 @@ public interface FilmService { Film update(Film film); + void deleteFilm(long filmId); + void addLike(long filmId, long userId); void deleteLike(long filmId, long userId); - List getPopularFilms(int count); + List getPopularFilms(int count, Long genreId, Integer year); + + List getDirectorFilms(long id, String sortBy); + + List getCommonFilms(long userId, long friendId); + + List getSearch(String query, String searchBy); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/FilmServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/FilmServiceImpl.java index 27b0ded..3474130 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmServiceImpl.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmServiceImpl.java @@ -4,10 +4,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; -import ru.yandex.practicum.filmorate.dal.JdbcFilmRepository; -import ru.yandex.practicum.filmorate.dal.JdbcGenreRepository; -import ru.yandex.practicum.filmorate.dal.JdbcMpaRepository; -import ru.yandex.practicum.filmorate.dal.JdbcUserRepository; +import ru.yandex.practicum.filmorate.dal.*; import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.exception.ValidationException; import ru.yandex.practicum.filmorate.model.Film; @@ -26,9 +23,10 @@ public class FilmServiceImpl implements FilmService { private final JdbcUserRepository jdbcUserRepository; private final JdbcGenreRepository jdbcGenreRepository; private final JdbcMpaRepository jdbcMpaRepository; + private final JdbcDirectorRepository jdbcDirectorRepository; @Override - public Film getById(long filmId) { + public Film getFilmById(long filmId) { return jdbcFilmRepository.getFilmById(filmId) .orElseThrow(() -> new NotFoundException("Фильм с id = " + filmId + " не найден")); } @@ -71,7 +69,7 @@ public Film update(Film film) { } if (jdbcFilmRepository.getAllFilms().stream().anyMatch((oldFilm) -> oldFilm.getId() == film.getId())) { - Film savedFilm = getById(film.getId()); + Film savedFilm = getFilmById(film.getId()); film.setLikesCount(savedFilm.getLikesCount()); Mpa mpa = jdbcMpaRepository.getMpaById(film.getMpa().getId()) .orElseThrow(() -> new NotFoundException("Рейтинг с id " + film.getMpa().getId() + " не найден")); @@ -97,9 +95,15 @@ public Film update(Film film) { throw new NotFoundException("Фильм с id = " + film.getId() + " не найден"); } + public void deleteFilm(long filmId) { + getFilmById(filmId); + jdbcFilmRepository.deleteFilm(filmId); + log.info("Фильм удален"); + } + @Override public void addLike(long filmId, long userId) { - Film film = getById(filmId); + Film film = getFilmById(filmId); jdbcUserRepository.getUserById(userId) .orElseThrow(() -> new NotFoundException("Пользователь с id = " + userId + " не найден")); @@ -112,7 +116,7 @@ public void addLike(long filmId, long userId) { @Override public void deleteLike(long filmId, long userId) { - Film film = getById(filmId); + Film film = getFilmById(filmId); jdbcUserRepository.getUserById(userId) .orElseThrow(() -> new NotFoundException("Пользователь с id = " + userId + " не найден")); @@ -124,8 +128,30 @@ public void deleteLike(long filmId, long userId) { } @Override - public List getPopularFilms(int count) { - List popularFilms = jdbcFilmRepository.getPopularFilms(count); + public List getPopularFilms(int count, Long genreId, Integer year) { + List popularFilms = jdbcFilmRepository.getPopularFilms(count, genreId, year); return popularFilms; } + + @Override + public List getDirectorFilms(long id, String sortBy) { + jdbcDirectorRepository.getById(id) + .orElseThrow(() -> new NotFoundException("Режиссёр с id = " + id + " не найден")); + if (sortBy.equals("year")) { + return jdbcFilmRepository.getDirectorFilmsByYear(id); + } else if (sortBy.equals("likes")) { + return jdbcFilmRepository.getDirectorFilmsByLikes(id); + } else throw new ValidationException("некорректный запрос"); + } + + @Override + public List getCommonFilms(long userId, long friendId) { + List commonFilms = jdbcFilmRepository.getCommonFilms(userId, friendId); + return commonFilms; + } + + @Override + public List getSearch(String query, String searchBy) { + return jdbcFilmRepository.getSearch(query, searchBy); + } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/RecommendationService.java b/src/main/java/ru/yandex/practicum/filmorate/service/RecommendationService.java new file mode 100644 index 0000000..b3f6f3d --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/RecommendationService.java @@ -0,0 +1,8 @@ +package ru.yandex.practicum.filmorate.service; + +import ru.yandex.practicum.filmorate.model.Film; +import java.util.List; + +public interface RecommendationService { + List getRecommendations(long userId); +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/RecommendationServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/RecommendationServiceImpl.java new file mode 100644 index 0000000..95ee2ec --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/RecommendationServiceImpl.java @@ -0,0 +1,67 @@ +package ru.yandex.practicum.filmorate.service; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.dal.JdbcUserRepository; +import ru.yandex.practicum.filmorate.dal.JdbcFilmRepository; +import ru.yandex.practicum.filmorate.model.Film; + +import java.util.*; +import java.util.stream.Collectors; + +@Service +@Slf4j +@RequiredArgsConstructor +public class RecommendationServiceImpl implements RecommendationService { + private final JdbcUserRepository userRepository; + private final JdbcFilmRepository filmRepository; + private final SlopeOne slopeOne; + + @PostConstruct + public void init() { + slopeOne.buildDifferences(Collections.emptyMap()); + } + + private void rebuildModel() { + var raw = userRepository.getAllUserLikes(); + var data = raw.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + e -> e.getValue().stream().collect(Collectors.toMap(f -> f, f -> 1.0)) + )); + slopeOne.buildDifferences(data); + } + + @Override + public List getRecommendations(long userId) { + var myLikes = userRepository.getAllUserLikes() + .getOrDefault(userId, Collections.emptySet()); + if (myLikes.isEmpty()) { + return Collections.emptyList(); + } + + rebuildModel(); + + var predictions = slopeOne.predictRatings( + myLikes.stream().collect(Collectors.toMap(f -> f, f -> 1.0)) + ); + predictions.keySet().removeAll(myLikes); + + if (predictions.isEmpty()) { + return Collections.emptyList(); + } + + var recommendations = predictions.entrySet().stream() + .sorted(Map.Entry.comparingByValue().reversed()) + .map(e -> filmRepository.getFilmById(e.getKey())) + .flatMap(Optional::stream) + .collect(Collectors.toList()); + + filmRepository.connectGenres(recommendations); + filmRepository.connectDirectors(recommendations); + + return recommendations; + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/ReviewService.java b/src/main/java/ru/yandex/practicum/filmorate/service/ReviewService.java new file mode 100644 index 0000000..f2a70b1 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/ReviewService.java @@ -0,0 +1,25 @@ +package ru.yandex.practicum.filmorate.service; + +import ru.yandex.practicum.filmorate.model.Review; + +import java.util.List; +import java.util.Optional; + +public interface ReviewService { + Optional getReviewById(long reviewId); + + List getAllReviewsByFilmId(long filmId, long count); + + Review create(Review review); + + Review update(Review review); + + void deleteReview(long reviewId); + + void addLike(long reviewId, long userId); + + void addDislike(long reviewId, long userId); + + void deleteReaction(long reviewId, long userId); + +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/ReviewServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/ReviewServiceImpl.java new file mode 100644 index 0000000..6b8f385 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/ReviewServiceImpl.java @@ -0,0 +1,89 @@ +package ru.yandex.practicum.filmorate.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; +import ru.yandex.practicum.filmorate.dal.JdbcFilmRepository; +import ru.yandex.practicum.filmorate.dal.JdbcReviewRepository; +import ru.yandex.practicum.filmorate.dal.JdbcUserRepository; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Review; + +import java.util.List; +import java.util.Optional; + +@Service +@Slf4j +@RequiredArgsConstructor +@Validated +public class ReviewServiceImpl implements ReviewService { + private final JdbcReviewRepository jdbcReviewRepository; + private final JdbcFilmRepository jdbcFilmRepository; + private final JdbcUserRepository jdbcUserRepository; + + @Override + public Optional getReviewById(long reviewId) { + checkReviewId(reviewId); + return jdbcReviewRepository.getReviewById(reviewId); + } + + @Override + public List getAllReviewsByFilmId(long filmId, long count) { + return jdbcReviewRepository.getAllReviewsByFilmId(filmId, count); + } + + @Override + public Review create(Review review) { + checkUserId(review.getUserId()); + checkFilmId(review.getFilmId()); + return jdbcReviewRepository.create(review); + } + + @Override + public Review update(Review review) { + checkUserId(review.getUserId()); + checkFilmId(review.getFilmId()); + return jdbcReviewRepository.update(review); + } + + @Override + public void deleteReview(long reviewId) { + checkReviewId(reviewId); + jdbcReviewRepository.deleteReview(reviewId); + } + + @Override + public void addLike(long reviewId, long userId) { + checkReviewId(reviewId); + checkUserId(userId); + jdbcReviewRepository.addLike(reviewId, userId); + } + + @Override + public void addDislike(long reviewId, long userId) { + checkReviewId(reviewId); + checkUserId(userId); + jdbcReviewRepository.addDislike(reviewId, userId); + } + + @Override + public void deleteReaction(long reviewId, long userId) { + checkReviewId(reviewId); + checkUserId(userId); + jdbcReviewRepository.deleteReaction(reviewId, userId); + } + + private void checkUserId(long id) { + jdbcUserRepository.getUserById(id).orElseThrow(() -> new NotFoundException("Пользователь не найден")); + } + + private void checkFilmId(long id) { + jdbcFilmRepository.getFilmById(id).orElseThrow(() -> new NotFoundException("Фильм не найден")); + } + + private void checkReviewId(long id) { + jdbcReviewRepository.getReviewById(id).orElseThrow(() -> new NotFoundException("Отзыв не найден")); + } + +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/SlopeOne.java b/src/main/java/ru/yandex/practicum/filmorate/service/SlopeOne.java new file mode 100644 index 0000000..be76669 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/SlopeOne.java @@ -0,0 +1,72 @@ +package ru.yandex.practicum.filmorate.service; + +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +@Component +public class SlopeOne { + private final Map> diffMatrix = new HashMap<>(); + + private final Map> freqMatrix = new HashMap<>(); + + public void buildDifferences(Map> data) { + for (Map userRatings : data.values()) { + for (Map.Entry e1 : userRatings.entrySet()) { + long i = e1.getKey(); + double r1 = e1.getValue(); + + diffMatrix.computeIfAbsent(i, k -> new HashMap<>()); + freqMatrix.computeIfAbsent(i, k -> new HashMap<>()); + + for (Map.Entry e2 : userRatings.entrySet()) { + long j = e2.getKey(); + double r2 = e2.getValue(); + + + diffMatrix.get(i).merge(j, r1 - r2, Double::sum); + + freqMatrix.get(i).merge(j, 1, Integer::sum); + } + } + } + + for (Long i : diffMatrix.keySet()) { + for (Long j : diffMatrix.get(i).keySet()) { + double totalDiff = diffMatrix.get(i).get(j); + int count = freqMatrix.get(i).get(j); + diffMatrix.get(i).put(j, totalDiff / count); + } + } + } + + public Map predictRatings(Map userRatings) { + Map predictions = new HashMap<>(); + Map counts = new HashMap<>(); + + for (Map.Entry entry : userRatings.entrySet()) { + long j = entry.getKey(); + double rj = entry.getValue(); + + for (Map.Entry> row : diffMatrix.entrySet()) { + long i = row.getKey(); + Map diffs = row.getValue(); + + if (diffs.containsKey(j)) { + double diff = diffs.get(j); + int freq = freqMatrix.get(i).get(j); + + predictions.merge(i, (diff + rj) * freq, Double::sum); + counts.merge(i, freq, Integer::sum); + } + } + } + + for (Long i : predictions.keySet()) { + predictions.put(i, predictions.get(i) / counts.get(i)); + } + + return predictions; + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java index 6e0a81e..0b68c8a 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java @@ -1,10 +1,13 @@ package ru.yandex.practicum.filmorate.service; +import ru.yandex.practicum.filmorate.model.Activity; import ru.yandex.practicum.filmorate.model.User; import java.util.List; public interface UserService { + List getActivityById(long activityId); + User getUserById(long userId); List getUsers(); @@ -13,6 +16,8 @@ public interface UserService { User update(User user); + void deleteUser(long userId); + void addFriend(long userId, long friendId); void deleteFriend(long userId, long friendId); diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/UserServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/UserServiceImpl.java index 4a88a17..3decc38 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/UserServiceImpl.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserServiceImpl.java @@ -7,9 +7,11 @@ import ru.yandex.practicum.filmorate.dal.JdbcUserRepository; import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.exception.ValidationException; +import ru.yandex.practicum.filmorate.model.Activity; import ru.yandex.practicum.filmorate.model.User; -import java.util.*; +import java.util.ArrayList; +import java.util.List; @Service @Slf4j @@ -18,6 +20,15 @@ public class UserServiceImpl implements UserService { private final JdbcUserRepository jdbcUserRepository; + @Override + public List getActivityById(long userId) { + List activities = jdbcUserRepository.getActivityById(userId); + if (activities.isEmpty()) { + throw new NotFoundException("Активность пользователя не найдена"); + } + return activities; + } + @Override public User getUserById(long userId) { return jdbcUserRepository.getUserById(userId) @@ -51,6 +62,13 @@ public User update(User user) { throw new NotFoundException("Пользователь с id = " + user.getId() + " не найден"); } + @Override + public void deleteUser(long userId) { + getUserById(userId); + jdbcUserRepository.deleteUser(userId); + log.info("Пользователь удален"); + } + private void checkUsersInTable(List usersIds) { List foundUsers = jdbcUserRepository.getUsersByIds(usersIds); if (foundUsers.size() != usersIds.size()) { @@ -75,10 +93,7 @@ public void deleteFriend(long userId, long friendId) { @Override public List getAllFriends(long userId) { - User user = getUserById(userId); - if (user == null) { - throw new NotFoundException("Пользователь с id = " + userId + " не найден"); - } + getUserById(userId); return jdbcUserRepository.getUserFriends(userId); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d365592..9e11135 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,6 +1,8 @@ logging.level.org.zalando.logbook=TRACE spring.sql.init.mode=always -spring.datasource.url=jdbc:h2:file:./db/filmorate +spring.datasource.url=jdbc:h2:file:./db/filmorate-test 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.datasource.schema=classpath:schema.sql +spring.datasource.data=classpath:data.sql \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index c446ed6..26525b5 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -9,11 +9,16 @@ CREATE TABLE IF NOT EXISTS mpa ( PRIMARY KEY (mpa_id) ); +CREATE TABLE IF NOT EXISTS directors ( + director_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + director_name VARCHAR(100) NOT NULL UNIQUE +); + CREATE TABLE IF NOT EXISTS films ( film_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name VARCHAR(255) NOT NULL, description VARCHAR(200), - release_date DATE CHECK (release_date <= CURRENT_DATE), + release_date DATE, duration INT CHECK (duration > 0), mpa_id BIGINT REFERENCES mpa(mpa_id) ); @@ -24,6 +29,12 @@ CREATE TABLE IF NOT EXISTS film_genres ( PRIMARY KEY (film_id, genre_id) ); +CREATE TABLE IF NOT EXISTS film_directors ( + film_id BIGINT NOT NULL REFERENCES films(film_id) ON DELETE CASCADE, + director_id BIGINT NOT NULL REFERENCES directors(director_id) ON DELETE CASCADE, + PRIMARY KEY (film_id, director_id) +); + CREATE TABLE IF NOT EXISTS users ( user_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, email VARCHAR(255) NOT NULL UNIQUE, @@ -43,3 +54,28 @@ CREATE TABLE IF NOT EXISTS likes ( user_id BIGINT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, PRIMARY KEY (film_id, user_id) ); + +CREATE TABLE IF NOT EXISTS reviews ( + reviewId BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + content VARCHAR(255) NOT NULL, + isPositive boolean NOT NULL, + userId BIGINT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, + filmId BIGINT NOT NULL REFERENCES films(film_id) ON DELETE CASCADE, + useful INTEGER DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS reviews_likes ( + reviewId BIGINT NOT NULL REFERENCES reviews(reviewId) ON DELETE CASCADE, + userId BIGINT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, + reaction_type SMALLINT NOT NULL CHECK (reaction_type IN (-1, 1)), + PRIMARY KEY (reviewId, userId) +); + +CREATE TABLE IF NOT EXISTS activity ( + eventId BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + userId BIGINT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, + entityId INT NOT NULL, + eventType VARCHAR(20) NOT NULL, + operation VARCHAR(20) NOT NULL, + timestamp BIGINT NOT NULL +); diff --git a/src/test/java/ru/yandex/practicum/filmorate/dal/JdbcFilmRepositoryTest.java b/src/test/java/ru/yandex/practicum/filmorate/dal/JdbcFilmRepositoryTest.java index 6fa993f..425bd3c 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/dal/JdbcFilmRepositoryTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/dal/JdbcFilmRepositoryTest.java @@ -51,6 +51,7 @@ static Film getTestFilm() { genre.setName("Комедия"); film.getGenres().add(genre); film.setLikesCount(1); + film.setDirectors(new LinkedHashSet<>()); return film; } @@ -76,6 +77,7 @@ private static Film getTestFilmToCreateOrUpdate() { film.getGenres().add(genre); film.setLikesCount(1); + film.setDirectors(new LinkedHashSet<>()); return film; } @@ -106,6 +108,7 @@ private static List getAllTestFilms() { film1.setGenres(new LinkedHashSet<>()); film1.getGenres().add(genre1); film1.setLikesCount(3); + film1.setDirectors(new LinkedHashSet<>()); films.add(film1); Film film2 = new Film(); @@ -123,6 +126,7 @@ private static List getAllTestFilms() { film2.getGenres().add(genre1); film2.getGenres().add(genre3); film2.setLikesCount(2); + film2.setDirectors(new LinkedHashSet<>()); films.add(film2); return films; @@ -197,6 +201,24 @@ void shouldUpdateFilm() { .isEqualTo(getTestFilmToCreateOrUpdate()); } + @Test + @DisplayName("Должен удалять фильм") + void shouldDeleteFilm() { + Film filmBeforeDelete = jdbcFilmRepository.getFilmById(TEST_FILM_ID) + .orElseThrow(() -> new NotFoundException("Не найден фильм с id = " + TEST_FILM_ID)); + + assertThat(jdbcFilmRepository.getAllFilms()).hasSize(3); + assertThat(jdbcFilmRepository.getPopularFilms(1000, 1L, 2000)).hasSize(1); + assertThat(filmBeforeDelete) + .usingRecursiveComparison() + .isEqualTo(getTestFilm()); + + jdbcFilmRepository.deleteFilm(TEST_FILM_ID); + + assertThat(jdbcFilmRepository.getAllFilms()).hasSize(2); + assertThat(jdbcFilmRepository.getPopularFilms(1000, 1L, 2000)).hasSize(0); + } + @Test @DisplayName("Должен добавлять записи о лайках") void shouldAddLike() { @@ -232,16 +254,13 @@ void shouldDeleteLikeRecord() { @Test @DisplayName("Должен возвращать сортированный список популярных фильмов") void shouldGetPopularFilms() { - List popularFilms = jdbcFilmRepository.getPopularFilms(2); + List popularFilms = jdbcFilmRepository.getPopularFilms(2, 1L, 1950); List filmsTest = getAllTestFilms(); - assertThat(popularFilms).hasSize(2); + assertThat(popularFilms).hasSize(1); assertThat(popularFilms.get(0)) .usingRecursiveComparison() .isEqualTo(filmsTest.get(1)); - assertThat(popularFilms.get(1)) - .usingRecursiveComparison() - .isEqualTo(filmsTest.get(2)); } } \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/dal/JdbcUserRepositoryTest.java b/src/test/java/ru/yandex/practicum/filmorate/dal/JdbcUserRepositoryTest.java index 271d633..9a16aa0 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/dal/JdbcUserRepositoryTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/dal/JdbcUserRepositoryTest.java @@ -153,6 +153,22 @@ void shouldUpdateUser() { .isEqualTo(getTestUserToUpdate()); } + @Test + @DisplayName("Должен удалять пользователя") + void shouldDeleteUser() { + User userBeforeDelete = jdbcUserRepository.getUserById(TEST_USER_ID) + .orElseThrow(() -> new NotFoundException("Не найден пользователь с id = " + TEST_USER_ID)); + + assertThat(jdbcUserRepository.getAllUsers()).hasSize(3); + assertThat(userBeforeDelete) + .usingRecursiveComparison() + .isEqualTo(getTestUser()); + + jdbcUserRepository.deleteUser(TEST_USER_ID); + + assertThat(jdbcUserRepository.getAllUsers()).hasSize(2); + } + @Test @DisplayName("Должен возвращать друзей пользователя") void shouldGetUserFriends() { diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index c0bd8e3..aee5f55 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -4,4 +4,4 @@ spring.datasource.url=jdbc:h2:file:./db/filmorate spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password=password -spring.sql.init.data-locations=classpath:test-data.sql \ No newline at end of file +spring.sql.init.data-locations=classpath:test-data.sql