diff --git a/img/schema-database.png b/img/schema-database.png
index 473408b..5854b0d 100644
Binary files a/img/schema-database.png and b/img/schema-database.png differ
diff --git a/pom.xml b/pom.xml
index 6ad9433..065620c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,7 +21,6 @@
org.springframework.boot
spring-boot-starter-web
-
org.projectlombok
lombok
@@ -41,6 +40,14 @@
logbook-spring-boot-starter
3.7.2
+
+ com.h2database
+ h2
+
+
+ org.springframework.boot
+ spring-boot-starter-data-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 ed7be62..6c7700c 100644
--- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java
+++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java
@@ -49,4 +49,9 @@ public List getPopularFilms(@Positive @RequestParam(defaultValue = "10") i
return filmService.getPopularFilms(count);
}
+ @GetMapping("/{id}")
+ public Film getFilm(@PathVariable Long id) {
+ return filmService.getFilmById(id);
+ }
+
}
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..7dbd3f2
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java
@@ -0,0 +1,28 @@
+package ru.yandex.practicum.filmorate.controller;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import ru.yandex.practicum.filmorate.model.Genre;
+import ru.yandex.practicum.filmorate.service.GenreService;
+
+import java.util.Collection;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/genres")
+public class GenreController {
+ private final GenreService genreService;
+
+ @GetMapping("{id}")
+ public Genre getGenre(@PathVariable Long id) {
+ return genreService.getGenreById(id);
+ }
+
+ @GetMapping
+ public Collection getGenres() {
+ return genreService.getGenres();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/RatingController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/RatingController.java
new file mode 100644
index 0000000..86449c6
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/controller/RatingController.java
@@ -0,0 +1,28 @@
+package ru.yandex.practicum.filmorate.controller;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import ru.yandex.practicum.filmorate.model.Rating;
+import ru.yandex.practicum.filmorate.service.RatingService;
+
+import java.util.Collection;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/mpa")
+public class RatingController {
+ private final RatingService ratingService;
+
+ @GetMapping("{id}")
+ public Rating getRating(@PathVariable Long id) {
+ return ratingService.getRatingById(id);
+ }
+
+ @GetMapping
+ public Collection getRating() {
+ return ratingService.getRatings();
+ }
+}
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 189c38e..4e7e84f 100644
--- a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java
+++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java
@@ -22,7 +22,7 @@ public Collection getUsers() {
}
@PostMapping
- public User createFilm(@Valid @RequestBody User user) {
+ public User createUser(@Valid @RequestBody User user) {
return userService.createUser(user);
}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/exception/ErrorResponse.java b/src/main/java/ru/yandex/practicum/filmorate/exception/ErrorResponse.java
index e04a4ac..c5fcbfd 100644
--- a/src/main/java/ru/yandex/practicum/filmorate/exception/ErrorResponse.java
+++ b/src/main/java/ru/yandex/practicum/filmorate/exception/ErrorResponse.java
@@ -11,5 +11,4 @@ public ErrorResponse(String error, String description) {
this.error = error;
this.description = description;
}
-
}
\ No newline at end of file
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 f5cf2c0..88f5669 100644
--- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java
+++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java
@@ -1,7 +1,9 @@
package ru.yandex.practicum.filmorate.model;
+import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import ru.yandex.practicum.filmorate.json.DeserializerDuration;
@@ -13,6 +15,7 @@
import java.time.Duration;
import java.time.LocalDate;
import java.util.HashSet;
+import java.util.List;
import java.util.Set;
@Data
@@ -30,5 +33,9 @@ public class Film {
@JsonDeserialize(using = DeserializerDuration.class)
private Duration duration;
private final Set likes = new HashSet<>();
-
+ @JsonProperty("mpa")
+ @Valid
+ private Rating rating;
+ @Valid
+ private List genres;
}
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..bab0def
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/model/Genre.java
@@ -0,0 +1,17 @@
+package ru.yandex.practicum.filmorate.model;
+
+import jakarta.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Setter
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+public class Genre {
+ @NotNull
+ private Long id;
+ private String name;
+}
\ No newline at end of file
diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Rating.java b/src/main/java/ru/yandex/practicum/filmorate/model/Rating.java
new file mode 100644
index 0000000..2ce9d76
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/model/Rating.java
@@ -0,0 +1,19 @@
+package ru.yandex.practicum.filmorate.model;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import jakarta.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Setter
+@Getter
+@JsonIgnoreProperties(ignoreUnknown = true)
+@NoArgsConstructor
+@AllArgsConstructor
+public class Rating {
+ @NotNull
+ private Long id;
+ private 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 4e90863..716ae6c 100644
--- a/src/main/java/ru/yandex/practicum/filmorate/model/User.java
+++ b/src/main/java/ru/yandex/practicum/filmorate/model/User.java
@@ -24,5 +24,4 @@ public class User {
@PastOrPresent
private LocalDate birthday;
private final Set friends = new HashSet<>();
-
-}
+}
\ No newline at end of file
diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java
index 8d54f38..f698d25 100644
--- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java
+++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java
@@ -2,12 +2,18 @@
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
import ru.yandex.practicum.filmorate.exception.ExceptionNotFound;
import ru.yandex.practicum.filmorate.model.Film;
+import ru.yandex.practicum.filmorate.model.Genre;
+import ru.yandex.practicum.filmorate.model.Rating;
+import ru.yandex.practicum.filmorate.storage.dal.dao.FilmGenreDbStorage;
import ru.yandex.practicum.filmorate.storage.film.FilmStorage;
import java.util.Collection;
import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
@RequiredArgsConstructor
@Service
@@ -15,18 +21,34 @@ public class FilmService {
private final FilmStorage filmStorage;
private final UserService userService;
+ private final GenreService genreService;
+ private final RatingService ratingService;
+ private final FilmGenreDbStorage filmGenreDbStorage;
public Collection getFilms() {
return filmStorage.getFilms();
}
+ @Transactional
public Film createFilm(Film film) {
- return filmStorage.createFilm(film);
+ validateFilm(film);
+ Film createdFilm = filmStorage.createFilm(film);
+ if (film.getGenres() != null && !film.getGenres().isEmpty()) {
+ filmGenreDbStorage.saveFilmGenres(createdFilm.getId(), film.getGenres());
+ }
+ return createdFilm;
}
+ @Transactional
public Film updateFilm(Film film) {
- return filmStorage.updateFilm(film)
+ validateFilm(film);
+ Film updatedFilm = filmStorage.updateFilm(film)
.orElseThrow(() -> new ExceptionNotFound("Фильм с идентификатором '%d' не найден".formatted(film.getId())));
+ filmGenreDbStorage.deleteFilmGenresForFilmId(updatedFilm.getId());
+ if (film.getGenres() != null && !film.getGenres().isEmpty()) {
+ filmGenreDbStorage.saveFilmGenres(updatedFilm.getId(), film.getGenres());
+ }
+ return updatedFilm;
}
public void addLike(Long filmId, Long userId) {
@@ -50,4 +72,28 @@ public Film getFilmById(Long id) {
.orElseThrow(() -> new ExceptionNotFound("Фильм с идентификатором '%d' не найден".formatted(id)));
}
+ private void validateFilm(Film film) {
+ if (film.getGenres() != null) {
+ Set allGenreId = genreService.getGenres()
+ .stream()
+ .map(Genre::getId)
+ .collect(Collectors.toSet());
+ Set allGenreIdFilm = film.getGenres()
+ .stream()
+ .map(Genre::getId)
+ .collect(Collectors.toSet());
+ if (!allGenreId.containsAll(allGenreIdFilm)) {
+ throw new ExceptionNotFound("Один или несколько жанров не существует.");
+ }
+ }
+ if (film.getRating() != null && film.getRating().getId() != null) {
+ Set ratingId = ratingService.getRatings()
+ .stream()
+ .map(Rating::getId)
+ .collect(Collectors.toSet());
+ if (!ratingId.contains(film.getRating().getId())) {
+ throw new ExceptionNotFound("Рейтинг с идентификатором %d не найден".formatted(film.getRating().getId()));
+ }
+ }
+ }
}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/GenreService.java b/src/main/java/ru/yandex/practicum/filmorate/service/GenreService.java
new file mode 100644
index 0000000..a088cd1
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/service/GenreService.java
@@ -0,0 +1,24 @@
+package ru.yandex.practicum.filmorate.service;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import ru.yandex.practicum.filmorate.exception.ExceptionNotFound;
+import ru.yandex.practicum.filmorate.model.Genre;
+import ru.yandex.practicum.filmorate.storage.dal.dao.GenreDbStorage;
+
+import java.util.Collection;
+
+@RequiredArgsConstructor
+@Service
+public class GenreService {
+ private final GenreDbStorage genreDbStorage;
+
+ public Genre getGenreById(Long id) {
+ return genreDbStorage.getGenreById(id)
+ .orElseThrow(() -> new ExceptionNotFound("Жанр с идентификатором '%d' не найден".formatted(id)));
+ }
+
+ public Collection getGenres() {
+ return genreDbStorage.getGenres();
+ }
+}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/RatingService.java b/src/main/java/ru/yandex/practicum/filmorate/service/RatingService.java
new file mode 100644
index 0000000..6ac4ef5
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/service/RatingService.java
@@ -0,0 +1,24 @@
+package ru.yandex.practicum.filmorate.service;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import ru.yandex.practicum.filmorate.exception.ExceptionNotFound;
+import ru.yandex.practicum.filmorate.model.Rating;
+import ru.yandex.practicum.filmorate.storage.dal.dao.RatingDbStorage;
+
+import java.util.Collection;
+
+@RequiredArgsConstructor
+@Service
+public class RatingService {
+ private final RatingDbStorage ratingDbStorage;
+
+ public Rating getRatingById(Long id) {
+ return ratingDbStorage.getRatingById(id)
+ .orElseThrow(() -> new ExceptionNotFound("Рейтинг с идентификатором '%d' не найден".formatted(id)));
+ }
+
+ public Collection getRatings() {
+ return ratingDbStorage.getRatings();
+ }
+}
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 3107cda..3eb57aa 100644
--- a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java
+++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java
@@ -13,7 +13,6 @@
@RequiredArgsConstructor
@Service
public class UserService {
-
private final UserStorage userStorage;
public Collection getUsers() {
diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/dal/dao/FilmDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/dal/dao/FilmDbStorage.java
new file mode 100644
index 0000000..f6f20bf
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/storage/dal/dao/FilmDbStorage.java
@@ -0,0 +1,143 @@
+package ru.yandex.practicum.filmorate.storage.dal.dao;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Primary;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.ResultSetExtractor;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.jdbc.support.GeneratedKeyHolder;
+import org.springframework.jdbc.support.KeyHolder;
+import org.springframework.stereotype.Repository;
+import ru.yandex.practicum.filmorate.model.Film;
+import ru.yandex.practicum.filmorate.storage.film.FilmStorage;
+
+import java.sql.PreparedStatement;
+import java.sql.Statement;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+
+@Primary
+@Repository
+@RequiredArgsConstructor
+public class FilmDbStorage implements FilmStorage {
+ private final JdbcTemplate jdbcTemplate;
+ private final RowMapper filmRowMapper;
+ private final ResultSetExtractor filmExtractor;
+ private final ResultSetExtractor> filmsExtractor;
+
+ @Override
+ public Collection getFilms() {
+ String query = """
+ SELECT
+ f.film_id AS film_id,
+ f.name AS film_name,
+ f.description AS description,
+ f.release_date AS release_date,
+ f.duration AS duration,
+ f.rating_id AS rating_id,
+ r.name AS rating_name,
+ g.genre_id AS genre_id,
+ g.name AS genre_name
+ FROM films f
+ JOIN ratings r ON f.rating_id = r.rating_id
+ LEFT JOIN film_genres fg ON f.film_id = fg.film_id
+ LEFT JOIN genres g ON fg.genre_id = g.genre_id
+ ORDER BY f.film_id
+ """;
+ return jdbcTemplate.query(query, filmsExtractor);
+ }
+
+ @Override
+ public Film createFilm(Film film) {
+ KeyHolder keyHolder = new GeneratedKeyHolder();
+ String query = "INSERT INTO films (name, description, release_date, duration, rating_id) VALUES (?, ?, ?, ?, ?)";
+ jdbcTemplate.update(connection -> {
+ PreparedStatement ps = connection.prepareStatement(query, Statement.RETURN_GENERATED_KEYS);
+ ps.setString(1, film.getName());
+ ps.setString(2, film.getDescription());
+ ps.setDate(3, java.sql.Date.valueOf(film.getReleaseDate()));
+ ps.setLong(4, film.getDuration().toMinutes());
+ ps.setLong(5, film.getRating().getId());
+ return ps;
+ }, keyHolder);
+ Long id = keyHolder.getKeyAs(Long.class);
+ if (id == null) {
+ throw new IllegalStateException("Не удалось сгенерировать идентификатор фильма");
+ }
+ film.setId(id);
+ return film;
+ }
+
+ @Override
+ public Optional updateFilm(Film film) {
+ String query = """
+ UPDATE films
+ SET name = ?, description = ?, release_date = ?, duration = ?, rating_id = ?
+ WHERE film_id = ?;
+ """;
+ int countRows = jdbcTemplate.update(query,
+ film.getName(),
+ film.getDescription(),
+ film.getReleaseDate(),
+ film.getDuration(),
+ film.getRating().getId(),
+ film.getId()
+ );
+ if (countRows > 0) {
+ return Optional.of(film);
+ }
+ return Optional.empty();
+ }
+
+ @Override
+ public Optional getFilmById(Long id) {
+ String query = """
+ SELECT
+ f.film_id AS film_id,
+ f.name AS film_name,
+ f.description AS description,
+ f.release_date AS release_date,
+ f.duration AS duration,
+ f.rating_id AS rating_id,
+ r.name AS rating_name,
+ g.genre_id AS genre_id,
+ g.name AS genre_name
+ FROM films f
+ JOIN ratings r ON f.rating_id = r.rating_id
+ LEFT JOIN film_genres fg ON f.film_id = fg.film_id
+ LEFT JOIN genres g ON fg.genre_id = g.genre_id
+ WHERE f.film_id = ?
+ """;
+ return Optional.ofNullable(jdbcTemplate.query(query, filmExtractor, id));
+ }
+
+ @Override
+ public void addLike(Long filmId, Long userId) {
+ String query = "INSERT INTO film_likes(user_id, film_id) VALUES (?, ?)";
+ jdbcTemplate.update(query, userId, filmId);
+ }
+
+ @Override
+ public void removeLike(Long filmId, Long userId) {
+ String query = "DELETE FROM film_likes WHERE user_id = ? AND film_id = ?";
+ jdbcTemplate.update(query, userId, filmId);
+ }
+
+ @Override
+ public List getPopularFilms(int count) {
+ String query = """
+ SELECT f.film_id, f.name, f.description, f.release_date, f.duration, f.rating_id
+ FROM films f
+ LEFT JOIN (
+ SELECT film_id, COUNT(user_id) AS count_like
+ FROM film_likes
+ GROUP BY film_id
+ ) fl ON f.film_id = fl.film_id
+ ORDER BY fl.count_like DESC
+ LIMIT ?
+ """;
+ return jdbcTemplate.query(query, filmRowMapper, count);
+ }
+
+}
diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/dal/dao/FilmGenreDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/dal/dao/FilmGenreDbStorage.java
new file mode 100644
index 0000000..127d349
--- /dev/null
+++ b/src/main/java/ru/yandex/practicum/filmorate/storage/dal/dao/FilmGenreDbStorage.java
@@ -0,0 +1,33 @@
+package ru.yandex.practicum.filmorate.storage.dal.dao;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Repository;
+import ru.yandex.practicum.filmorate.model.Genre;
+
+import java.util.List;
+
+@Repository
+@RequiredArgsConstructor
+public class FilmGenreDbStorage {
+ private final JdbcTemplate jdbcTemplate;
+
+ public void saveFilmGenres(Long filmId, List genres) {
+ String query = """
+ INSERT INTO film_genres(film_id, genre_id)
+ VALUES (?, ?)
+ """;
+ List