From fa8c6d07257022e04fce4bfe94c1541afbf3e5ef Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 29 Apr 2025 13:59:57 +0300 Subject: [PATCH 1/4] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=BE=D0=BD?= =?UTF-8?q?=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=E2=80=94=20?= =?UTF-8?q?=D1=81=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D1=81=D0=BE=D1=81=D1=82=D0=BE=D1=8F=D0=BD=D0=B8=D1=8F=20=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5?= =?UTF-8?q?=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B7=D0=B0=D0=BF=D1=83=D1=81=D0=BA?= =?UTF-8?q?=D0=B0.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 11 ++ .../filmorate/controller/FilmController.java | 76 +++------ .../filmorate/controller/GenreController.java | 25 +++ .../controller/GlobalExceptionHandler.java | 30 ++-- .../filmorate/controller/MpaController.java | 25 +++ .../filmorate/controller/UserController.java | 94 ++--------- .../practicum/filmorate/model/Film.java | 33 +++- .../practicum/filmorate/model/Genre.java | 13 ++ .../yandex/practicum/filmorate/model/Mpa.java | 24 +++ .../practicum/filmorate/model/User.java | 30 +++- .../filmorate/service/FilmService.java | 40 ++++- .../filmorate/service/UserService.java | 39 +++-- .../filmorate/storage/FilmDbStorage.java | 157 ++++++++++++++++++ .../filmorate/storage/GenreDbStorage.java | 38 +++++ .../storage/InMemoryFilmStorage.java | 79 --------- .../storage/InMemoryUserStorage.java | 89 ---------- .../filmorate/storage/MpaDbStorage.java | 32 ++++ .../filmorate/storage/UserDbStorage.java | 115 +++++++++++++ src/main/resources/application.properties | 8 +- src/main/resources/data.sql | 12 ++ src/main/resources/schema.sql | 51 ++++++ .../practicum/filmorate/BoundaryTests.java | 56 ------- .../filmorate/FilmControllerTest.java | 109 ------------ .../filmorate/UserControllerTest.java | 113 ------------- .../filmorate/UserDbStorageTest.java | 40 +++++ 25 files changed, 710 insertions(+), 629 deletions(-) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/controller/MpaController.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/model/Genre.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/model/Mpa.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/FilmDbStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/GenreDbStorage.java delete mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryFilmStorage.java delete mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryUserStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/MpaDbStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/UserDbStorage.java create mode 100644 src/main/resources/data.sql create mode 100644 src/main/resources/schema.sql delete mode 100644 src/test/java/ru/yandex/practicum/filmorate/BoundaryTests.java delete mode 100644 src/test/java/ru/yandex/practicum/filmorate/FilmControllerTest.java delete mode 100644 src/test/java/ru/yandex/practicum/filmorate/UserControllerTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/UserDbStorageTest.java diff --git a/pom.xml b/pom.xml index 9c95881..d92b5c6 100644 --- a/pom.xml +++ b/pom.xml @@ -45,6 +45,17 @@ 3.7.2 + + com.h2database + h2 + runtime + + + + org.springframework.boot + spring-boot-starter-jdbc + + 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 068d66f..38d4c93 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -1,21 +1,19 @@ package ru.yandex.practicum.filmorate.controller; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; -import jakarta.validation.Valid; -import jakarta.validation.constraints.Positive; import ru.yandex.practicum.filmorate.exception.ValidationException; -import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.model.Film; import ru.yandex.practicum.filmorate.service.FilmService; import ru.yandex.practicum.filmorate.service.UserService; -import ru.yandex.practicum.filmorate.storage.InMemoryFilmStorage; -import ru.yandex.practicum.filmorate.storage.InMemoryUserStorage; -import java.time.LocalDate; import java.util.List; +import java.util.Map; @RestController @RequestMapping("/films") @@ -25,11 +23,6 @@ public class FilmController { private final FilmService filmService; private final UserService userService; - public FilmController() { - this.filmService = new FilmService(new InMemoryFilmStorage()); - this.userService = new UserService(new InMemoryUserStorage()); - } - @Autowired public FilmController(FilmService filmService, UserService userService) { this.filmService = filmService; @@ -38,84 +31,57 @@ public FilmController(FilmService filmService, UserService userService) { @GetMapping public List getAllFilms() { - log.info("Fetching all films"); return filmService.getAllFilms(); } @GetMapping("/{id}") public Film getFilm(@Positive @PathVariable Long id) { - log.info("Fetching film with id {}", id); return filmService.getFilmById(id); } @PostMapping public Film addFilm(@Valid @RequestBody Film film) { - log.info("Adding film: {}", film); - validateFilm(film); return filmService.addFilm(film); } @PutMapping public Film updateFilm(@Valid @RequestBody Film film) { - log.info("Updating film: {}", film); - validateFilm(film); - try { - return filmService.updateFilm(film); - } catch (NotFoundException e) { - log.warn("Film not found: {}", film.getId()); - throw new ValidationException("Film not found"); - } + return filmService.updateFilm(film); } @DeleteMapping("/{id}") public void deleteFilm(@Positive @PathVariable Long id) { - log.info("Deleting film with id {}", id); filmService.removeFilm(id); } @PutMapping("/{id}/like/{userId}") - public void addLike(@Positive @PathVariable Long id, - @Positive @PathVariable Long userId) { - log.info("User {} likes film {}", userId, id); + public ResponseEntity> addLike(@PathVariable Long id, + @PathVariable Long userId) { + if (id <= 0 || userId <= 0) { + throw new ValidationException("Film ID and User ID must be positive and non-zero"); + } userService.getUserById(userId); filmService.addLike(id, userId); + return ResponseEntity.ok(Map.of("message", "Like added")); } @DeleteMapping("/{id}/like/{userId}") - public void removeLike(@Positive @PathVariable Long id, - @Positive @PathVariable Long userId) { - log.info("User {} removes like from film {}", userId, id); + public ResponseEntity> removeLike(@PathVariable Long id, + @PathVariable Long userId) { + if (id <= 0 || userId <= 0) { + throw new ValidationException("Film ID and User ID must be positive and non-zero"); + } userService.getUserById(userId); filmService.removeLike(id, userId); + return ResponseEntity.ok(Map.of("message", "Like removed")); } @GetMapping("/popular") - public List getPopularFilms(@RequestParam(defaultValue = "10") @Positive int count) { - log.info("Fetching top {} popular films", count); - return filmService.getPopularFilms(count); - } - - private void validateFilm(Film film) { - if (film == null) { - throw new ValidationException("Film cannot be null"); - } - if (film.getName() == null || film.getName().isBlank()) { - log.warn("Film name is invalid"); - throw new ValidationException("Film name is invalid"); - } - if (film.getDescription() != null && film.getDescription().length() > 200) { - log.warn("Film description is too long"); - throw new ValidationException("Description length exceeds 200 characters"); - } - if (film.getReleaseDate() == null || film.getReleaseDate().isBefore(LocalDate.of(1895, 12, 28))) { - log.warn("Film release date is invalid"); - throw new ValidationException("Release date is invalid"); - } - if (film.getDuration() <= 0) { - log.warn("Film duration is invalid"); - throw new ValidationException("Duration must be positive"); + public List getPopularFilms(@RequestParam(defaultValue = "10") int count) { + if (count <= 0) { + throw new ValidationException("Count must be positive"); } + return filmService.getPopularFilms(count); } } - diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java new file mode 100644 index 0000000..967a1da --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java @@ -0,0 +1,25 @@ +package ru.yandex.practicum.filmorate.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.storage.GenreDbStorage; + +import java.util.List; + +@RestController +@RequestMapping("/genres") +@RequiredArgsConstructor +public class GenreController { + private final GenreDbStorage genreStorage; + + @GetMapping + public List getAll() { + return genreStorage.findAll(); + } + + @GetMapping("/{id}") + public Genre getById(@PathVariable int id) { + return genreStorage.findById(id); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/GlobalExceptionHandler.java b/src/main/java/ru/yandex/practicum/filmorate/controller/GlobalExceptionHandler.java index adc883f..40c4f6c 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/GlobalExceptionHandler.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/GlobalExceptionHandler.java @@ -18,40 +18,30 @@ public class GlobalExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity> handleMethodArgumentNotValid(MethodArgumentNotValidException ex) { - log.error("MethodArgumentNotValidException: {}", ex.getMessage(), ex); - String errorMsg = ex.getBindingResult() - .getFieldErrors() - .stream() + log.error("Validation error: {}", ex.getMessage()); + String errorMsg = ex.getBindingResult().getFieldErrors().stream() .findFirst() .map(FieldError::getDefaultMessage) .orElse("Validation failed"); - return ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(Map.of("error", errorMsg)); + return ResponseEntity.badRequest().body(Map.of("error", errorMsg)); } @ExceptionHandler(ValidationException.class) public ResponseEntity> handleValidation(ValidationException ex) { - log.error("ValidationException: {}", ex.getMessage(), ex); - String msg = ex.getMessage(); - if (msg != null && msg.toLowerCase().contains("not found")) { - return ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(Map.of("error", msg)); - } - return ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(Map.of("error", msg)); + log.error("ValidationException: {}", ex.getMessage()); + return ResponseEntity.badRequest().body(Map.of("error", ex.getMessage())); } @ExceptionHandler(NotFoundException.class) public ResponseEntity> handleNotFound(NotFoundException ex) { - log.error("NotFoundException: {}", ex.getMessage(), ex); - return ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(Map.of("error", ex.getMessage())); + log.error("NotFoundException: {}", ex.getMessage()); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("error", ex.getMessage())); } @ExceptionHandler(Exception.class) public ResponseEntity> handleOther(Exception ex) { - log.error("Unexpected exception: {}", ex.getMessage(), ex); + log.error("Unexpected exception: {}", ex.getMessage()); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(Map.of("error", "An unexpected error occurred")); + .body(Map.of("error", "An unexpected error occurred: " + ex.getMessage())); } -} +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/MpaController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/MpaController.java new file mode 100644 index 0000000..3690a50 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/MpaController.java @@ -0,0 +1,25 @@ +package ru.yandex.practicum.filmorate.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.model.Mpa; +import ru.yandex.practicum.filmorate.storage.MpaDbStorage; + +import java.util.List; + +@RestController +@RequestMapping("/mpa") +@RequiredArgsConstructor +public class MpaController { + private final MpaDbStorage mpaStorage; + + @GetMapping + public List getAll() { + return mpaStorage.findAll(); + } + + @GetMapping("/{id}") + public Mpa getById(@PathVariable int id) { + return mpaStorage.findById(id); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java index 15399cf..0a3fa50 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java @@ -1,119 +1,57 @@ package ru.yandex.practicum.filmorate.controller; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; import jakarta.validation.Valid; -import jakarta.validation.constraints.Positive; -import ru.yandex.practicum.filmorate.exception.ValidationException; -import ru.yandex.practicum.filmorate.exception.NotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; import ru.yandex.practicum.filmorate.model.User; import ru.yandex.practicum.filmorate.service.UserService; -import ru.yandex.practicum.filmorate.storage.InMemoryUserStorage; -import java.time.LocalDate; import java.util.List; @RestController @RequestMapping("/users") -@Slf4j -@Validated +@RequiredArgsConstructor public class UserController { private final UserService userService; - public UserController() { - this.userService = new UserService(new InMemoryUserStorage()); + @PostMapping + public User create(@Valid @RequestBody User user) { + return userService.createUser(user); } - @Autowired - public UserController(UserService userService) { - this.userService = userService; + @PutMapping + public User update(@Valid @RequestBody User user) { + return userService.updateUser(user); } @GetMapping - public List getAllUsers() { - log.info("Fetching all users"); + public List findAll() { return userService.getAllUsers(); } @GetMapping("/{id}") - public User getUser(@Positive @PathVariable Long id) { - log.info("Fetching user with id {}", id); + public User getUser(@PathVariable long id) { return userService.getUserById(id); } - @PostMapping - public User createUser(@Valid @RequestBody User user) { - log.info("Creating user: {}", user); - validateUser(user); - return userService.createUser(user); - } - - @PutMapping - public User updateUser(@Valid @RequestBody User user) { - log.info("Updating user: {}", user); - validateUser(user); - try { - return userService.updateUser(user); - } catch (NotFoundException e) { - log.warn("User not found: {}", user.getId()); - throw new ValidationException("User not found"); - } - } - - @DeleteMapping("/{id}") - public void deleteUser(@Positive @PathVariable Long id) { - log.info("Deleting user with id {}", id); - userService.removeUser(id); - } - @PutMapping("/{id}/friends/{friendId}") - public void addFriend(@Positive @PathVariable Long id, - @Positive @PathVariable Long friendId) { - log.info("User {} adds friend {}", id, friendId); + public void addFriend(@PathVariable long id, @PathVariable long friendId) { userService.addFriend(id, friendId); } @DeleteMapping("/{id}/friends/{friendId}") - public void removeFriend(@Positive @PathVariable Long id, - @Positive @PathVariable Long friendId) { - log.info("User {} removes friend {}", id, friendId); + public void removeFriend(@PathVariable long id, @PathVariable long friendId) { userService.removeFriend(id, friendId); } @GetMapping("/{id}/friends") - public List getFriends(@Positive @PathVariable Long id) { - log.info("Fetching friends of user {}", id); + public List getFriends(@PathVariable long id) { return userService.getFriends(id); } @GetMapping("/{id}/friends/common/{otherId}") - public List getCommonFriends(@Positive @PathVariable Long id, - @Positive @PathVariable Long otherId) { - log.info("Fetching common friends of users {} and {}", id, otherId); + public List getCommonFriends(@PathVariable long id, @PathVariable long otherId) { return userService.getCommonFriends(id, otherId); } - - private void validateUser(User user) { - if (user == null) { - throw new ValidationException("User cannot be null"); - } - if (user.getEmail() == null || user.getEmail().isBlank() || !user.getEmail().contains("@")) { - log.warn("Email is invalid"); - throw new ValidationException("Email is invalid"); - } - if (user.getLogin() == null || user.getLogin().isBlank() || user.getLogin().contains(" ")) { - log.warn("Login is invalid"); - throw new ValidationException("Login is invalid"); - } - if (user.getName() == null || user.getName().isBlank()) { - user.setName(user.getLogin()); - log.info("Name set to login for user {}", user.getLogin()); - } - if (user.getBirthday() == null || user.getBirthday().isAfter(LocalDate.now())) { - log.warn("Birthday is invalid"); - throw new ValidationException("Birthday is invalid"); - } - } } + 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 ae6de4f..cc29105 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java @@ -1,28 +1,45 @@ package ru.yandex.practicum.filmorate.model; -import jakarta.validation.constraints.Min; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PositiveOrZero; import jakarta.validation.constraints.Size; -import lombok.Data; - import java.time.LocalDate; +import java.util.HashSet; +import java.util.Set; - -@Data +@Builder(toBuilder = true) +@AllArgsConstructor +@Getter +@Setter public class Film { - private Long id; + private long id; @NotBlank private String name; @NotBlank - @Size(min = 1, max = 200) + @Size(max = 200) private String description; @NotNull private LocalDate releaseDate; - @Min(1) + @PositiveOrZero private int duration; + + @NotNull + private Mpa mpa; + + private Set genres = new HashSet<>(); + + @JsonIgnore + private Set likes = new HashSet<>(); } + diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Genre.java b/src/main/java/ru/yandex/practicum/filmorate/model/Genre.java new file mode 100644 index 0000000..bd3ebca --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Genre.java @@ -0,0 +1,13 @@ +package ru.yandex.practicum.filmorate.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Genre { + private int id; + private String name; +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Mpa.java b/src/main/java/ru/yandex/practicum/filmorate/model/Mpa.java new file mode 100644 index 0000000..30b90ea --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Mpa.java @@ -0,0 +1,24 @@ +package ru.yandex.practicum.filmorate.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.stereotype.Service; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Positive; + +@Service +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class Mpa { + + @Positive + protected int id; + + @NotBlank + protected String name; +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/User.java b/src/main/java/ru/yandex/practicum/filmorate/model/User.java index 15af210..d0833d2 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/User.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/User.java @@ -2,13 +2,24 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PastOrPresent; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import java.time.LocalDate; +import java.util.HashSet; +import java.util.Set; @Data +@Builder(toBuilder = true) +@NoArgsConstructor +@AllArgsConstructor public class User { - private Long id; + + private long id; @NotBlank @Email @@ -16,6 +27,21 @@ public class User { @NotBlank private String login; + private String name; + + @NotNull + @PastOrPresent private LocalDate birthday; -} \ No newline at end of file + + private Set friends = new HashSet<>(); + + public User(Long id, String email, String login, String name, LocalDate birthday) { + this.id = (id != null ? id : 0L); + this.email = email; + this.login = login; + this.name = (name == null || name.isBlank() ? login : name); + this.birthday = birthday; + this.friends = new HashSet<>(); + } +} 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 ecea46f..d4ecc7e 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java @@ -1,24 +1,36 @@ package ru.yandex.practicum.filmorate.service; +import lombok.RequiredArgsConstructor; 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.Film; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.model.Mpa; import ru.yandex.practicum.filmorate.storage.FilmStorage; +import ru.yandex.practicum.filmorate.storage.GenreDbStorage; +import ru.yandex.practicum.filmorate.storage.MpaDbStorage; +import java.time.LocalDate; import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; @Service +@RequiredArgsConstructor public class FilmService { private final FilmStorage filmStorage; - - public FilmService(FilmStorage filmStorage) { - this.filmStorage = filmStorage; - } + private final MpaDbStorage mpaStorage; + private final GenreDbStorage genreStorage; public Film addFilm(Film film) { + validateMpaAndGenres(film); return filmStorage.addFilm(film); } public Film updateFilm(Film film) { + validateMpaAndGenres(film); return filmStorage.updateFilm(film); } @@ -45,4 +57,24 @@ public void removeLike(Long filmId, Long userId) { public List getPopularFilms(int count) { return filmStorage.getPopularFilms(count); } + + private void validateMpaAndGenres(Film film) { + if (film.getReleaseDate().isBefore(LocalDate.of(1895, 12, 28))) { + throw new ValidationException("Release date must not be earlier than 1895-12-28"); + } + if (film.getMpa() == null || film.getMpa().getId() == 0) { + throw new NotFoundException("MPA rating is required"); + } + + Mpa mpa = mpaStorage.findById(film.getMpa().getId()); + film.setMpa(mpa); + + if (film.getGenres() != null) { + Set genres = film.getGenres().stream() + .filter(Objects::nonNull) + .map(g -> genreStorage.findById(g.getId())) + .collect(Collectors.toSet()); + film.setGenres(genres); + } + } } 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 8046e56..039a93a 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java @@ -1,53 +1,62 @@ package ru.yandex.practicum.filmorate.service; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.model.User; import ru.yandex.practicum.filmorate.storage.UserStorage; import java.util.List; @Service +@RequiredArgsConstructor public class UserService { private final UserStorage userStorage; - public UserService(UserStorage userStorage) { - this.userStorage = userStorage; - } - public User createUser(User user) { return userStorage.addUser(user); } public User updateUser(User user) { + User existing = userStorage.getUserById(user.getId()); + if (existing == null) { + throw new NotFoundException("User not found: " + user.getId()); + } return userStorage.updateUser(user); } - public User getUserById(Long id) { - return userStorage.getUserById(id); - } - public List getAllUsers() { return userStorage.getAllUsers(); } - public void removeUser(Long id) { - userStorage.removeUser(id); + public User getUserById(long id) { + User user = userStorage.getUserById(id); + if (user == null) { + throw new NotFoundException("User not found: " + id); + } + return user; } - public void addFriend(Long userId, Long friendId) { + public void addFriend(long userId, long friendId) { + getUserById(userId); + getUserById(friendId); userStorage.addFriend(userId, friendId); } - public void removeFriend(Long userId, Long friendId) { + public void removeFriend(long userId, long friendId) { + getUserById(userId); + getUserById(friendId); userStorage.removeFriend(userId, friendId); } - public List getFriends(Long userId) { + public List getFriends(long userId) { + getUserById(userId); return userStorage.getFriends(userId); } - public List getCommonFriends(Long userId, Long otherId) { + public List getCommonFriends(long userId, long otherId) { + getUserById(userId); + getUserById(otherId); return userStorage.getCommonFriends(userId, otherId); } } - diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/FilmDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/FilmDbStorage.java new file mode 100644 index 0000000..f65e138 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/FilmDbStorage.java @@ -0,0 +1,157 @@ +package ru.yandex.practicum.filmorate.storage; + +import lombok.RequiredArgsConstructor; +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.Film; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.model.Mpa; + +import java.sql.Date; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +@Repository +@RequiredArgsConstructor +public class FilmDbStorage implements FilmStorage { + private final JdbcTemplate jdbc; + private final MpaDbStorage mpaStorage; + private final GenreDbStorage genreStorage; + + @Override + public Film addFilm(Film film) { + String sql = "INSERT INTO films (name, description, release_date, duration, mpa_id) VALUES (?, ?, ?, ?, ?)"; + KeyHolder keyHolder = new GeneratedKeyHolder(); + jdbc.update(con -> { + PreparedStatement ps = con.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); + ps.setString(1, film.getName()); + ps.setString(2, film.getDescription()); + ps.setDate(3, Date.valueOf(film.getReleaseDate())); + ps.setInt(4, film.getDuration()); + ps.setInt(5, film.getMpa().getId()); + return ps; + }, keyHolder); + long id = keyHolder.getKey().longValue(); + film.setId(id); + updateFilmGenres(film); + return getFilmById(id); + } + + @Override + public Film updateFilm(Film film) { + getFilmById(film.getId()); + String sql = "UPDATE films SET name = ?, description = ?, release_date = ?, duration = ?, mpa_id = ? WHERE id = ?"; + jdbc.update(sql, + film.getName(), + film.getDescription(), + Date.valueOf(film.getReleaseDate()), + film.getDuration(), + film.getMpa().getId(), + film.getId()); + deleteFilmGenres(film.getId()); + updateFilmGenres(film); + return getFilmById(film.getId()); + } + + @Override + public Film getFilmById(Long id) { + String sql = "SELECT * FROM films WHERE id = ?"; + Film film = jdbc.query(sql, this::makeFilm, id) + .stream() + .findFirst() + .orElseThrow(() -> new NotFoundException("Film not found: " + id)); + film.setMpa(mpaStorage.findById(film.getMpa().getId())); + List genres = genreStorage.getGenresForFilm(film.getId()); + film.setGenres(new LinkedHashSet<>(genres)); + film.setLikes(getLikes(film.getId())); + return film; + } + + @Override + public List getAllFilms() { + String sql = "SELECT * FROM films"; + List films = jdbc.query(sql, this::makeFilm); + for (Film f : films) { + f.setMpa(mpaStorage.findById(f.getMpa().getId())); + List genres = genreStorage.getGenresForFilm(f.getId()); + f.setGenres(new LinkedHashSet<>(genres)); + f.setLikes(getLikes(f.getId())); + } + return films; + } + + @Override + public void removeFilm(Long id) { + getFilmById(id); + jdbc.update("DELETE FROM films WHERE id = ?", id); + } + + @Override + public void addLike(Long filmId, Long userId) { + getFilmById(filmId); + String sql = "INSERT INTO likes (film_id, user_id) VALUES (?, ?)"; + jdbc.update(sql, filmId, userId); + } + + @Override + public void removeLike(Long filmId, Long userId) { + String sql = "DELETE FROM likes WHERE film_id = ? AND user_id = ?"; + jdbc.update(sql, filmId, userId); + } + + @Override + public List getPopularFilms(int count) { + String sql = "SELECT f.*, COUNT(l.user_id) AS cnt FROM films f " + + "LEFT JOIN likes l ON f.id = l.film_id " + + "GROUP BY f.id ORDER BY cnt DESC LIMIT ?"; + List films = jdbc.query(sql, this::makeFilm, count); + for (Film f : films) { + f.setMpa(mpaStorage.findById(f.getMpa().getId())); + List genres = genreStorage.getGenresForFilm(f.getId()); + f.setGenres(new LinkedHashSet<>(genres)); + f.setLikes(getLikes(f.getId())); + } + return films; + } + + private Set getLikes(Long filmId) { + String sql = "SELECT user_id FROM likes WHERE film_id = ?"; + return new HashSet<>(jdbc.queryForList(sql, Long.class, filmId)); + } + + private void deleteFilmGenres(Long filmId) { + jdbc.update("DELETE FROM film_genres WHERE film_id = ?", filmId); + } + + private void updateFilmGenres(Film film) { + if (film.getGenres() != null && !film.getGenres().isEmpty()) { + String sql = "INSERT INTO film_genres (film_id, genre_id) VALUES (?, ?)"; + for (Genre genre : film.getGenres()) { + jdbc.update(sql, film.getId(), genre.getId()); + } + } + } + + private Film makeFilm(ResultSet rs, int rowNum) throws SQLException { + return Film.builder() + .id(rs.getLong("id")) + .name(rs.getString("name")) + .description(rs.getString("description")) + .releaseDate(rs.getDate("release_date").toLocalDate()) + .duration(rs.getInt("duration")) + .mpa(new Mpa(rs.getInt("mpa_id"), null)) + .genres(new LinkedHashSet<>()) + .likes(new LinkedHashSet<>()) + .build(); + } +} + diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/GenreDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/GenreDbStorage.java new file mode 100644 index 0000000..c67192a --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/GenreDbStorage.java @@ -0,0 +1,38 @@ +package ru.yandex.practicum.filmorate.storage; + +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Genre; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class GenreDbStorage { + private final JdbcTemplate jdbc; + + public List findAll() { + return jdbc.query("SELECT * FROM genres ORDER BY id", this::mapRowToGenre); + } + + public Genre findById(int id) { + String sql = "SELECT * FROM genres WHERE id = ?"; + return jdbc.query(sql, this::mapRowToGenre, id).stream() + .findFirst() + .orElseThrow(() -> new NotFoundException("Genre not found")); + } + + public List getGenresForFilm(Long filmId) { + String sql = "SELECT g.id, g.name FROM genres g " + + "JOIN film_genres fg ON g.id = fg.genre_id WHERE fg.film_id = ? ORDER BY g.id"; + return jdbc.query(sql, this::mapRowToGenre, filmId); + } + + private Genre mapRowToGenre(ResultSet rs, int rowNum) throws SQLException { + return new Genre(rs.getInt("id"), rs.getString("name")); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryFilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryFilmStorage.java deleted file mode 100644 index 57f74b4..0000000 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryFilmStorage.java +++ /dev/null @@ -1,79 +0,0 @@ -package ru.yandex.practicum.filmorate.storage; - -import org.springframework.stereotype.Component; -import ru.yandex.practicum.filmorate.exception.NotFoundException; -import ru.yandex.practicum.filmorate.model.Film; - -import java.util.*; - -@Component -public class InMemoryFilmStorage implements FilmStorage { - private final Map films = new HashMap<>(); - private final Map> likes = new HashMap<>(); - private long idCounter = 1L; - - @Override - public Film addFilm(Film film) { - film.setId(idCounter++); - films.put(film.getId(), film); - likes.put(film.getId(), new HashSet<>()); - return film; - } - - @Override - public Film updateFilm(Film film) { - if (film.getId() == null || !films.containsKey(film.getId())) { - throw new NotFoundException("Film not found"); - } - films.put(film.getId(), film); - return film; - } - - @Override - public Film getFilmById(Long id) { - Film film = films.get(id); - if (film == null) { - throw new NotFoundException("Film not found"); - } - return film; - } - - @Override - public List getAllFilms() { - return new ArrayList<>(films.values()); - } - - @Override - public void removeFilm(Long id) { - if (!films.containsKey(id)) { - throw new NotFoundException("Film not found"); - } - films.remove(id); - likes.remove(id); - } - - @Override - public void addLike(Long filmId, Long userId) { - if (!films.containsKey(filmId)) { - throw new NotFoundException("Film not found"); - } - likes.get(filmId).add(userId); - } - - @Override - public void removeLike(Long filmId, Long userId) { - if (!films.containsKey(filmId)) { - throw new NotFoundException("Film not found"); - } - likes.get(filmId).remove(userId); - } - - @Override - public List getPopularFilms(int count) { - return films.values().stream() - .sorted((f1, f2) -> Integer.compare( - likes.get(f2.getId()).size(), likes.get(f1.getId()).size())) - .limit(count) - .toList(); - } -} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryUserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryUserStorage.java deleted file mode 100644 index 620aa36..0000000 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/InMemoryUserStorage.java +++ /dev/null @@ -1,89 +0,0 @@ -package ru.yandex.practicum.filmorate.storage; - -import org.springframework.stereotype.Component; -import ru.yandex.practicum.filmorate.model.User; -import ru.yandex.practicum.filmorate.exception.NotFoundException; - -import java.util.*; - -@Component -public class InMemoryUserStorage implements UserStorage { - private final Map users = new HashMap<>(); - private final Map> friends = new HashMap<>(); - private long idCounter = 1L; - - @Override - public User addUser(User user) { - user.setId(idCounter++); - users.put(user.getId(), user); - friends.put(user.getId(), new HashSet<>()); - return user; - } - - @Override - public User updateUser(User user) { - if (user.getId() == null || !users.containsKey(user.getId())) { - throw new NotFoundException("User not found"); - } - users.put(user.getId(), user); - return user; - } - - @Override - public User getUserById(Long id) { - User user = users.get(id); - if (user == null) { - throw new NotFoundException("User not found"); - } - return user; - } - - @Override - public List getAllUsers() { - return new ArrayList<>(users.values()); - } - - @Override - public void removeUser(Long id) { - if (!users.containsKey(id)) { - throw new NotFoundException("User not found"); - } - users.remove(id); - friends.remove(id); - } - - @Override - public void addFriend(Long userId, Long friendId) { - getUserById(userId); - getUserById(friendId); - friends.get(userId).add(friendId); - friends.get(friendId).add(userId); - } - - @Override - public void removeFriend(Long userId, Long friendId) { - getUserById(userId); - getUserById(friendId); - friends.get(userId).remove(friendId); - friends.get(friendId).remove(userId); - } - - @Override - public List getFriends(Long userId) { - getUserById(userId); - return friends.get(userId).stream() - .map(users::get) - .toList(); - } - - @Override - public List getCommonFriends(Long userId, Long otherId) { - getUserById(userId); - getUserById(otherId); - Set common = new HashSet<>(friends.get(userId)); - common.retainAll(friends.get(otherId)); - return common.stream() - .map(users::get) - .toList(); - } -} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/MpaDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/MpaDbStorage.java new file mode 100644 index 0000000..8f04cc5 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/MpaDbStorage.java @@ -0,0 +1,32 @@ +package ru.yandex.practicum.filmorate.storage; + +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.model.Mpa; +import ru.yandex.practicum.filmorate.exception.NotFoundException; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class MpaDbStorage { + private final JdbcTemplate jdbc; + + public List findAll() { + return jdbc.query("SELECT * FROM mpa ORDER BY id", this::mapRowToMpa); + } + + public Mpa findById(int id) { + String sql = "SELECT * FROM mpa WHERE id = ?"; + return jdbc.query(sql, this::mapRowToMpa, id).stream() + .findFirst() + .orElseThrow(() -> new NotFoundException("MPA rating not found")); + } + + private Mpa mapRowToMpa(ResultSet rs, int rowNum) throws SQLException { + return new Mpa(rs.getInt("id"), rs.getString("name")); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/UserDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/UserDbStorage.java new file mode 100644 index 0000000..d6b1b2f --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/UserDbStorage.java @@ -0,0 +1,115 @@ +package ru.yandex.practicum.filmorate.storage; + +import lombok.RequiredArgsConstructor; +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.User; + +import java.sql.Date; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class UserDbStorage implements UserStorage { + private final JdbcTemplate jdbc; + + @Override + public User addUser(User user) { + if (user.getName() == null || user.getName().isBlank()) { + user.setName(user.getLogin()); + } + String sql = "INSERT INTO users (email, login, name, birthday) VALUES (?, ?, ?, ?)"; + KeyHolder keyHolder = new GeneratedKeyHolder(); + jdbc.update(con -> { + PreparedStatement ps = con.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); + ps.setString(1, user.getEmail()); + ps.setString(2, user.getLogin()); + ps.setString(3, user.getName()); + ps.setDate(4, Date.valueOf(user.getBirthday())); + return ps; + }, keyHolder); + long id = keyHolder.getKey().longValue(); + user.setId(id); + return user; + } + + @Override + public User updateUser(User user) { + getUserById(user.getId()); + String sql = "UPDATE users SET email = ?, login = ?, name = ?, birthday = ? WHERE id = ?"; + jdbc.update(sql, + user.getEmail(), + user.getLogin(), + user.getName(), + Date.valueOf(user.getBirthday()), + user.getId()); + return user; + } + + @Override + public User getUserById(Long id) { + String sql = "SELECT * FROM users WHERE id = ?"; + return jdbc.query(sql, this::makeUser, id) + .stream() + .findFirst() + .orElseThrow(() -> new NotFoundException("User not found: " + id)); + } + + @Override + public List getAllUsers() { + String sql = "SELECT * FROM users"; + return jdbc.query(sql, this::makeUser); + } + + @Override + public void removeUser(Long id) { + getUserById(id); + jdbc.update("DELETE FROM users WHERE id = ?", id); + } + + @Override + public void addFriend(Long userId, Long friendId) { + getUserById(userId); + getUserById(friendId); + String sql = "INSERT INTO friends (user_id, friend_id) VALUES (?, ?)"; + jdbc.update(sql, userId, friendId); + } + + @Override + public void removeFriend(Long userId, Long friendId) { + String sql = "DELETE FROM friends WHERE user_id = ? AND friend_id = ?"; + jdbc.update(sql, userId, friendId); + } + + @Override + public List getFriends(Long userId) { + String sql = "SELECT u.* FROM users u JOIN friends f ON u.id = f.friend_id WHERE f.user_id = ?"; + return jdbc.query(sql, this::makeUser, userId); + } + + @Override + public List getCommonFriends(Long userId, Long otherId) { + String sql = "SELECT u.* FROM users u " + + "JOIN friends f1 ON u.id = f1.friend_id " + + "JOIN friends f2 ON u.id = f2.friend_id " + + "WHERE f1.user_id = ? AND f2.user_id = ?"; + return jdbc.query(sql, this::makeUser, userId, otherId); + } + + private User makeUser(ResultSet rs, int rowNum) throws SQLException { + return User.builder() + .id(rs.getLong("id")) + .email(rs.getString("email")) + .login(rs.getString("login")) + .name(rs.getString("name")) + .birthday(rs.getDate("birthday").toLocalDate()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 1d1b4bf..f146f9c 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,7 @@ -logging.level.org.zalando.logbook: TRACE +spring.sql.init.mode=always +spring.sql.init.schema-locations=classpath:schema.sql +spring.sql.init.data-locations=classpath:data.sql +spring.datasource.url=jdbc:h2:file:./db/filmorate +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password=password diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql new file mode 100644 index 0000000..f1392b9 --- /dev/null +++ b/src/main/resources/data.sql @@ -0,0 +1,12 @@ +MERGE INTO mpa (id, name) KEY(id) VALUES (1,'G'); +MERGE INTO mpa (id, name) KEY(id) VALUES (2,'PG'); +MERGE INTO mpa (id, name) KEY(id) VALUES (3,'PG-13'); +MERGE INTO mpa (id, name) KEY(id) VALUES (4,'R'); +MERGE INTO mpa (id, name) KEY(id) VALUES (5,'NC-17'); + +MERGE INTO genres (id, name) KEY(id) VALUES (1,'Комедия'); +MERGE INTO genres (id, name) KEY(id) VALUES (2,'Драма'); +MERGE INTO genres (id, name) KEY(id) VALUES (3,'Мультфильм'); +MERGE INTO genres (id, name) KEY(id) VALUES (4,'Триллер'); +MERGE INTO genres (id, name) KEY(id) VALUES (5,'Документальный'); +MERGE INTO genres (id, name) KEY(id) VALUES (6,'Боевик'); diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000..57a70e6 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,51 @@ +CREATE TABLE IF NOT EXISTS users ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + email VARCHAR(255) NOT NULL, + login VARCHAR(255) NOT NULL, + name VARCHAR(255), + birthday DATE NOT NULL +); + +CREATE TABLE IF NOT EXISTS mpa ( + id INT PRIMARY KEY, + name VARCHAR(10) NOT NULL +); + +CREATE TABLE IF NOT EXISTS genres ( + id INT PRIMARY KEY, + name VARCHAR(50) NOT NULL +); + +CREATE TABLE IF NOT EXISTS friends ( + user_id BIGINT NOT NULL, + friend_id BIGINT NOT NULL, + PRIMARY KEY (user_id, friend_id), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (friend_id) REFERENCES users(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS films ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description VARCHAR(200), + release_date DATE NOT NULL, + duration INT NOT NULL, + mpa_id INT NOT NULL, + FOREIGN KEY (mpa_id) REFERENCES mpa(id) +); + +CREATE TABLE IF NOT EXISTS film_genres ( + film_id BIGINT NOT NULL, + genre_id INT NOT NULL, + PRIMARY KEY (film_id, genre_id), + FOREIGN KEY (film_id) REFERENCES films(id) ON DELETE CASCADE, + FOREIGN KEY (genre_id) REFERENCES genres(id) +); + +CREATE TABLE IF NOT EXISTS likes ( + film_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + PRIMARY KEY (film_id, user_id), + FOREIGN KEY (film_id) REFERENCES films(id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users(id) +); diff --git a/src/test/java/ru/yandex/practicum/filmorate/BoundaryTests.java b/src/test/java/ru/yandex/practicum/filmorate/BoundaryTests.java deleted file mode 100644 index 48e3c78..0000000 --- a/src/test/java/ru/yandex/practicum/filmorate/BoundaryTests.java +++ /dev/null @@ -1,56 +0,0 @@ -package ru.yandex.practicum.filmorate; - -import ru.yandex.practicum.filmorate.controller.FilmController; -import ru.yandex.practicum.filmorate.controller.UserController; -import ru.yandex.practicum.filmorate.model.Film; -import ru.yandex.practicum.filmorate.model.User; -import org.junit.jupiter.api.Test; -import java.time.LocalDate; -import static org.junit.jupiter.api.Assertions.*; - -class BoundaryTests { - private FilmController filmController = new FilmController(); - private UserController userController = new UserController(); - - @Test - void filmValidation_BoundaryValues() { - // Проверка минимально допустимой даты релиза - Film film1 = new Film(); - film1.setName("Boundary Film"); - film1.setDescription("Desc"); - film1.setReleaseDate(LocalDate.of(1895, 12, 28)); - film1.setDuration(1); - assertDoesNotThrow(() -> filmController.addFilm(film1)); - - // Проверка максимальной длины описания - Film film2 = new Film(); - film2.setName("Boundary Film"); - film2.setDescription("a".repeat(200)); - film2.setReleaseDate(LocalDate.of(2000, 1, 1)); - film2.setDuration(1); - assertDoesNotThrow(() -> filmController.addFilm(film2)); - } - - @Test - void userValidation_BoundaryValues() { - // Проверка сегодняшней даты рождения - User user1 = new User(); - user1.setEmail("test@example.com"); - user1.setLogin("testlogin"); - user1.setBirthday(LocalDate.now()); - assertDoesNotThrow(() -> userController.createUser(user1)); - - // Проверка минимально допустимого логина - User user2 = new User(); - user2.setEmail("test@example.com"); - user2.setLogin("a"); // Минимальный логин - user2.setBirthday(LocalDate.of(1990, 1, 1)); - assertDoesNotThrow(() -> userController.createUser(user2)); - } - - @Test - void nullRequest_ThrowsException() { - assertThrows(Exception.class, () -> filmController.addFilm(null)); - assertThrows(Exception.class, () -> userController.createUser(null)); - } -} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/FilmControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/FilmControllerTest.java deleted file mode 100644 index 0c95ae8..0000000 --- a/src/test/java/ru/yandex/practicum/filmorate/FilmControllerTest.java +++ /dev/null @@ -1,109 +0,0 @@ -package ru.yandex.practicum.filmorate; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import ru.yandex.practicum.filmorate.controller.FilmController; -import ru.yandex.practicum.filmorate.exception.ValidationException; -import ru.yandex.practicum.filmorate.model.Film; - -import java.time.LocalDate; - -import static org.junit.jupiter.api.Assertions.*; - -class FilmControllerTest { - private FilmController filmController; - - @BeforeEach - void setUp() { - filmController = new FilmController(); - } - - @Test - void addValidFilm() { - Film film = new Film(); - film.setName("Valid Film"); - film.setDescription("Valid Description"); - film.setReleaseDate(LocalDate.of(2000, 1, 1)); - film.setDuration(120); - - Film addedFilm = filmController.addFilm(film); - assertNotNull(addedFilm.getId()); - assertEquals(1, filmController.getAllFilms().size()); - } - - @Test - void addFilmWithEmptyName() { - Film film = new Film(); - film.setName(""); - film.setDescription("Description"); - film.setReleaseDate(LocalDate.of(2000, 1, 1)); - film.setDuration(120); - - assertThrows(ValidationException.class, () -> filmController.addFilm(film)); - } - - @Test - void addFilmWithLongDescription() { - Film film = new Film(); - film.setName("Name"); - film.setDescription("A".repeat(201)); - film.setReleaseDate(LocalDate.of(2000, 1, 1)); - film.setDuration(120); - - assertThrows(ValidationException.class, () -> filmController.addFilm(film)); - } - - @Test - void addFilmWithEarlyReleaseDate() { - Film film = new Film(); - film.setName("Name"); - film.setDescription("Description"); - film.setReleaseDate(LocalDate.of(1895, 12, 27)); - film.setDuration(120); - - assertThrows(ValidationException.class, () -> filmController.addFilm(film)); - } - - @Test - void addFilmWithNegativeDuration() { - Film film = new Film(); - film.setName("Name"); - film.setDescription("Description"); - film.setReleaseDate(LocalDate.of(2000, 1, 1)); - film.setDuration(-120); - - assertThrows(ValidationException.class, () -> filmController.addFilm(film)); - } - - @Test - void updateFilmWithInvalidId() { - Film film = new Film(); - film.setId(999L); - film.setName("Name"); - film.setDescription("Description"); - film.setReleaseDate(LocalDate.of(2000, 1, 1)); - film.setDuration(120); - - assertThrows(ValidationException.class, () -> filmController.updateFilm(film)); - } - - @Test - void getAllFilms() { - Film film1 = createTestFilm("Film 1"); - Film film2 = createTestFilm("Film 2"); - - filmController.addFilm(film1); - filmController.addFilm(film2); - - assertEquals(2, filmController.getAllFilms().size()); - } - - private Film createTestFilm(String name) { - Film film = new Film(); - film.setName(name); - film.setDescription("Description"); - film.setReleaseDate(LocalDate.of(2000, 1, 1)); - film.setDuration(120); - return film; - } -} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/UserControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/UserControllerTest.java deleted file mode 100644 index a1a9f16..0000000 --- a/src/test/java/ru/yandex/practicum/filmorate/UserControllerTest.java +++ /dev/null @@ -1,113 +0,0 @@ -package ru.yandex.practicum.filmorate; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import ru.yandex.practicum.filmorate.controller.UserController; -import ru.yandex.practicum.filmorate.exception.ValidationException; -import ru.yandex.practicum.filmorate.model.User; - -import java.time.LocalDate; - -import static org.junit.jupiter.api.Assertions.*; - -class UserControllerTest { - private UserController userController; - - @BeforeEach - void setUp() { - userController = new UserController(); - } - - @Test - void createValidUser() { - User user = new User(); - user.setEmail("test@mail.com"); - user.setLogin("testlogin"); - user.setBirthday(LocalDate.of(1990, 1, 1)); - - User createdUser = userController.createUser(user); - assertNotNull(createdUser.getId()); - assertEquals("testlogin", createdUser.getName()); - assertEquals(1, userController.getAllUsers().size()); - } - - @Test - void createUserWithEmptyEmail() { - User user = new User(); - user.setEmail(""); - user.setLogin("login"); - user.setBirthday(LocalDate.of(1990, 1, 1)); - - assertThrows(ValidationException.class, () -> userController.createUser(user)); - } - - @Test - void createUserWithInvalidEmail() { - User user = new User(); - user.setEmail("invalid-email"); - user.setLogin("login"); - user.setBirthday(LocalDate.of(1990, 1, 1)); - - assertThrows(ValidationException.class, () -> userController.createUser(user)); - } - - @Test - void createUserWithEmptyLogin() { - User user = new User(); - user.setEmail("test@mail.com"); - user.setLogin(""); - user.setBirthday(LocalDate.of(1990, 1, 1)); - - assertThrows(ValidationException.class, () -> userController.createUser(user)); - } - - @Test - void createUserWithLoginContainingSpaces() { - User user = new User(); - user.setEmail("test@mail.com"); - user.setLogin("login with spaces"); - user.setBirthday(LocalDate.of(1990, 1, 1)); - - assertThrows(ValidationException.class, () -> userController.createUser(user)); - } - - @Test - void createUserWithFutureBirthday() { - User user = new User(); - user.setEmail("test@mail.com"); - user.setLogin("login"); - user.setBirthday(LocalDate.now().plusDays(1)); - - assertThrows(ValidationException.class, () -> userController.createUser(user)); - } - - @Test - void updateUserWithInvalidId() { - User user = new User(); - user.setId(999L); - user.setEmail("test@mail.com"); - user.setLogin("login"); - user.setBirthday(LocalDate.of(1990, 1, 1)); - - assertThrows(ValidationException.class, () -> userController.updateUser(user)); - } - - @Test - void getAllUsers() { - User user1 = createTestUser("user1@mail.com", "user1"); - User user2 = createTestUser("user2@mail.com", "user2"); - - userController.createUser(user1); - userController.createUser(user2); - - assertEquals(2, userController.getAllUsers().size()); - } - - private User createTestUser(String email, String login) { - User user = new User(); - user.setEmail(email); - user.setLogin(login); - user.setBirthday(LocalDate.of(1990, 1, 1)); - return user; - } -} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/UserDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/UserDbStorageTest.java new file mode 100644 index 0000000..22d4092 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/UserDbStorageTest.java @@ -0,0 +1,40 @@ +package ru.yandex.practicum.filmorate; + +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.User; +import ru.yandex.practicum.filmorate.storage.UserDbStorage; + +import java.time.LocalDate; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +@JdbcTest +@AutoConfigureTestDatabase +@Import(UserDbStorage.class) +class UserDbStorageTest { + + @Autowired + private UserDbStorage userStorage; + + @Test + void testAddAndFindUser() { + User user = new User(null, "test@mail.com", "testlogin", "Test Name", LocalDate.of(1990, 1, 1)); + User created = userStorage.addUser(user); + + Optional fromDb = Optional.of(userStorage.getUserById(created.getId())); + + assertThat(fromDb) + .isPresent() + .hasValueSatisfying(u -> + assertThat(u) + .hasFieldOrPropertyWithValue("id", created.getId()) + .hasFieldOrPropertyWithValue("email", "test@mail.com") + .hasFieldOrPropertyWithValue("login", "testlogin") + ); + } +} From ac33cd8ff7d2c8fb47175289975e4225bd8f22b6 Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 29 Apr 2025 20:15:55 +0300 Subject: [PATCH 2/4] =?UTF-8?q?=D0=98=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B8=20=D0=B2=20?= =?UTF-8?q?FilmDbStorage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../filmorate/service/FilmService.java | 68 ++++++---- .../filmorate/storage/FilmDbStorage.java | 122 +++++++----------- .../filmorate/storage/GenreDbStorage.java | 36 +++++- 3 files changed, 121 insertions(+), 105 deletions(-) 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 d4ecc7e..ac6a62d 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java @@ -2,16 +2,17 @@ import lombok.RequiredArgsConstructor; 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.Film; import ru.yandex.practicum.filmorate.model.Genre; import ru.yandex.practicum.filmorate.model.Mpa; -import ru.yandex.practicum.filmorate.storage.FilmStorage; +import ru.yandex.practicum.filmorate.storage.FilmDbStorage; import ru.yandex.practicum.filmorate.storage.GenreDbStorage; import ru.yandex.practicum.filmorate.storage.MpaDbStorage; import java.time.LocalDate; +import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; import java.util.Set; @@ -20,26 +21,55 @@ @Service @RequiredArgsConstructor public class FilmService { - private final FilmStorage filmStorage; + private final FilmDbStorage filmStorage; private final MpaDbStorage mpaStorage; private final GenreDbStorage genreStorage; public Film addFilm(Film film) { - validateMpaAndGenres(film); - return filmStorage.addFilm(film); + validateReleaseDate(film); + Mpa mpa = mpaStorage.findById(film.getMpa().getId()); + film.setMpa(mpa); + Set genres = Collections.emptySet(); + if (film.getGenres() != null) { + genres = film.getGenres().stream() + .filter(Objects::nonNull) + .map(g -> genreStorage.findById(g.getId())) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + film.setGenres(genres); + Film created = filmStorage.addFilm(film); + return getFilmById(created.getId()); } public Film updateFilm(Film film) { - validateMpaAndGenres(film); - return filmStorage.updateFilm(film); + filmStorage.getFilmById(film.getId()); + validateReleaseDate(film); + Mpa mpa = mpaStorage.findById(film.getMpa().getId()); + film.setMpa(mpa); + Set genres = Collections.emptySet(); + if (film.getGenres() != null) { + genres = film.getGenres().stream() + .filter(Objects::nonNull) + .map(g -> genreStorage.findById(g.getId())) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + film.setGenres(genres); + filmStorage.updateFilm(film); + return getFilmById(film.getId()); } public Film getFilmById(Long id) { - return filmStorage.getFilmById(id); + Film film = filmStorage.getFilmById(id); + film.setMpa(mpaStorage.findById(film.getMpa().getId())); + film.setGenres(new LinkedHashSet<>(genreStorage.getGenresForFilm(id))); + film.setLikes(filmStorage.getLikes(id)); + return film; } public List getAllFilms() { - return filmStorage.getAllFilms(); + return filmStorage.getAllFilms().stream() + .map(f -> getFilmById(f.getId())) + .collect(Collectors.toList()); } public void removeFilm(Long id) { @@ -55,26 +85,14 @@ public void removeLike(Long filmId, Long userId) { } public List getPopularFilms(int count) { - return filmStorage.getPopularFilms(count); + return filmStorage.getPopularFilms(count).stream() + .map(f -> getFilmById(f.getId())) + .collect(Collectors.toList()); } - private void validateMpaAndGenres(Film film) { + private void validateReleaseDate(Film film) { if (film.getReleaseDate().isBefore(LocalDate.of(1895, 12, 28))) { throw new ValidationException("Release date must not be earlier than 1895-12-28"); } - if (film.getMpa() == null || film.getMpa().getId() == 0) { - throw new NotFoundException("MPA rating is required"); - } - - Mpa mpa = mpaStorage.findById(film.getMpa().getId()); - film.setMpa(mpa); - - if (film.getGenres() != null) { - Set genres = film.getGenres().stream() - .filter(Objects::nonNull) - .map(g -> genreStorage.findById(g.getId())) - .collect(Collectors.toSet()); - film.setGenres(genres); - } } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/FilmDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/FilmDbStorage.java index f65e138..4b34f65 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/FilmDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/FilmDbStorage.java @@ -5,27 +5,21 @@ 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.Film; -import ru.yandex.practicum.filmorate.model.Genre; import ru.yandex.practicum.filmorate.model.Mpa; import java.sql.Date; import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; import java.sql.Statement; import java.util.HashSet; -import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; @Repository @RequiredArgsConstructor public class FilmDbStorage implements FilmStorage { private final JdbcTemplate jdbc; - private final MpaDbStorage mpaStorage; - private final GenreDbStorage genreStorage; @Override public Film addFilm(Film film) { @@ -42,13 +36,18 @@ public Film addFilm(Film film) { }, keyHolder); long id = keyHolder.getKey().longValue(); film.setId(id); - updateFilmGenres(film); - return getFilmById(id); + if (!film.getGenres().isEmpty()) { + String sqlG = "INSERT INTO film_genres (film_id, genre_id) VALUES (?, ?)"; + List batch = film.getGenres().stream() + .map(g -> new Object[]{id, g.getId()}) + .collect(Collectors.toList()); + jdbc.batchUpdate(sqlG, batch); + } + return film; } @Override public Film updateFilm(Film film) { - getFilmById(film.getId()); String sql = "UPDATE films SET name = ?, description = ?, release_date = ?, duration = ?, mpa_id = ? WHERE id = ?"; jdbc.update(sql, film.getName(), @@ -57,101 +56,76 @@ public Film updateFilm(Film film) { film.getDuration(), film.getMpa().getId(), film.getId()); - deleteFilmGenres(film.getId()); - updateFilmGenres(film); - return getFilmById(film.getId()); + jdbc.update("DELETE FROM film_genres WHERE film_id = ?", film.getId()); + if (!film.getGenres().isEmpty()) { + String sqlG = "INSERT INTO film_genres (film_id, genre_id) VALUES (?, ?)"; + List batch = film.getGenres().stream() + .map(g -> new Object[]{film.getId(), g.getId()}) + .collect(Collectors.toList()); + jdbc.batchUpdate(sqlG, batch); + } + return film; } @Override public Film getFilmById(Long id) { - String sql = "SELECT * FROM films WHERE id = ?"; - Film film = jdbc.query(sql, this::makeFilm, id) - .stream() - .findFirst() - .orElseThrow(() -> new NotFoundException("Film not found: " + id)); - film.setMpa(mpaStorage.findById(film.getMpa().getId())); - List genres = genreStorage.getGenresForFilm(film.getId()); - film.setGenres(new LinkedHashSet<>(genres)); - film.setLikes(getLikes(film.getId())); - return film; + String sql = "SELECT id, name, description, release_date, duration, mpa_id FROM films WHERE id = ?"; + return jdbc.queryForObject(sql, (rs, rn) -> Film.builder() + .id(rs.getLong("id")) + .name(rs.getString("name")) + .description(rs.getString("description")) + .releaseDate(rs.getDate("release_date").toLocalDate()) + .duration(rs.getInt("duration")) + .mpa(new Mpa(rs.getInt("mpa_id"), null)) + .build(), id); } @Override public List getAllFilms() { - String sql = "SELECT * FROM films"; - List films = jdbc.query(sql, this::makeFilm); - for (Film f : films) { - f.setMpa(mpaStorage.findById(f.getMpa().getId())); - List genres = genreStorage.getGenresForFilm(f.getId()); - f.setGenres(new LinkedHashSet<>(genres)); - f.setLikes(getLikes(f.getId())); - } - return films; + String sql = "SELECT id, name, description, release_date, duration, mpa_id FROM films"; + return jdbc.query(sql, (rs, rn) -> Film.builder() + .id(rs.getLong("id")) + .name(rs.getString("name")) + .description(rs.getString("description")) + .releaseDate(rs.getDate("release_date").toLocalDate()) + .duration(rs.getInt("duration")) + .mpa(new Mpa(rs.getInt("mpa_id"), null)) + .build()); } @Override public void removeFilm(Long id) { - getFilmById(id); jdbc.update("DELETE FROM films WHERE id = ?", id); } @Override public void addLike(Long filmId, Long userId) { - getFilmById(filmId); - String sql = "INSERT INTO likes (film_id, user_id) VALUES (?, ?)"; - jdbc.update(sql, filmId, userId); + jdbc.update("INSERT INTO likes (film_id, user_id) VALUES (?, ?)", filmId, userId); } @Override public void removeLike(Long filmId, Long userId) { - String sql = "DELETE FROM likes WHERE film_id = ? AND user_id = ?"; - jdbc.update(sql, filmId, userId); + jdbc.update("DELETE FROM likes WHERE film_id = ? AND user_id = ?", filmId, userId); } @Override public List getPopularFilms(int count) { - String sql = "SELECT f.*, COUNT(l.user_id) AS cnt FROM films f " + - "LEFT JOIN likes l ON f.id = l.film_id " + - "GROUP BY f.id ORDER BY cnt DESC LIMIT ?"; - List films = jdbc.query(sql, this::makeFilm, count); - for (Film f : films) { - f.setMpa(mpaStorage.findById(f.getMpa().getId())); - List genres = genreStorage.getGenresForFilm(f.getId()); - f.setGenres(new LinkedHashSet<>(genres)); - f.setLikes(getLikes(f.getId())); - } - return films; - } - - private Set getLikes(Long filmId) { - String sql = "SELECT user_id FROM likes WHERE film_id = ?"; - return new HashSet<>(jdbc.queryForList(sql, Long.class, filmId)); - } - - private void deleteFilmGenres(Long filmId) { - jdbc.update("DELETE FROM film_genres WHERE film_id = ?", filmId); - } - - private void updateFilmGenres(Film film) { - if (film.getGenres() != null && !film.getGenres().isEmpty()) { - String sql = "INSERT INTO film_genres (film_id, genre_id) VALUES (?, ?)"; - for (Genre genre : film.getGenres()) { - jdbc.update(sql, film.getId(), genre.getId()); - } - } - } - - private Film makeFilm(ResultSet rs, int rowNum) throws SQLException { - return Film.builder() + String sql = "SELECT f.id, f.name, f.description, f.release_date, f.duration, f.mpa_id " + + "FROM films f LEFT JOIN likes l ON f.id = l.film_id " + + "GROUP BY f.id ORDER BY COUNT(l.user_id) DESC LIMIT ?"; + return jdbc.query(sql, (rs, rn) -> Film.builder() .id(rs.getLong("id")) .name(rs.getString("name")) .description(rs.getString("description")) .releaseDate(rs.getDate("release_date").toLocalDate()) .duration(rs.getInt("duration")) .mpa(new Mpa(rs.getInt("mpa_id"), null)) - .genres(new LinkedHashSet<>()) - .likes(new LinkedHashSet<>()) - .build(); + .build(), count); + } + + public Set getLikes(Long filmId) { + return new HashSet<>(jdbc.queryForList( + "SELECT user_id FROM likes WHERE film_id = ?", Long.class, filmId)); } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/GenreDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/GenreDbStorage.java index c67192a..8670065 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/GenreDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/GenreDbStorage.java @@ -8,7 +8,8 @@ import java.sql.ResultSet; import java.sql.SQLException; -import java.util.List; +import java.util.*; + @Repository @RequiredArgsConstructor @@ -16,23 +17,46 @@ public class GenreDbStorage { private final JdbcTemplate jdbc; public List findAll() { - return jdbc.query("SELECT * FROM genres ORDER BY id", this::mapRowToGenre); + String sql = "SELECT * FROM genres ORDER BY id"; + return jdbc.query(sql, this::mapRowToGenre); } public Genre findById(int id) { String sql = "SELECT * FROM genres WHERE id = ?"; - return jdbc.query(sql, this::mapRowToGenre, id).stream() + return jdbc.query(sql, this::mapRowToGenre, id) + .stream() .findFirst() - .orElseThrow(() -> new NotFoundException("Genre not found")); + .orElseThrow(() -> new NotFoundException("Genre not found: " + id)); } public List getGenresForFilm(Long filmId) { String sql = "SELECT g.id, g.name FROM genres g " + - "JOIN film_genres fg ON g.id = fg.genre_id WHERE fg.film_id = ? ORDER BY g.id"; + "JOIN film_genres fg ON g.id = fg.genre_id " + + "WHERE fg.film_id = ? ORDER BY g.id"; return jdbc.query(sql, this::mapRowToGenre, filmId); } + public Map> getGenresForFilms(List filmIds) { + if (filmIds.isEmpty()) return Collections.emptyMap(); + String placeholders = String.join(",", Collections.nCopies(filmIds.size(), "?")); + String sql = "SELECT fg.film_id, g.id, g.name FROM film_genres fg " + + "JOIN genres g ON fg.genre_id = g.id " + + "WHERE fg.film_id IN (" + placeholders + ") " + + "ORDER BY fg.film_id, g.id"; + Map> map = new HashMap<>(); + jdbc.query(con -> { + var ps = con.prepareStatement(sql); + for (int i = 0; i < filmIds.size(); i++) ps.setLong(i+1, filmIds.get(i)); + return ps; + }, rs -> { + long fid = rs.getLong("film_id"); + Genre genre = new Genre(rs.getInt("id"), rs.getString("name")); + map.computeIfAbsent(fid, k -> new ArrayList<>()).add(genre); + }); + return map; + } + private Genre mapRowToGenre(ResultSet rs, int rowNum) throws SQLException { return new Genre(rs.getInt("id"), rs.getString("name")); } -} +} \ No newline at end of file From b1ef1d04138d5b1a1d1d2c31df27d989288ddf3a Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 29 Apr 2025 20:19:50 +0300 Subject: [PATCH 3/4] =?UTF-8?q?=D0=98=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B8=20=D0=B2=20?= =?UTF-8?q?FilmDbStorage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yandex/practicum/filmorate/storage/GenreDbStorage.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/GenreDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/GenreDbStorage.java index 8670065..9444bd4 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/GenreDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/GenreDbStorage.java @@ -10,7 +10,6 @@ import java.sql.SQLException; import java.util.*; - @Repository @RequiredArgsConstructor public class GenreDbStorage { @@ -39,8 +38,9 @@ public List getGenresForFilm(Long filmId) { public Map> getGenresForFilms(List filmIds) { if (filmIds.isEmpty()) return Collections.emptyMap(); String placeholders = String.join(",", Collections.nCopies(filmIds.size(), "?")); - String sql = "SELECT fg.film_id, g.id, g.name FROM film_genres fg " + - "JOIN genres g ON fg.genre_id = g.id " + + String sql = "SELECT fg.film_id, g.id, g.name " + + "FROM genres g " + + "JOIN film_genres fg ON g.id = fg.genre_id " + "WHERE fg.film_id IN (" + placeholders + ") " + "ORDER BY fg.film_id, g.id"; Map> map = new HashMap<>(); From 2e5a3d71a119c83e97584307d54c5a80d757ec35 Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 29 Apr 2025 20:22:44 +0300 Subject: [PATCH 4/4] =?UTF-8?q?=D0=98=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B8=20=D0=B2=20?= =?UTF-8?q?FilmDbStorage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ru/yandex/practicum/filmorate/storage/GenreDbStorage.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/GenreDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/GenreDbStorage.java index 9444bd4..f5ad0a8 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/GenreDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/GenreDbStorage.java @@ -46,7 +46,7 @@ public Map> getGenresForFilms(List filmIds) { Map> map = new HashMap<>(); jdbc.query(con -> { var ps = con.prepareStatement(sql); - for (int i = 0; i < filmIds.size(); i++) ps.setLong(i+1, filmIds.get(i)); + for (int i = 0; i < filmIds.size(); i++) ps.setLong(i + 1, filmIds.get(i)); return ps; }, rs -> { long fid = rs.getLong("film_id");