From 0db63fe4ca1c30d36e9f61a997abc77eb51d4193 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Sun, 19 Jan 2025 18:24:34 +0700 Subject: [PATCH 01/19] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=BC=20=D0=B1=D0=B0=D0=B7=D1=83=20=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D1=85.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 58 ++++- pom.xml | 21 +- schema.png | Bin 0 -> 49865 bytes .../practicum/filmorate/model/Film.java | 8 + src/main/resources/application.properties | 6 +- src/main/resources/data.sql | 18 ++ src/main/resources/schema.sql | 50 +++++ .../controller/FilmControllerTest.java | 204 ------------------ .../filmorate/model/FilmApiTest.java | 136 ------------ .../practicum/filmorate/model/FilmTest.java | 112 ---------- 10 files changed, 152 insertions(+), 461 deletions(-) create mode 100644 schema.png 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/controller/FilmControllerTest.java delete mode 100644 src/test/java/ru/yandex/practicum/filmorate/model/FilmApiTest.java delete mode 100644 src/test/java/ru/yandex/practicum/filmorate/model/FilmTest.java diff --git a/README.md b/README.md index 3eee4ad..a2883fd 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,58 @@ Учебный проект. Созданиие приложений на основе шаблона "Spring"
-## Спринт 10 -Добавляем контроллеры фильмов и пользователей.
+## Спринт 12 (часть 1) +Добавляем базу данных. +![схема базы данных](/schema.png) -## Спринт 11 -Добавляем работу с "друзьями" и "лайками" \ No newline at end of file +### Описание таблиц базы данных + +1. **users** - таблица описания пользователей.
+поля: + - первичный ключ *id* - идентификатор подьзователя; + - *email* - адрес электронно почты пользователя; + - *login* - логин пользоателя; + - *name* - имя пользователя; + - *birthday* - дата рождения пользователя; + +
+2. **friends** - таблица связи с "друзьями" пользователя.
+ поля: + - *user_id* - идентификатор пользователя (отсылает к таблице *users*) - идентификатор пользователя; + - *friend_id* - идентификатор друга (отсылает к таблице *users*) - идентификатор пользователя; + - *confirmed* - флаг подтвержденной дружбы (если "дружба" двусторонняя); + +
+3. **genre** - таблица описания жанро фильма.
+ поля: + - первичный ключ *id* - идентификатор жанра; + - *name* - наименование жанра; + +
+4. **MPA** - таблица описания рейтингов Ассоциации кинокомпаний (MPA).
+ поля: + - первичный ключ *id* - идентификатор рейтинга; + - *code* - буквенный код рейтинга (G, PG, PG-13, R, NC-17); + - *description* - описание рейтинга; + +
+5. **films** - таблица описания фильмов.
+ поля: + - первичный ключ *id* - идентификатор фильма; + - *name* - название фильма; + - *description* - описание фильма; + - *releaseDAte* - бата выпуска фильма; + - *len_min* - длительность фильма в минутах; + - *MPA_id* -рейтинг MPA. (отсылает к таблице *MPA*) - идентификатор рейтинга; + +
+6. **film_genre** - таблица определения жанров фильма.
+ поля: + - *film_id* - идентификатор фильма (отсылает к таблице *films*); + - *genre_id* - идентификатор жанра (отсылает к таблице *genre*); + +
+7. **likes** - таблица "лайков" пользователей.
+ поля: + - *user_id* - идентификатор пользователя (отсылает к таблице *users*); + - *film_id* - идентификатор фильма (отсылает к таблице *films*); diff --git a/pom.xml b/pom.xml index 0a23621..eb4b913 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 3.2.4 + 3.4.1 ru.yandex.practicum @@ -22,15 +22,28 @@ gson + + org.projectlombok + lombok + provided + + org.springframework.boot spring-boot-starter-web + 3.4.1 - org.projectlombok - lombok - provided + com.h2database + h2 + 2.3.232 + + + + org.springframework.boot + spring-boot-starter-jdbc + 3.4.1 diff --git a/schema.png b/schema.png new file mode 100644 index 0000000000000000000000000000000000000000..c131c65f7f543e7381422356a76fd25c7b816732 GIT binary patch literal 49865 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 genres = new LinkedHashSet<>(); + } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 28aacc1..bfe0776 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,4 +1,8 @@ server.port=8080 - logging.level.org.zalando.logbook: TRACE +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..9f81de4 --- /dev/null +++ b/src/main/resources/data.sql @@ -0,0 +1,18 @@ +-- Заполняем справочник жанров +MERGE INTO genre (id, name) + VALUES ( 1, 'Комедия.'), + (2, 'Драма.'), + (3, 'Драма.'), + (4, 'Мультфильм.'), + (5, 'Триллер.'), + (6, 'Документальный.'), + (7, 'Боевик.'); + +-- Заполняем справочник рейтингов MPA +MERGE INTO MPA (id, code, description) + VALUES ( 1, 'G', 'у фильма нет возрастных ограничений'), + (2, 'PG', 'детям рекомендуется смотреть фильм с родителями'), + (3, 'PG-13', 'детям до 13 лет просмотр не желателен'), + (4, 'R', 'лицам до 17 лет просматривать фильм можно только в присутствии взрослого'), + (5, 'NC-17', 'лицам до 18 лет просмотр запрещён'); + diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000..666a38d --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,50 @@ +-- Создаем таблицу пользователей +CREATE TABLE IF NOT EXISTS users ( + id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + email VARCHAR(255) NOT NULL, + login VARCHAR(40) NOT NULL, + name VARCHAR(40) NOT NULL, + birthday DATE NOT NULL + ); + +-- Создаем таблицу друзей +CREATE TABLE IF NOT EXISTS friends ( + user_id INTEGER NOT NULL REFERENCES users(id), + friend_id INTEGER NOT NULL REFERENCES users(id), + confirmed BOOLEAN NOT NULL DEFAULT FALSE + ); + +-- Создаем справочник жанров фильма +CREATE TABLE IF NOT EXISTS genre ( + id INTEGER PRIMARY KEY, + name VARCHAR(40) NOT NULL + ); + +-- Создаем справочник рейтинга MPA +CREATE TABLE IF NOT EXISTS MPA ( + id INTEGER PRIMARY KEY, + code VARCHAR(8) NOT NULL, + description VARCHAR(80) + ); + +-- Создаем таблицу описания фильма +CREATE TABLE IF NOT EXISTS films ( + id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name VARCHAR(40) NOT NULL, + description VARCHAR(200), + releaseDate DATE, + len_min INTEGER, + MPA_id INTEGER NOT NULL REFERENCES MPA(id) + ); + +-- Создаем таблицу описания жанра фильма +CREATE TABLE IF NOT EXISTS film_genre ( + film_id INTEGER NOT NULL REFERENCES films(id), + genre_id INTEGER NOT NULL REFERENCES genre(id) + ); + +-- Создаем таблицу "лайков" к фильмам +CREATE TABLE IF NOT EXISTS likes ( + user_id INTEGER NOT NULL REFERENCES users(id), + film_id INTEGER NOT NULL REFERENCES films(id) + ); diff --git a/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java deleted file mode 100644 index cc3de5e..0000000 --- a/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java +++ /dev/null @@ -1,204 +0,0 @@ -package ru.yandex.practicum.filmorate.controller; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; -import ru.yandex.practicum.filmorate.model.Film; -import ru.yandex.practicum.filmorate.model.LocalDateAdapter; -import ru.yandex.practicum.filmorate.model.User; - -import java.time.LocalDate; - -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -/** - * Тестируем контроллер запросов о фильмах - */ -@SpringBootTest -@AutoConfigureMockMvc -class FilmControllerTest { - @Autowired - MockMvc mvc; - - static Gson gson = new GsonBuilder() - .setPrettyPrinting() - .registerTypeAdapter(LocalDate.class, new LocalDateAdapter()) - .create(); - - /** - * Перед каждым тестом очищаем список фильмов. - */ - @BeforeEach - void setUp() throws Exception { - mvc.perform(delete("/films")) - .andExpect(status().isOk()); - - mvc.perform(delete("/users")) - .andExpect(status().isOk()); - - // Создадим одного пользователя для "лайков" - User user = new User("User1234@domain", - "user1234", "test user", - LocalDate.now().minusYears(22)); - - String jsonString = gson.toJson(user); - mvc.perform(post("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()); - } - - /** - * Тестируем режим поиска фильмов. - */ - @Test - void findAllFilms() throws Exception { - makeFilms(3); - mvc.perform(get("/films")) - .andExpect(status().isOk()); - } - - /** - * Тестируем добавление информации о новом фильме. - */ - @Test - void addNewFilm() throws Exception { - Film film = new Film("Film Test1", - "Testing addNewFilm", - LocalDate.now().minusYears(10), - 60, 0); - String jsonString = gson.toJson(film); - - // При успешном добавлении фильма - // должен возвращаться статус 200 "Ok" - mvc.perform(post("/films") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()); - - // При повторном добавлении фильма - // должен возвращаться статус 400 "BadRequest" - mvc.perform(post("/films") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); - } - - /** - * Тестируем обновление информации о фильме - */ - @Test - void updateFilm() throws Exception { - Film film = new Film("Film Test2", - "Testing updateFilm", - LocalDate.now().minusYears(10), - 60, 0); - String jsonString = gson.toJson(film); - - // Добавляем тестовый фильм - mvc.perform(post("/films") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()); - - film.setDescription("Updated."); - jsonString = gson.toJson(film); - // При обновлении фильма с отсутствующим id - // должен возвращаться статус 400 "BadRequest" - mvc.perform(put("/films") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); - - - film.setId(1000); - jsonString = gson.toJson(film); - // При обновлении фильма с неверным id - // должен возвращаться статус 404 "NotFound" - mvc.perform(put("/films") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isNotFound()); - - film.setId(1); - jsonString = gson.toJson(film); - // При обновлении фильма с корректным id - // должен возвращаться статус 200 "Ok" - mvc.perform(put("/films") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); - } - - /** - * Тестируем добавление "лайка" - * - * @throws Exception - */ - @Test - void addLike() throws Exception { - makeFilms(3); - - // При добавлении "лайка" от несуществующего пользователя - // должен возвращаться статус 404 "NotFound" - mvc.perform(put("/films/1/like/1000") - .content("") - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isNotFound()); - - // При добавлении "лайка" - // должен возвращаться статус 200 "Ok" - mvc.perform(put("/films/2/like/1") - .content("") - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); - } - - /** - * Тестируем удаление "лайка" - * - * @throws Exception - */ - @Test - void deleteLike() throws Exception { - addLike(); - - // При удалении "лайка" - // должен возвращаться статус 200 "Ok" - mvc.perform(delete("/films/2/like/1") - .content("") - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); - } - - /** - * Генерация тестовых фильмов - * - * @param count - количество фильмов - * @throws Exception - */ - void makeFilms(int count) throws Exception { - StringBuilder fBuilder = new StringBuilder(); - fBuilder.append("{\"name\": \"Film%d\","); - fBuilder.append("\"description\": \"description%d\","); - fBuilder.append("\"releaseDate\": \"2000-01-%02d\","); - fBuilder.append("\"duration\": %d}"); - String formatStr = fBuilder.toString(); - - for (int i = 1; i <= count; i++) { - String jsonString = String.format(formatStr, i, i, i, i * 10); - mvc.perform(post("/films") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()); - } - - } -} diff --git a/src/test/java/ru/yandex/practicum/filmorate/model/FilmApiTest.java b/src/test/java/ru/yandex/practicum/filmorate/model/FilmApiTest.java deleted file mode 100644 index a72aa0e..0000000 --- a/src/test/java/ru/yandex/practicum/filmorate/model/FilmApiTest.java +++ /dev/null @@ -1,136 +0,0 @@ -package ru.yandex.practicum.filmorate.model; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; - -import java.time.LocalDate; - -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -/** - * Тестирование ограничений на значения полей класса Film при Http запросах - * Тестирование использования объектов в качестве параметров методов - */ -@SpringBootTest -@AutoConfigureMockMvc -class FilmApiTest { - @Autowired - private MockMvc mvc; - - private Gson gson = new GsonBuilder() - .setPrettyPrinting() - .registerTypeAdapter(LocalDate.class, new LocalDateAdapter()) - .create(); - - /** - * Перед каждым тестом очищаем список фильмов. - */ - @BeforeEach - void setUp() throws Exception { - mvc.perform(delete("/films")) - .andExpect(status().isOk()); - } - - /** - * Проверка непустого названия фильма. - */ - @Test - void testName() throws Exception { - Film film = new Film("", - "Testing film.name", - LocalDate.now().minusYears(10), - 60, 0); - String jsonString = gson.toJson(film); - // При добавлении фильма без названия - // должен возвращаться статус 400 "BadRequest" - mvc.perform(post("/films") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()) - .andDo(print()); - } - - /** - * Проверка допусимого размера описания. - */ - @Test - void testDescription() throws Exception { - Film film = new Film("Film", - "12345678901234567890123456789012345678901234567890" - + "12345678901234567890123456789012345678901234567890" - + "12345678901234567890123456789012345678901234567890" - + "12345678901234567890123456789012345678901234567890" - + "12345678901234567890123456789012345678901234567890", - LocalDate.now().minusYears(10), - 60, 0); - String jsonString = gson.toJson(film); - // При добавлении фильма - // должен возвращаться статус 400 "BadRequest" - mvc.perform(post("/films") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()) - .andDo(print()); - - } - - /** - * Проверка допустимой даты выпуска фильма - */ - @Test - void testReleaseDate() throws Exception { - Film film = new Film("Film", - "Testing film.releaseDate", - LocalDate.now().plusDays(1), - 60, 0); - String jsonString = gson.toJson(film); - // При добавлении фильма - // должен возвращаться статус 400 "BadRequest" - mvc.perform(post("/films") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()) - .andDo(print()); - - film.setReleaseDate(LocalDate.of(1895, 12, 27)); - jsonString = gson.toJson(film); - // При добавлении фильма - // должен возвращаться статус 400 "BadRequest" - mvc.perform(post("/films") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()) - .andDo(print()); - } - - /** - * Проверка допустимой длительности фильма - */ - @Test - void testDuration() throws Exception { - Film film = new Film("Film", - "Testing film.releaseDate", - LocalDate.now().minusYears(10), - 0, 0); - String jsonString = gson.toJson(film); - - // При добавлении фильма - // должен возвращаться статус 400 "BadRequest" - mvc.perform(post("/films") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()) - .andDo(print()); - } - -} diff --git a/src/test/java/ru/yandex/practicum/filmorate/model/FilmTest.java b/src/test/java/ru/yandex/practicum/filmorate/model/FilmTest.java deleted file mode 100644 index f5da073..0000000 --- a/src/test/java/ru/yandex/practicum/filmorate/model/FilmTest.java +++ /dev/null @@ -1,112 +0,0 @@ -package ru.yandex.practicum.filmorate.model; - -import jakarta.validation.ConstraintViolation; -import jakarta.validation.Validation; -import jakarta.validation.Validator; -import jakarta.validation.ValidatorFactory; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.time.LocalDate; -import java.util.Set; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** - * Тестирование ограничений на значения полей класса Film - * Автономный тест (Junit). - */ -class FilmTest { - private Validator validator; - - /** - * Перед каждым тестом готовим Validator - */ - @BeforeEach - void setUp() { - ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); - validator = factory.getValidator(); - } - - /** - * Проверка непустого названия фильма. - */ - @Test - void testName() { - Film film = new Film("", - "Testing film.name", - LocalDate.now().minusYears(10), - 60, 0); - - Set> violations = validator.validate(film, Marker.OnBasic.class); - assertFalse(violations.isEmpty()); - } - - /** - * Проверка допусимого размера описания. - */ - @Test - void testDescription() { - Film film = new Film("Film", - "12345678901234567890123456789012345678901234567890" - + "12345678901234567890123456789012345678901234567890" - + "12345678901234567890123456789012345678901234567890" - + "12345678901234567890123456789012345678901234567890" - + "12345678901234567890123456789012345678901234567890", - LocalDate.now().minusYears(10), - 60, 0); - - Set> violations = validator.validate(film, Marker.OnBasic.class); - assertFalse(violations.isEmpty()); - } - - /** - * Проверка допустимой даты выпуска фильма - */ - @Test - void testReleaseDate() { - Film film = new Film("Film", - "Testing film.releaseDate", - LocalDate.now().plusDays(1), - 60, 0); - - Set> violations = validator.validate(film, Marker.OnBasic.class); - assertFalse(violations.isEmpty()); - - film.setReleaseDate(LocalDate.of(1895, 12, 27)); - - violations.clear(); - violations = validator.validate(film, Marker.OnBasic.class); - assertFalse(violations.isEmpty()); - } - - /** - * Проверка допустимой длительности фильма - */ - @Test - void testDuration() { - Film film = new Film("Film", - "Testing film.duration", - LocalDate.now().minusYears(10), - 0, 0); - - Set> violations = validator.validate(film, Marker.OnBasic.class); - assertFalse(violations.isEmpty()); - } - - /** - * Тестируем отсутствие ограничений при корректном создании фильма - */ - @Test - void testFilmOk() { - Film film = new Film("Film Ok", - "Testing film", - LocalDate.now().minusYears(10), - 60, 0); - - Set> violations = validator.validate(film, Marker.OnBasic.class); - assertTrue(violations.isEmpty(), violations.toString()); - } - -} \ No newline at end of file From 20d08308ad611458238488d29712d2798bdbe4ee Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Sun, 19 Jan 2025 18:35:47 +0700 Subject: [PATCH 02/19] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=81=D1=82=D0=B8=D0=BB=D1=8F?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/ru/yandex/practicum/filmorate/model/Film.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 5b080d1..005f039 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java @@ -40,7 +40,7 @@ public class Film extends StorageData { private Integer rank = 0; // рейтинг Ассоциации кинокомпаний - private Integer MPA_id = 0; + private Integer mpa_id = 1; // жанры фильма private LinkedHashSet genres = new LinkedHashSet<>(); From a1f5cb0754742ca7131126a94d7f2f2b3cb3b65a Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Sun, 19 Jan 2025 18:35:47 +0700 Subject: [PATCH 03/19] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=81=D1=82=D0=B8=D0=BB=D1=8F?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/ru/yandex/practicum/filmorate/model/Film.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 005f039..5913a8e 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java @@ -40,7 +40,7 @@ public class Film extends StorageData { private Integer rank = 0; // рейтинг Ассоциации кинокомпаний - private Integer mpa_id = 1; + private Integer mpaId = 1; // жанры фильма private LinkedHashSet genres = new LinkedHashSet<>(); From 50c17793b9d2bb9fbe04eace1d89906547c9e682 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Sun, 19 Jan 2025 18:40:12 +0700 Subject: [PATCH 04/19] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=81=D1=82=D0=B8=D0=BB=D1=8F?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/ru/yandex/practicum/filmorate/model/Film.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 5913a8e..8af34f5 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java @@ -40,7 +40,7 @@ public class Film extends StorageData { private Integer rank = 0; // рейтинг Ассоциации кинокомпаний - private Integer mpaId = 1; + private Integer mpaId = 0; // жанры фильма private LinkedHashSet genres = new LinkedHashSet<>(); From 5ded6e4f1586f63f9d6a1da265743affc2c5145e Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Thu, 23 Jan 2025 21:29:36 +0700 Subject: [PATCH 05/19] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D1=81=D1=85=D0=B5=D0=BC=D0=B0=20=D0=B1?= =?UTF-8?q?=D0=B0=D0=B7=D1=8B=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- schema.png | Bin 49865 -> 49447 bytes src/main/resources/data.sql | 2 +- src/main/resources/schema.sql | 13 ++++++++----- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/schema.png b/schema.png index c131c65f7f543e7381422356a76fd25c7b816732..31ffa589147858b4139a85a8a7180766375eae8f 100644 GIT binary patch literal 49447 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 Date: Thu, 23 Jan 2025 21:36:48 +0700 Subject: [PATCH 06/19] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D1=81=D1=85=D0=B5=D0=BC=D0=B0=20=D0=B1?= =?UTF-8?q?=D0=B0=D0=B7=D1=8B=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/schema.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 9d12393..e2d1aa6 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -41,7 +41,7 @@ CREATE TABLE IF NOT EXISTS films ( -- Создаем таблицу описания жанра фильма CREATE TABLE IF NOT EXISTS films_genres ( film_id INTEGER NOT NULL REFERENCES films(id), - genre_id INTEGER NOT NULL REFERENCES genre(id), + genre_id INTEGER NOT NULL REFERENCES genres(id), PRIMARY KEY (film_id, genre_id) ); From 67844a900a73d73a92d12bfad476de4bb2339a76 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Thu, 23 Jan 2025 21:44:21 +0700 Subject: [PATCH 07/19] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=D0=B0=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=B0=20=D1=81=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D1=82=D0=B5=D0=BB=D1=8F=D0=BC=D0=B8=20-=20UserDbStorage.?= =?UTF-8?q?java?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../filmorate/controller/ErrorHandler.java | 13 +- .../filmorate/controller/FilmController.java | 2 +- .../filmorate/controller/UserController.java | 4 +- .../exception/InternalServerException.java | 7 + .../practicum/filmorate/model/Film.java | 14 +- .../filmorate/model/StorageData.java | 13 - .../practicum/filmorate/model/User.java | 17 +- .../filmorate/service/FilmService.java | 3 +- .../filmorate/service/UserService.java | 35 +-- .../storage/user/InMemoryUserStorage.java | 24 +- .../filmorate/storage/user/UserDbStorage.java | 196 ++++++++++++ .../filmorate/storage/user/UserRowMapper.java | 23 ++ .../filmorate/storage/user/UserStorage.java | 5 +- .../{model => validator}/LegalFilmDate.java | 2 +- .../LegalFilmDateValidator.java | 2 +- .../LocalDateAdapter.java | 2 +- .../{model => validator}/Marker.java | 2 +- .../ValidationErrorResponse.java | 2 +- .../{model => validator}/Violation.java | 2 +- .../controller/UserControllerTest.java | 283 ------------------ .../filmorate/model/UserApiTest.java | 209 ------------- .../practicum/filmorate/model/UserTest.java | 95 ------ 22 files changed, 297 insertions(+), 658 deletions(-) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/exception/InternalServerException.java delete mode 100644 src/main/java/ru/yandex/practicum/filmorate/model/StorageData.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/user/UserRowMapper.java rename src/main/java/ru/yandex/practicum/filmorate/{model => validator}/LegalFilmDate.java (92%) rename src/main/java/ru/yandex/practicum/filmorate/{model => validator}/LegalFilmDateValidator.java (94%) rename src/main/java/ru/yandex/practicum/filmorate/{model => validator}/LocalDateAdapter.java (95%) rename src/main/java/ru/yandex/practicum/filmorate/{model => validator}/Marker.java (87%) rename src/main/java/ru/yandex/practicum/filmorate/{model => validator}/ValidationErrorResponse.java (90%) rename src/main/java/ru/yandex/practicum/filmorate/{model => validator}/Violation.java (88%) delete mode 100644 src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java delete mode 100644 src/test/java/ru/yandex/practicum/filmorate/model/UserApiTest.java delete mode 100644 src/test/java/ru/yandex/practicum/filmorate/model/UserTest.java diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/ErrorHandler.java b/src/main/java/ru/yandex/practicum/filmorate/controller/ErrorHandler.java index c3ef798..b4cf882 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/ErrorHandler.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/ErrorHandler.java @@ -9,11 +9,12 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; +import ru.yandex.practicum.filmorate.exception.InternalServerException; import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.exception.ValidationException; import ru.yandex.practicum.filmorate.model.ErrorMessage; -import ru.yandex.practicum.filmorate.model.ValidationErrorResponse; -import ru.yandex.practicum.filmorate.model.Violation; +import ru.yandex.practicum.filmorate.validator.ValidationErrorResponse; +import ru.yandex.practicum.filmorate.validator.Violation; import java.util.List; import java.util.stream.Collectors; @@ -101,6 +102,14 @@ public ResponseEntity onHttpMessageNotReadableException( .body(new ErrorMessage("В запросе отсутствуют необходимые данные.")); } + + @ExceptionHandler + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ErrorMessage internalException(final InternalServerException e) { + log.warn("Error", e); + return new ErrorMessage(e.getMessage()); + } + /** * Обработка непредвиденного исключения * 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 e343f1a..06fccc5 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -6,7 +6,7 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import ru.yandex.practicum.filmorate.model.Film; -import ru.yandex.practicum.filmorate.model.Marker; +import ru.yandex.practicum.filmorate.validator.Marker; import ru.yandex.practicum.filmorate.service.FilmService; import java.util.Collection; 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 250a3e4..51e2f8d 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java @@ -5,9 +5,9 @@ import org.springframework.http.HttpStatus; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; -import ru.yandex.practicum.filmorate.model.Marker; import ru.yandex.practicum.filmorate.model.User; import ru.yandex.practicum.filmorate.service.UserService; +import ru.yandex.practicum.filmorate.validator.Marker; import java.util.Collection; @@ -59,7 +59,7 @@ public User findUser(@PathVariable Integer id) { @GetMapping("/{id}/friends") public Collection findUsersFriends(@PathVariable Integer id) { log.info("Ищем друзей пользователя id={}.", id); - return service.getUsersFriends(id); + return service.getUserFriends(id); } /** diff --git a/src/main/java/ru/yandex/practicum/filmorate/exception/InternalServerException.java b/src/main/java/ru/yandex/practicum/filmorate/exception/InternalServerException.java new file mode 100644 index 0000000..2cce47d --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/exception/InternalServerException.java @@ -0,0 +1,7 @@ +package ru.yandex.practicum.filmorate.exception; + +public class InternalServerException extends RuntimeException { + public InternalServerException(String message) { + super(message); + } +} \ 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 8af34f5..859107a 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java @@ -1,13 +1,14 @@ package ru.yandex.practicum.filmorate.model; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; -import lombok.ToString; import org.springframework.validation.annotation.Validated; +import ru.yandex.practicum.filmorate.validator.LegalFilmDate; +import ru.yandex.practicum.filmorate.validator.Marker; import java.time.LocalDate; import java.util.LinkedHashSet; @@ -16,11 +17,12 @@ * Класс описания фильма. */ @Data -@ToString(callSuper = false) -@EqualsAndHashCode(exclude = {"id", "description"}) // при сравнении не учитывать: id, description -@AllArgsConstructor +@EqualsAndHashCode(of = {"name", "releaseDate"}) @Validated -public class Film extends StorageData { +public class Film { + + @NotNull(groups = {Marker.OnUpdate.class}, message = "id должен быть определен") + protected Integer id; @NotBlank(message = "Название фильма не может быть пустым.", groups = {Marker.OnBasic.class, Marker.OnUpdate.class}) diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/StorageData.java b/src/main/java/ru/yandex/practicum/filmorate/model/StorageData.java deleted file mode 100644 index 5a8bb54..0000000 --- a/src/main/java/ru/yandex/practicum/filmorate/model/StorageData.java +++ /dev/null @@ -1,13 +0,0 @@ -package ru.yandex.practicum.filmorate.model; - -import jakarta.validation.constraints.NotNull; -import lombok.Data; - -/** - * Класс данных для наследования классов модели - */ -@Data -public class StorageData { - @NotNull(groups = {Marker.OnUpdate.class}, message = "id должен быть определен") - protected Integer id; -} 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 fd9ad3f..5d7c350 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/User.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/User.java @@ -1,14 +1,10 @@ package ru.yandex.practicum.filmorate.model; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.PastOrPresent; -import jakarta.validation.constraints.Pattern; -import lombok.AllArgsConstructor; +import jakarta.validation.constraints.*; import lombok.Data; import lombok.EqualsAndHashCode; -import lombok.ToString; import org.springframework.validation.annotation.Validated; +import ru.yandex.practicum.filmorate.validator.Marker; import java.time.LocalDate; @@ -16,11 +12,12 @@ * Класс описания пользователя. */ @Data -@ToString(callSuper = false) -@EqualsAndHashCode(exclude = {"id", "name", "birthday"}) -@AllArgsConstructor +@EqualsAndHashCode(of = {"email"}) @Validated -public class User extends StorageData { +public class User { + + @NotNull(groups = {Marker.OnUpdate.class}, message = "id должен быть определен") + protected Integer id; @NotBlank(message = "Email не может быть пустым", groups = Marker.OnBasic.class) @Email(message = "Email должен удовлетворять правилам формирования почтовых адресов.", 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 fc5634c..36a380f 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java @@ -1,6 +1,7 @@ package ru.yandex.practicum.filmorate.service; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.exception.ValidationException; @@ -22,7 +23,7 @@ public class FilmService { private final FilmStorage films; private final UserStorage users; - public FilmService(FilmStorage filmStorage, UserStorage users) { + public FilmService(FilmStorage filmStorage, @Qualifier("userDbStorage") UserStorage users) { this.films = filmStorage; this.users = users; } 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 6007441..ad4ad1f 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java @@ -1,16 +1,14 @@ package ru.yandex.practicum.filmorate.service; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; 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.User; import ru.yandex.practicum.filmorate.storage.user.UserStorage; -import java.util.ArrayList; import java.util.Collection; -import java.util.List; /** * Класс реализации запросов к информации о пользователях @@ -21,9 +19,8 @@ public class UserService { private final UserStorage users; - @Autowired - public UserService(UserStorage userStorage) { - this.users = userStorage; + public UserService(@Qualifier("userDbStorage") UserStorage users) { + this.users = users; } /** @@ -49,7 +46,7 @@ public User addNewUser(User user) { } if (users.findAllUsers().contains(user)) { throw new ValidationException("Пользователь уже существует " - + user.getEmail()); + + user.getEmail()); } return users.addNewUser(user); } @@ -92,6 +89,8 @@ public User updateUser(User updUser) { if (updUser.getBirthday() != null) { user.setBirthday(updUser.getBirthday()); } + + users.updateUser(user); return user; } @@ -119,10 +118,8 @@ public void addFriends(Integer id1, Integer id2) { users.getUserById(id2).orElseThrow(() -> new NotFoundException("Не найден пользователь id=" + id2)); - // Добавление в друзья происходит без подтверждения. - // Еслb id1 дружит с id2, то автоматически id2 дружит с id1 + // Добавление в друзья users.addFriend(id1, id2); - users.addFriend(id2, id1); } /** @@ -147,19 +144,15 @@ public void breakUpFriends(Integer id1, Integer id2) { * @param userId - идентификатор пользователя * @return - список друзей */ - public Collection getUsersFriends(Integer userId) { + public Collection getUserFriends(Integer userId) { users.getUserById(userId).orElseThrow(() -> new NotFoundException("Не найден пользователь id=" + userId)); - List friends = new ArrayList<>(); - for (Integer friendId : users.findAllFriends(userId)) { - friends.add(users.getUserById(friendId).get()); - } - return friends; + return users.getUserFriends(userId); } /** - * Метод поиска общих друзей пользователей + * Поиск общих друзей пользователей * * @param id1 - идентификатор пользователя * @param id2 - идентификатор другого пользователя @@ -171,12 +164,6 @@ public Collection getCommonFriends(Integer id1, Integer id2) { users.getUserById(id2).orElseThrow(() -> new NotFoundException("Не найден пользователь id=" + id2)); - List friendsId = users.findAllFriends(id1); - friendsId.retainAll(users.findAllFriends(id2)); - List friends = new ArrayList<>(); - for (Integer id : friendsId) { - friends.add(users.getUserById(id).get()); - } - return friends; + return users.getCommonFriends(id1, id2); } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java index 6e9f9fa..9d3cf13 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java @@ -1,11 +1,11 @@ package ru.yandex.practicum.filmorate.storage.user; -import org.springframework.stereotype.Component; +import org.springframework.stereotype.Repository; import ru.yandex.practicum.filmorate.model.User; import java.util.*; -@Component +@Repository("inMemoryUserStorage") public class InMemoryUserStorage implements UserStorage { private final Map users = new HashMap<>(); @@ -46,6 +46,7 @@ public void removeAllUsers() { @Override public void addFriend(Integer userId, Integer friendId) { friends.get(userId).add(friendId); + friends.get(friendId).add(userId); } @Override @@ -55,7 +56,22 @@ public void breakUpFriends(Integer id1, Integer id2) { } @Override - public List findAllFriends(Integer userId) { - return new ArrayList<>(friends.get(userId)); + public Collection getUserFriends(Integer userId) { + List dtoFriends = new ArrayList<>(); + for (Integer friendId : friends.get(userId)) { + dtoFriends.add(users.get(friendId)); + } + return dtoFriends; + } + + @Override + public Collection getCommonFriends(Integer id1, Integer id2) { + List friendsId = new ArrayList<>(friends.get(id1)); + friendsId.retainAll(new ArrayList<>(friends.get(id2))); + List dtoFriends = new ArrayList<>(); + for (Integer id : friendsId) { + dtoFriends.add(users.get(id)); + } + return dtoFriends; } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java new file mode 100644 index 0000000..dcfae97 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java @@ -0,0 +1,196 @@ +package ru.yandex.practicum.filmorate.storage.user; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.exception.InternalServerException; +import ru.yandex.practicum.filmorate.model.User; + +import java.sql.Types; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +@Slf4j +@Repository("userDbStorage") +public class UserDbStorage implements UserStorage { + private static final String SQL_INSERT_USER = "INSERT INTO users (email, login, name, birthday) VALUES (:email, :login, :name, :birthday)"; + private static final String SQL_UPDATE_USER = "UPDATE users SET email = :email, login = :login, name = :name, birthday = :birthday WHERE id = :id"; + private static final String SQL_FIND_USER = "SELECT * FROM users WHERE id = :id"; + private static final String SQL_FIND_ALL_USERS = "SELECT * FROM users"; + private static final String SQL_DELETE_USERS = "DELETE FROM users WHERE id <> :id"; + private static final String SQL_ADD_FRIEND = "MERGE INTO friends (user_id, friend_id, confirmed) VALUES (:userId, :friendId, FALSE)"; + private static final String SQL_REMOVE_FRIEND = "DELETE FROM friends WHERE (user_id = :userId) AND (friend_id = :friendId)"; + + + private final NamedParameterJdbcTemplate jdbc; + private final UserRowMapper mapper; + + public UserDbStorage(NamedParameterJdbcTemplate jdbc, UserRowMapper mapper) { + this.jdbc = jdbc; + this.mapper = mapper; + } + + /** + * Добавление в базу нового пользователя + * + * @param newUser - объект для добавления + * @return - подтвержденный объект + */ + @Override + public User addNewUser(User newUser) { + // для доступа к сгенерированому ключу новой записи создаем объект GeneratedKeyHolder + GeneratedKeyHolder generatedKeyHolder = new GeneratedKeyHolder(); + + jdbc.update(SQL_INSERT_USER, + new MapSqlParameterSource() + .addValue("email", newUser.getEmail()) + .addValue("login", newUser.getLogin()) + .addValue("name", newUser.getName()) + .addValue("birthday", newUser.getBirthday(), Types.DATE), + generatedKeyHolder + ); + + // присваиваем сгенерирванный ключ записи в качестве идентификатора пользователя + newUser.setId(generatedKeyHolder.getKey().intValue()); + return newUser; + } + + /** + * Поиск пользователя по идентификатору. + * + * @param id - идентификатор пользователя + * @return - Optional + */ + @Override + public Optional getUserById(Integer id) { + try { + User user = jdbc.queryForObject(SQL_FIND_USER, + new MapSqlParameterSource() + .addValue("id", id), + mapper); + return Optional.ofNullable(user); + } catch (EmptyResultDataAccessException ignored) { + return Optional.empty(); + } + } + + /** + * Поиск всех пользователей + * + * @return - список пользователей + */ + @Override + public Collection findAllUsers() { + try { + return jdbc.query(SQL_FIND_ALL_USERS, mapper); + } catch (EmptyResultDataAccessException ignored) { + return List.of(); + } + } + + /** + * Обновление сведений о пользователе + * + * @param updUser - объект с обновленной информацией + */ + @Override + public void updateUser(User updUser) { + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("email", updUser.getEmail()); + params.addValue("login", updUser.getLogin()); + params.addValue("name", updUser.getName()); + params.addValue("birthday", updUser.getBirthday(), Types.DATE); + params.addValue("id", updUser.getId()); + + int rowsUpdated = jdbc.update(SQL_UPDATE_USER, params); + if (rowsUpdated == 0) { + throw new InternalServerException("Не удалось обновить данные"); + } + } + + /** + * Удаление всех пользователей + */ + @Override + public void removeAllUsers() { + jdbc.update(SQL_DELETE_USERS, new MapSqlParameterSource() + .addValue("id", 0) + ); + } + + /** + * Добавление "друга" + * + * @param userId - идентификатор пользователя + * @param friendId - идентификатор друга + */ + @Override + public void addFriend(Integer userId, Integer friendId) { + jdbc.update(SQL_ADD_FRIEND, new MapSqlParameterSource() + .addValue("userId", userId) + .addValue("friendId", friendId) + ); + } + + /** + * Исключение из "друзей" + * + * @param userId - идентификатор пользователя + * @param friendsId - идентификатор друга + */ + @Override + public void breakUpFriends(Integer userId, Integer friendsId) { + jdbc.update(SQL_REMOVE_FRIEND, new MapSqlParameterSource() + .addValue("userId", userId) + .addValue("friendId", friendsId) + ); + } + + /** + * Поиск друзей пользователя. + * + * @param userId - идентификатор пользователя + * @return - список друзей + */ + @Override + public Collection getUserFriends(Integer userId) { + String sql = "SELECT * FROM users WHERE id IN " + + "(SELECT friend_id FROM friends WHERE user_id = :userId)"; + try { + return jdbc.query(sql, new MapSqlParameterSource() + .addValue("userId", userId), + mapper + ); + } catch (EmptyResultDataAccessException ignored) { + return List.of(); + } + } + + /** + * Поиск общих друзей + * + * @param id1 - идентификатор первого пользователя + * @param id2 - идентификатор второго пользователя + * @return - спмсок общих друзей + */ + @Override + public Collection getCommonFriends(Integer id1, Integer id2) { + String sql = "SELECT * FROM users WHERE id IN (SELECT tu.friend_id " + + "FROM (SELECT * FROM friends WHERE user_id = :userId1) AS tu " + + "INNER JOIN (SELECT * FROM friends WHERE user_id = :userId2) AS tf " + + "WHERE tu.friend_id = tf.friend_id)"; + try { + return jdbc.query(sql, new MapSqlParameterSource() + .addValue("userId1", id1) + .addValue("userId2", id2), + mapper + ); + } catch (EmptyResultDataAccessException ignored) { + return List.of(); + } + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserRowMapper.java new file mode 100644 index 0000000..8f1bc91 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserRowMapper.java @@ -0,0 +1,23 @@ +package ru.yandex.practicum.filmorate.storage.user; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.User; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@Component +public class UserRowMapper implements RowMapper { + + @Override + public User mapRow(ResultSet resultSet, int rowNum) throws SQLException { + User user = new User(); + user.setId(resultSet.getInt("id")); + user.setEmail(resultSet.getString("email")); + user.setLogin(resultSet.getString("login")); + user.setName(resultSet.getString("name")); + user.setBirthday(resultSet.getDate(5).toLocalDate()); + return user; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java index 3fa46c4..094492d 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java @@ -3,7 +3,6 @@ import ru.yandex.practicum.filmorate.model.User; import java.util.Collection; -import java.util.List; import java.util.Optional; public interface UserStorage { @@ -24,5 +23,7 @@ public interface UserStorage { void breakUpFriends(Integer id1, Integer id2); - List findAllFriends(Integer userId); + Collection getUserFriends(Integer userId); + + Collection getCommonFriends(Integer id1, Integer id2); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/LegalFilmDate.java b/src/main/java/ru/yandex/practicum/filmorate/validator/LegalFilmDate.java similarity index 92% rename from src/main/java/ru/yandex/practicum/filmorate/model/LegalFilmDate.java rename to src/main/java/ru/yandex/practicum/filmorate/validator/LegalFilmDate.java index 6a48a1c..d0d9105 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/LegalFilmDate.java +++ b/src/main/java/ru/yandex/practicum/filmorate/validator/LegalFilmDate.java @@ -1,4 +1,4 @@ -package ru.yandex.practicum.filmorate.model; +package ru.yandex.practicum.filmorate.validator; import jakarta.validation.Constraint; diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/LegalFilmDateValidator.java b/src/main/java/ru/yandex/practicum/filmorate/validator/LegalFilmDateValidator.java similarity index 94% rename from src/main/java/ru/yandex/practicum/filmorate/model/LegalFilmDateValidator.java rename to src/main/java/ru/yandex/practicum/filmorate/validator/LegalFilmDateValidator.java index 23eae05..99b44b3 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/LegalFilmDateValidator.java +++ b/src/main/java/ru/yandex/practicum/filmorate/validator/LegalFilmDateValidator.java @@ -1,4 +1,4 @@ -package ru.yandex.practicum.filmorate.model; +package ru.yandex.practicum.filmorate.validator; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/LocalDateAdapter.java b/src/main/java/ru/yandex/practicum/filmorate/validator/LocalDateAdapter.java similarity index 95% rename from src/main/java/ru/yandex/practicum/filmorate/model/LocalDateAdapter.java rename to src/main/java/ru/yandex/practicum/filmorate/validator/LocalDateAdapter.java index 3722b3c..6f015e2 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/LocalDateAdapter.java +++ b/src/main/java/ru/yandex/practicum/filmorate/validator/LocalDateAdapter.java @@ -1,4 +1,4 @@ -package ru.yandex.practicum.filmorate.model; +package ru.yandex.practicum.filmorate.validator; import com.google.gson.TypeAdapter; import com.google.gson.stream.JsonReader; diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Marker.java b/src/main/java/ru/yandex/practicum/filmorate/validator/Marker.java similarity index 87% rename from src/main/java/ru/yandex/practicum/filmorate/model/Marker.java rename to src/main/java/ru/yandex/practicum/filmorate/validator/Marker.java index e595677..665d46f 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Marker.java +++ b/src/main/java/ru/yandex/practicum/filmorate/validator/Marker.java @@ -1,4 +1,4 @@ -package ru.yandex.practicum.filmorate.model; +package ru.yandex.practicum.filmorate.validator; /** * Описание интерфейсов групп проверки аннотаций diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/ValidationErrorResponse.java b/src/main/java/ru/yandex/practicum/filmorate/validator/ValidationErrorResponse.java similarity index 90% rename from src/main/java/ru/yandex/practicum/filmorate/model/ValidationErrorResponse.java rename to src/main/java/ru/yandex/practicum/filmorate/validator/ValidationErrorResponse.java index 08d0619..42e9cf7 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/ValidationErrorResponse.java +++ b/src/main/java/ru/yandex/practicum/filmorate/validator/ValidationErrorResponse.java @@ -1,4 +1,4 @@ -package ru.yandex.practicum.filmorate.model; +package ru.yandex.practicum.filmorate.validator; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Violation.java b/src/main/java/ru/yandex/practicum/filmorate/validator/Violation.java similarity index 88% rename from src/main/java/ru/yandex/practicum/filmorate/model/Violation.java rename to src/main/java/ru/yandex/practicum/filmorate/validator/Violation.java index e28b642..68db96c 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Violation.java +++ b/src/main/java/ru/yandex/practicum/filmorate/validator/Violation.java @@ -1,4 +1,4 @@ -package ru.yandex.practicum.filmorate.model; +package ru.yandex.practicum.filmorate.validator; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java deleted file mode 100644 index 67c49b1..0000000 --- a/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java +++ /dev/null @@ -1,283 +0,0 @@ -package ru.yandex.practicum.filmorate.controller; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; -import ru.yandex.practicum.filmorate.model.LocalDateAdapter; -import ru.yandex.practicum.filmorate.model.User; - -import java.time.LocalDate; - -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -/** - * Тестируем контроллер запросов данных о пользователях - */ -@SpringBootTest -@AutoConfigureMockMvc -class UserControllerTest { - @Autowired - MockMvc mvc; - - Gson gson = new GsonBuilder() - .setPrettyPrinting() - .registerTypeAdapter(LocalDate.class, new LocalDateAdapter()) - .create(); - - /** - * Перед каждым тестом удаляем всех пользователей - */ - @BeforeEach - void setUp() throws Exception { - mvc.perform(delete("/users")) - .andExpect(status().isOk()); - } - - /** - * Тестируем чтение списка пользователей - */ - @Test - void findAllUser() throws Exception { - makeUsers(3); - - mvc.perform(get("/users")) - .andExpect(status().isOk()) // ожидается код статус 200 - .andDo(print()); - } - - /** - * Тестируем добавление нового пользователя - */ - @Test - void addNewUser() throws Exception { - User user = new User("User1234@domain", - "user1234", "test user", - LocalDate.now().minusYears(22)); - String jsonString = gson.toJson(user); - - // При успешном добавлении пользователя - // должен возвращаться статус 200 "Ok" - mvc.perform(post("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()); - - // Повторное добавление пользователя - // должно возвращать статус 400 "BadRequest" - mvc.perform(post("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); - } - - /** - * Тестирование обновления сведений о пользователе - */ - @Test - void updateUser() throws Exception { - User user = new User("User1234@domain", - "user0000", "testing user", - LocalDate.now().minusYears(22)); - String jsonString = gson.toJson(user); - - // Создаем тестового пользователя - mvc.perform(post("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()); - - user.setLogin("user12345"); - user.setName("Updated user."); - jsonString = gson.toJson(user); - - // Обновление записи без идентификатора - // должно возвращать статус 400 "BadRequest" - mvc.perform(put("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); - - user.setId(1000); - jsonString = gson.toJson(user); - // Обновление записи c несуществющим идентификатором - // должно возвращать статус 404 "NotFound" - mvc.perform(put("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isNotFound()); - - user.setId(1); - jsonString = gson.toJson(user); - // Успешное обновление записи - // должно возвращать статус 200 "Ok" - mvc.perform(put("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); - } - - /** - * Тестируем добавление друзей - * - * @throws Exception - */ - @Test - void addFriends() throws Exception { - makeUsers(3); - - // Объявление в "друзья" несуществующего пользователя - // должно возвращать статус 404 "NotFound" - mvc.perform(put("/users/1000/friends/1") - .content("") - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isNotFound()); - - // Объявление в "друзья" несуществующего друга - // должно возвращать статус 404 "NotFound()" - mvc.perform(put("/users/1/friends/1000") - .content("") - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isNotFound()); - - // Объявление в "друзья" сущществующих пользователей - // должно возвращать статус 200 "ok" - mvc.perform(put("/users/1/friends/2") - .content("") - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); - - // Объявление в "друзья" сущществующих пользователей (граничный случай) - // должно возвращать статус 200 "ok" - mvc.perform(put("/users/3/friends/2") - .content("") - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); - } - - /** - * Тестируем удаление друзей - * - * @throws Exception - */ - @Test - void removeFriends() throws Exception { - addFriends(); - - // Удаление из "друзьей" не сущществующих пользователей - // должно возвращать статус 404 "NotFound" - mvc.perform(delete("/users/1/friends/1000") - .content("") - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isNotFound()); - - // Удаление из "друзьей" сущществующих пользователей - // должно возвращать статус 200 "Ok" - mvc.perform(delete("/users/1/friends/2") - .content("") - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); - } - - /** - * Тестируем чтение списка друзей - * - * @throws Exception - */ - @Test - void getFriends() throws Exception { - makeUsers(3); - - // Объявление в "друзья" - // должно возвращать статус 200 "ok" - mvc.perform(put("/users/1/friends/2") - .content("") - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); - - // Объявление в "друзья" - // должно возвращать статус 200 "ok" - mvc.perform(put("/users/3/friends/2") - .content("") - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); - - // читаем список "друзей", несуществующего пользователя - // должно возвращать статус 404 "NotFound" - mvc.perform(get("/users/2000/friends") - .content("") - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isNotFound()); - - - // читаем список "друзей" - // должно возвращать статус 200 "ok" - mvc.perform(get("/users/2/friends") - .content("") - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); - } - - /** - * Тестируем поиск общих друзей - * - * @throws Exception - */ - @Test - void findCommonFrends() throws Exception { - makeUsers(3); - - // Объявление в "друзья" - // должно возвращать статус 200 "ok" - mvc.perform(put("/users/1/friends/2") - .content("") - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); - - // Объявление в "друзья" - // должно возвращать статус 200 "ok" - mvc.perform(put("/users/3/friends/2") - .content("") - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); - - // читаем список общих "друзей" - // должно возвращать статус 200 "ok" - mvc.perform(get("/users/1/friends/common/3") - .content("") - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); - } - - /** - * Создание тестовых пользователей - * - * @param count - требуемое клличество тестовых пользователей - * @throws Exception - */ - void makeUsers(int count) throws Exception { - StringBuilder fBuilder = new StringBuilder(); - fBuilder.append("{\"email\": \"user000%d@domain\","); - fBuilder.append("\"login\": \"USER000%d\","); - fBuilder.append("\"name\": \"userName00%d\","); - fBuilder.append("\"birthday\": \"2000-01-%02d\"}"); - String formatStr = fBuilder.toString(); - - for (int i = 1; i <= count; i++) { - String jsonString = String.format(formatStr, i, i, i, i); - // При успешном добавлении пользователя - // должен возвращаться статус 200 "Ok" - mvc.perform(post("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()); - } - } -} diff --git a/src/test/java/ru/yandex/practicum/filmorate/model/UserApiTest.java b/src/test/java/ru/yandex/practicum/filmorate/model/UserApiTest.java deleted file mode 100644 index f14854a..0000000 --- a/src/test/java/ru/yandex/practicum/filmorate/model/UserApiTest.java +++ /dev/null @@ -1,209 +0,0 @@ -package ru.yandex.practicum.filmorate.model; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; - -import java.time.LocalDate; - -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -/** - * Тестирование ограничений на значения класса User при Http запросах. - * Тестирование использования объектов в качестве параметров методов - */ -@SpringBootTest -@AutoConfigureMockMvc -class UserApiTest { - @Autowired - private MockMvc mvc; - - private Gson gson = new GsonBuilder() - .setPrettyPrinting() - .registerTypeAdapter(LocalDate.class, new LocalDateAdapter()) - .create(); - - /** - * Перед каждым тестом удаляем всех пользователей - */ - @BeforeEach - void setUp(/*@Autowired MockMvc mvc*/) throws Exception { - mvc.perform(delete("/users")) - .andExpect(status().isOk()); - } - - /** - * Тестирование email пользователя - */ - @Test - void testEmail() throws Exception { - User user = new User(null, - "userTest", - "Testing user", - LocalDate.now().minusYears(32)); - String jsonString = gson.toJson(user); - - // Создание пользователя без email - // должно возвращать статус 400 "BadRequest" - mvc.perform(post("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); - - user.setEmail("user.domain@"); - jsonString = gson.toJson(user); - // Создание пользователя с неправильным email - // должно возвращать статус 400 "BadRequest" - mvc.perform(post("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); - - user.setEmail("user@domain"); - jsonString = gson.toJson(user); - // Создание пользователя с корректным email - // должно возвращать статус 201 "Created" - mvc.perform(post("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()); - } - - /** - * Тестирование login пользователя - */ - @Test - void testLogin() throws Exception { - User user = new User("user1234@test", - "", - "Testing user", - LocalDate.now().minusYears(32)); - String jsonString = gson.toJson(user); - - // Создание пользователя с пустым login - // должно возвращать статус 400 "BadRequest" - mvc.perform(post("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); - - user.setLogin("user test"); - jsonString = gson.toJson(user); - // Создание пользователя с login содержащим пробел - // должно возвращать статус 400 "BadRequest" - mvc.perform(post("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); - - user.setLogin("user1234"); - jsonString = gson.toJson(user); - // Создание пользователя с корректным login (содержит только латинские буквы и цифры) - // должно возвращать статус 201 "Created" - mvc.perform(post("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()); - } - - /** - * Тестируем корректность даты рождения - */ - @Test - void testBirthday() throws Exception { - User user = new User("user1234@test", - "user1234", - "Testing user", - LocalDate.now().plusDays(30)); - - String jsonString = gson.toJson(user); - // Создание пользователя с датой рождения в будущем - // должно возвращать статус 400 "BadRequest" - mvc.perform(post("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); - - user.setBirthday(LocalDate.now().minusYears(30)); - jsonString = gson.toJson(user); - // Создание тестового пользователя с корректной датой рождения - // должно возвращать статус 200 "Ok" - mvc.perform(post("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()); - } - - /** - * Тестируем группу аннотаций для режима обновления данных - */ - @Test - void testUpdateUser() throws Exception { - User user = new User("user1234@test", - "user1234", - "Testing user", - LocalDate.now().minusYears(32)); - String jsonString = gson.toJson(user); - // Создание тестового пользователя - // должно возвращать статус 201 "Created" - mvc.perform(post("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()); - - jsonString = "{\"id\": 1, \"email\": \"user.domain@\"}"; - // Изменение пользователю email на некорректный - // должно возвращать статус 400 "BadRequest" - mvc.perform(put("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); - - jsonString = "{\"id\": 1, \"email\": \"user@host.domain\"}"; - // Изменение пользователю email на допустимый - // должно возвращать статус 200 "Ok" - mvc.perform(put("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); - - jsonString = "{\"id\": 1, \"login\": \"user test12\"}"; - // Изменение пользователю login на некорректный - // должно возвращать статус 400 "BadRequest" - mvc.perform(put("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); - - jsonString = "{\"id\": 1, \"login\": \"userTest\"}"; - // Изменение пользователю login на допустимый - // должно возвращать статус 200 "Ok" - mvc.perform(put("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); - - jsonString = "{\"id\": 1, \"birthday\": \"2050-01-01\"}"; - // Обновление пользователя с датой рождения в будущем - // должно возвращать статус 400 "BadRequest" - mvc.perform(put("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); - - jsonString = "{\"id\": 1, \"birthday\": \"2005-01-01\"}"; - // Обновление пользователя корректной датой рождения - // должно возвращать статус 200 "Ok" - mvc.perform(put("/users") - .content(jsonString) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()); - } -} diff --git a/src/test/java/ru/yandex/practicum/filmorate/model/UserTest.java b/src/test/java/ru/yandex/practicum/filmorate/model/UserTest.java deleted file mode 100644 index 2e61eb5..0000000 --- a/src/test/java/ru/yandex/practicum/filmorate/model/UserTest.java +++ /dev/null @@ -1,95 +0,0 @@ -package ru.yandex.practicum.filmorate.model; - -import jakarta.validation.ConstraintViolation; -import jakarta.validation.Validation; -import jakarta.validation.Validator; -import jakarta.validation.ValidatorFactory; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.time.LocalDate; -import java.util.Set; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** - * Тестирование ограничений на значения полей класса User. - * Автономный тест (Junit). - */ -// @SpringBootTest -// @AutoConfigureMockMvc -class UserTest { - private Validator validator; - - /** - * Перед каждым тестом готовим Validator - */ - @BeforeEach - void setUp() { - ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); - validator = factory.getValidator(); - } - - /** - * Тестирование email пользователя - */ - @Test - void testInvalidEmail() throws Exception { - User user = new User("", - "userTest", - "Testing user", - LocalDate.now().minusYears(22)); - - Set> violations = validator.validate(user, Marker.OnBasic.class); - assertFalse(violations.isEmpty()); - } - - /** - * Тестирование login пользователя - */ - @Test - void testInvalidLogin() throws Exception { - User user = new User("user1234@test", - "", // login не должен быть пустым - "Testing user", - LocalDate.now().minusYears(32)); - - Set> violations = validator.validate(user, Marker.OnBasic.class); - assertFalse(violations.isEmpty()); - - // login должен содержать только буквы и цифры - user.setLogin("yu%3242 @#"); - violations.clear(); - violations = validator.validate(user, Marker.OnBasic.class); - assertFalse(violations.isEmpty()); - } - - /** - * Тестируем корректность даты рождения - */ - @Test - void testInvalidBirthday() throws Exception { - User user = new User("user1234@test", - "user1234", - "Testing user", - LocalDate.now().plusDays(1)); // Дата рождения в будущем - - Set> violations = validator.validate(user, Marker.OnBasic.class); - assertFalse(violations.isEmpty()); - } - - /** - * Тестируем отсутствие ошибок при корректном заполнение полей. - */ - @Test - void testUserOk() { - User user = new User("user1234@test", - "user1234", - "Testing user", - LocalDate.now().minusYears(18)); - - Set> violations = validator.validate(user, Marker.OnBasic.class); - assertTrue(violations.isEmpty(), violations.toString()); - } -} From 1bf776b66f7b2efeed7e90a48e52ac5eb4b898c0 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Fri, 24 Jan 2025 21:41:29 +0700 Subject: [PATCH 08/19] =?UTF-8?q?=D0=9E=D0=BF=D1=82=D0=B8=D0=BC=D0=B8?= =?UTF-8?q?=D0=B7=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B0=20=D1=80=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D1=82=D0=B0=20=D1=81=20=D1=84=D0=B8=D0=BB=D1=8C?= =?UTF-8?q?=D0=BC=D0=B0=D0=BC=D0=B8=20-=20InMemoryFilmStorage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../practicum/filmorate/model/Film.java | 2 -- .../filmorate/service/FilmService.java | 8 +++---- .../filmorate/storage/film/FilmStorage.java | 3 +++ .../storage/film/InMemoryFilmStorage.java | 22 ++++++++++++++----- 4 files changed, 23 insertions(+), 12 deletions(-) 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 859107a..75af024 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java @@ -39,8 +39,6 @@ public class Film { groups = {Marker.OnBasic.class, Marker.OnUpdate.class}) private int duration; - private Integer rank = 0; - // рейтинг Ассоциации кинокомпаний private Integer mpaId = 0; 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 36a380f..913b4f1 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java @@ -105,8 +105,7 @@ public Integer addNewLike(Integer filmId, Integer userId) { users.getUserById(userId).orElseThrow(() -> new NotFoundException("Не найден пользователь id=" + userId)); - film.setRank(films.addNewLike(filmId, userId)); - return film.getRank(); + return films.addNewLike(filmId, userId); } public Integer removeLike(Integer filmId, Integer userId) { @@ -115,8 +114,7 @@ public Integer removeLike(Integer filmId, Integer userId) { users.getUserById(userId).orElseThrow(() -> new NotFoundException("Не найден пользователь id=" + userId)); - film.setRank(films.removeLike(filmId, userId)); - return film.getRank(); + return films.removeLike(filmId, userId); } public Collection findPopularFilms(int count) { @@ -129,7 +127,7 @@ public Map getFilmRank(Integer filmId) { Map response = new HashMap<>(); response.put("Фильм ", film.getName()); - response.put("Рейтинг", film.getRank().toString()); + response.put("Рейтинг", films.getFilmRank(filmId).toString()); return response; } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java index 826408c..7216873 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmStorage.java @@ -27,5 +27,8 @@ public interface FilmStorage { // удаление "лайка" к фильму Integer removeLike(Integer filmId, Integer userId); + // Чтение числа "лайков" у филььма + Integer getFilmRank(Integer filmId); + void removeAllFilms(); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java index 814cfc0..f6bde3d 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java @@ -17,7 +17,7 @@ public class InMemoryFilmStorage implements FilmStorage { public Film addNewFilm(Film film) { filmId++; film.setId(filmId); - film.setRank(0); + // film.setRank(0); films.put(filmId, film); likes.put(filmId, new HashSet<>()); filmsRating.add(film); @@ -50,7 +50,7 @@ public void updateFilm(Film updFilm) { public Integer addNewLike(Integer filmId, Integer userId) { likes.get(filmId).add(userId); Film film = films.get(filmId); - film.setRank(likes.get(filmId).size()); + // film.setRank(likes.get(filmId).size()); setFilmsRating(film); return likes.get(filmId).size(); } @@ -66,11 +66,20 @@ public Integer addNewLike(Integer filmId, Integer userId) { public Integer removeLike(Integer filmId, Integer userId) { likes.get(filmId).remove(userId); Film film = films.get(filmId); - film.setRank(likes.get(filmId).size()); + // film.setRank(likes.get(filmId).size()); setFilmsRating(film); return likes.get(filmId).size(); } + @Override + public Integer getFilmRank(Integer filmId) { + int filmRank = 0; + if (likes.containsKey(filmId)) { + filmRank = likes.get(filmId).size(); + } + return filmRank; + } + /** * Определение позиции фильма в рейтинге. * Так как рейтинг представляет собой уже упорядоченный список, @@ -86,18 +95,21 @@ private void setFilmsRating(Film film) { if (ratingSize < 2) { return; } + int index = filmsRating.indexOf(film); + Integer filmRank = getFilmRank(film.getId()); + // Проверяем изменение рейтинга на возрастание while ((index > 0) && - (film.getRank() > filmsRating.get(index - 1).getRank())) { + (filmRank > likes.get(filmsRating.get(index - 1).getId()).size())) { filmsRating.set(index, filmsRating.get(index - 1)); filmsRating.set(--index, film); } // Проверяем изменение рейтинга на убывание while (index < (ratingSize - 1) && - (film.getRank() < filmsRating.get(index + 1).getRank())) { + (filmRank < likes.get(filmsRating.get(index + 1).getId()).size())) { filmsRating.set(index, filmsRating.get(index + 1)); filmsRating.set(++index, film); } From 334a5bb0c7babc057271fe8eac4b67d5fd0b8f51 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Wed, 12 Feb 2025 22:55:07 +0700 Subject: [PATCH 09/19] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=D0=B0=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=B0=20=D1=81=20=D0=B1=D0=B0=D0=B7=D0=BE=D0=B9=20=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D1=85.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../filmorate/controller/ErrorHandler.java | 8 + .../filmorate/controller/GenreController.java | 39 ++ .../filmorate/controller/MpaController.java | 36 ++ .../filmorate/mapper/FilmGenreRowMapper.java | 20 + .../filmorate/mapper/FilmRowMapper.java | 22 ++ .../filmorate/mapper/GenreRowMapper.java | 19 + .../filmorate/mapper/MpaRowMapper.java | 19 + .../user => mapper}/UserRowMapper.java | 2 +- .../practicum/filmorate/model/Film.java | 18 +- .../practicum/filmorate/model/FilmGenre.java | 13 + .../practicum/filmorate/model/Genre.java | 9 + .../yandex/practicum/filmorate/model/Mpa.java | 13 + .../filmorate/service/FilmService.java | 22 +- .../filmorate/service/GenreService.java | 26 ++ .../filmorate/service/MpaService.java | 26 ++ .../filmorate/storage/film/FilmDbStorage.java | 370 ++++++++++++++++++ .../storage/film/InMemoryFilmStorage.java | 3 +- .../storage/genre/GenreDbStorage.java | 60 +++ .../filmorate/storage/genre/GenreStorage.java | 11 + .../filmorate/storage/mpa/MpaDbStorage.java | 48 +++ .../filmorate/storage/mpa/MpaStorage.java | 11 + .../filmorate/storage/user/UserDbStorage.java | 12 +- src/main/resources/data.sql | 17 +- src/main/resources/schema.sql | 2 +- 24 files changed, 799 insertions(+), 27 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/mapper/FilmGenreRowMapper.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/mapper/FilmRowMapper.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/mapper/GenreRowMapper.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/mapper/MpaRowMapper.java rename src/main/java/ru/yandex/practicum/filmorate/{storage/user => mapper}/UserRowMapper.java (92%) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/model/FilmGenre.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/service/GenreService.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/service/MpaService.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/genre/GenreDbStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/genre/GenreStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/mpa/MpaDbStorage.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/mpa/MpaStorage.java diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/ErrorHandler.java b/src/main/java/ru/yandex/practicum/filmorate/controller/ErrorHandler.java index b4cf882..2276139 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/ErrorHandler.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/ErrorHandler.java @@ -2,6 +2,7 @@ import jakarta.validation.ConstraintViolationException; import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DataAccessException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; @@ -86,6 +87,13 @@ public ErrorMessage notFoundObject(NotFoundException exception) { return new ErrorMessage(exception.getMessage()); } + @ExceptionHandler + @ResponseStatus(HttpStatus.NOT_FOUND) + public ErrorMessage notFoundData(DataAccessException exception) { + return new ErrorMessage(exception.getMessage()); + } + + /** * Обработка исключения HttpMessageNotReadableException при поступлении пустого запроса * 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..de38ece --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java @@ -0,0 +1,39 @@ +package ru.yandex.practicum.filmorate.controller; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.service.GenreService; +import ru.yandex.practicum.filmorate.service.UserService; + +import java.util.Collection; + +@Slf4j +@RestController +@RequestMapping("/genres") +public class GenreController { + + private final GenreService genreService; + + public GenreController(GenreService genreService) { + this.genreService = genreService; + } + + @GetMapping + @ResponseStatus(HttpStatus.OK) + public Collection findAllGenres() { + log.info("Запрашиваем список всех жанров."); + return genreService.getAllGenres(); + } + + @GetMapping("/{id}") + @ResponseStatus(HttpStatus.OK) + public Genre findGenreById(@PathVariable int id) { + log.info("Ищем жанр id={}.", id); + return genreService.getGenreById(id); + } + +} + 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..b2b4d64 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/MpaController.java @@ -0,0 +1,36 @@ +package ru.yandex.practicum.filmorate.controller; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.model.Mpa; +import ru.yandex.practicum.filmorate.service.MpaService; + +import java.util.Collection; + +@Slf4j +@RestController +@RequestMapping("/mpa") +public class MpaController { + + private final MpaService mpaService; + + public MpaController(MpaService mpaService) { + this.mpaService = mpaService; + } + + @GetMapping + @ResponseStatus(HttpStatus.OK) + public Collection findAllMpa() { + log.info("Запрашиваем список всех рейтинков MPA."); + return mpaService.filndAllMpa(); + } + + @GetMapping("/{id}") + @ResponseStatus(HttpStatus.OK) + public Mpa findMpaById(@PathVariable int id) { + log.info("Ищем рейтинг id={}.", id); + return mpaService.findMpa(id); + } + +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmGenreRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmGenreRowMapper.java new file mode 100644 index 0000000..85f0f59 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmGenreRowMapper.java @@ -0,0 +1,20 @@ +package ru.yandex.practicum.filmorate.mapper; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.FilmGenre; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@Component +public class FilmGenreRowMapper implements RowMapper { + @Override + public FilmGenre mapRow(ResultSet resultSet, int rowNum) throws SQLException { + FilmGenre filmGenre = new FilmGenre(); + filmGenre.setFilmId(resultSet.getInt("film_id")); + filmGenre.setGenreId(resultSet.getInt("genre_id")); + filmGenre.setGenreName(resultSet.getString("genre_name")); + return filmGenre; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmRowMapper.java new file mode 100644 index 0000000..9d87952 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmRowMapper.java @@ -0,0 +1,22 @@ +package ru.yandex.practicum.filmorate.mapper; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.Film; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@Component +public class FilmRowMapper implements RowMapper { + @Override + public Film mapRow(ResultSet resultSet, int rowNum) throws SQLException { + Film film = new Film(); + film.setId(resultSet.getInt("id")); + film.setName(resultSet.getString("name")); + film.setDescription(resultSet.getString("description")); + film.setReleaseDate(resultSet.getDate(4).toLocalDate()); + film.setDuration(resultSet.getInt("len_min")); + return film; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/mapper/GenreRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mapper/GenreRowMapper.java new file mode 100644 index 0000000..46a75dd --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/GenreRowMapper.java @@ -0,0 +1,19 @@ +package ru.yandex.practicum.filmorate.mapper; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.Genre; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@Component +public class GenreRowMapper implements RowMapper { + @Override + public Genre mapRow(ResultSet resultSet, int rowNum) throws SQLException { + Genre genre = new Genre(); + genre.setId(resultSet.getInt("id")); + genre.setName(resultSet.getString("name")); + return genre; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/mapper/MpaRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mapper/MpaRowMapper.java new file mode 100644 index 0000000..5100e36 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/MpaRowMapper.java @@ -0,0 +1,19 @@ +package ru.yandex.practicum.filmorate.mapper; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.Mpa; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@Component +public class MpaRowMapper implements RowMapper { + public Mpa mapRow(ResultSet rs, int rowNum) throws SQLException { + Mpa mpa = new Mpa(); + mpa.setId(rs.getInt("id")); + mpa.setName(rs.getString("name")); + mpa.setDescription(rs.getString("description")); + return mpa; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mapper/UserRowMapper.java similarity index 92% rename from src/main/java/ru/yandex/practicum/filmorate/storage/user/UserRowMapper.java rename to src/main/java/ru/yandex/practicum/filmorate/mapper/UserRowMapper.java index 8f1bc91..ab46955 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserRowMapper.java +++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/UserRowMapper.java @@ -1,4 +1,4 @@ -package ru.yandex.practicum.filmorate.storage.user; +package ru.yandex.practicum.filmorate.mapper; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Component; 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 75af024..bd4dd27 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java @@ -18,7 +18,7 @@ */ @Data @EqualsAndHashCode(of = {"name", "releaseDate"}) -@Validated +//@Validated public class Film { @NotNull(groups = {Marker.OnUpdate.class}, message = "id должен быть определен") @@ -40,9 +40,21 @@ public class Film { private int duration; // рейтинг Ассоциации кинокомпаний - private Integer mpaId = 0; + @NotNull(groups = {Marker.OnBasic.class}, message = "рейтинг MPA должен быть определен") + private Mpa mpa; // жанры фильма - private LinkedHashSet genres = new LinkedHashSet<>(); + private LinkedHashSet genres = new LinkedHashSet<>(); + public void addGenre(Genre genre) { + genres.add(genre); + } + + public void removeGenre(Genre genre) { + genres.remove(genre); + } + + public void clearGenres() { + genres.clear(); + } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/FilmGenre.java b/src/main/java/ru/yandex/practicum/filmorate/model/FilmGenre.java new file mode 100644 index 0000000..6eefaef --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/FilmGenre.java @@ -0,0 +1,13 @@ +package ru.yandex.practicum.filmorate.model; + +import lombok.Data; + +/** + * Сопоставление идентификатора фильма с соответствующим жанром + */ +@Data +public class FilmGenre { + private Integer filmId; + private Integer genreId; + private String genreName; +} 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..5eb2277 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Genre.java @@ -0,0 +1,9 @@ +package ru.yandex.practicum.filmorate.model; + +import lombok.Data; + +@Data +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..25022e3 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Mpa.java @@ -0,0 +1,13 @@ +package ru.yandex.practicum.filmorate.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; + +@Data +public class Mpa { + private int id; + private String name; + + @JsonIgnore + private String description; +} 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 913b4f1..afc1bc1 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java @@ -1,8 +1,8 @@ package ru.yandex.practicum.filmorate.service; -import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.exception.InternalServerException; import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.exception.ValidationException; import ru.yandex.practicum.filmorate.model.Film; @@ -16,14 +16,14 @@ /** * Класс реализации запросов к информации о фильмах */ -@Slf4j @Service public class FilmService { private final FilmStorage films; private final UserStorage users; - public FilmService(FilmStorage filmStorage, @Qualifier("userDbStorage") UserStorage users) { + public FilmService(@Qualifier("filmDbStorage") FilmStorage filmStorage, + @Qualifier("userDbStorage") UserStorage users) { this.films = filmStorage; this.users = users; } @@ -73,7 +73,6 @@ public Film updateFilm(Film updFilm) { Film film = films.getFilmById(id).orElseThrow(() -> new NotFoundException("Не найден фильм id=" + id)); - // Обновляем информаию во временном объекте if (updFilm.getName() != null) { film.setName(updFilm.getName()); } @@ -86,7 +85,16 @@ public Film updateFilm(Film updFilm) { if (updFilm.getDuration() > 0) { film.setDuration(updFilm.getDuration()); } - return film; + if (updFilm.getMpa() != null) { + film.setMpa(updFilm.getMpa()); + } + if (updFilm.getGenres().size() > 0) { + film.setGenres(updFilm.getGenres()); + } + films.updateFilm(film); + + return films.getFilmById(id).orElseThrow(() -> + new InternalServerException("Ошибка обновления фильма id=" + id)); } /** @@ -126,8 +134,8 @@ public Map getFilmRank(Integer filmId) { new NotFoundException("Не найден фильм id=" + filmId)); Map response = new HashMap<>(); - response.put("Фильм ", film.getName()); - response.put("Рейтинг", films.getFilmRank(filmId).toString()); + response.put("Фильм ", film.getName()); + response.put("лайков", films.getFilmRank(filmId).toString()); return response; } } 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..d40c2fc --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/GenreService.java @@ -0,0 +1,26 @@ +package ru.yandex.practicum.filmorate.service; + +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.storage.genre.GenreStorage; + +import java.util.Collection; + +@Service +public class GenreService { + private final GenreStorage genereStorage; + + public GenreService(GenreStorage genereStorage) { + this.genereStorage = genereStorage; + } + + public Collection getAllGenres() { + return genereStorage.getAllGenres(); + } + + public Genre getGenreById(int id) { + return genereStorage.getGenre(id).orElseThrow(() -> + new NotFoundException("Не найден фильм id=" + id)); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/MpaService.java b/src/main/java/ru/yandex/practicum/filmorate/service/MpaService.java new file mode 100644 index 0000000..c065418 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/MpaService.java @@ -0,0 +1,26 @@ +package ru.yandex.practicum.filmorate.service; + +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Mpa; +import ru.yandex.practicum.filmorate.storage.mpa.MpaStorage; + +import java.util.Collection; + +@Service +public class MpaService { + private final MpaStorage mpaStorage; + + public MpaService(MpaStorage mpaStorage) { + this.mpaStorage = mpaStorage; + } + + public Collection filndAllMpa() { + return mpaStorage.findAllMpa(); + } + + public Mpa findMpa(Integer id) { + return mpaStorage.findMpa(id).orElseThrow(() -> + new NotFoundException("Не найден рейтинг id=" + id)); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java new file mode 100644 index 0000000..16f052c --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java @@ -0,0 +1,370 @@ +package ru.yandex.practicum.filmorate.storage.film; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.ResultSetExtractor; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.exception.InternalServerException; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.exception.ValidationException; +import ru.yandex.practicum.filmorate.mapper.FilmGenreRowMapper; +import ru.yandex.practicum.filmorate.mapper.FilmRowMapper; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.model.FilmGenre; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.model.Mpa; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.*; + +@Repository("filmDbStorage") +public class FilmDbStorage implements FilmStorage { + + private static final String SQL_INSERT_FILM = + "INSERT INTO films (name, description, releasedate, len_min, mpa_id)" + + "VALUES ( :name, :description, :releasedate, :len_min, :mpa_id)"; + private final String SQL_UPDATE_GENRES = "MERGE INTO films_genres (film_id, genre_id) " + + "VALUES (:film_id, :genre_id)"; + private static final String SQL_FIND_FILM_BY_ID = "SELECT f.*, mpa.name as mpa_name, fg.genre_id, g.name AS genre_name\n" + + " FROM (films AS f INNER JOIN mpa ON f.MPA_ID = mpa.ID)\n" + + " LEFT JOIN (films_genres AS fg INNER JOIN genres AS g ON fg.GENRE_ID = g.ID) ON fg.film_id = f.id\n" + + " WHERE f.id = :id"; + private static final String SQL_UPDATE_FILM = "UPDATE films SET name = :name, description = :description, " + + "releasedate = :releasedate, len_min = :len_min, mpa_id = :mpa_id WHERE id = :id"; + private static final String SQL_ADD_LIKE = "MERGE INTO likes (user_id, film_id) VALUES (:userId, :filmId)"; + private static final String SQL_REMOVE_LIKE = "DELETE FROM likes WHERE user_id = :userId AND film_id = :filmId"; + + private final NamedParameterJdbcTemplate jdbc; + private final FilmRowMapper filmMapper; + private final FilmGenreRowMapper filmGenreRowMapper; + + /** + * Основной конструктор + * + * @param jdbc - объект работы с базой данных + * @param filmMapper - объект преобразования строк базы данных в объекты класса Film + */ + public FilmDbStorage(NamedParameterJdbcTemplate jdbc, FilmRowMapper filmMapper, + FilmGenreRowMapper filmGenreRowMapper) { + this.jdbc = jdbc; + this.filmMapper = filmMapper; + this.filmGenreRowMapper = filmGenreRowMapper; + } + + /** + * Добавление информации о фильме + * + * @param newFilm - объект для добавления + * @return - подтвержденный объект + */ + @Override + public Film addNewFilm(Film newFilm) { + GeneratedKeyHolder generatedKeyHolder = new GeneratedKeyHolder(); + + // сохраняем информацию о фильме в базу данных + try { + jdbc.update(SQL_INSERT_FILM, + new MapSqlParameterSource() + .addValue("name", newFilm.getName()) + .addValue("description", newFilm.getDescription()) + .addValue("releasedate", newFilm.getReleaseDate(), Types.DATE) + .addValue("len_min", newFilm.getDuration()) + .addValue("mpa_id", newFilm.getMpa().getId()), + generatedKeyHolder + ); + } catch (DataAccessException e) { + throw new NotFoundException("Получены недопустимые параметры запроса: " + + e.getMessage()); + } + + // получаем идентификатор фильма + final Integer filmId = generatedKeyHolder.getKey().intValue(); + newFilm.setId(filmId); + + // добавляем жанры Фильма Если определены + if (!newFilm.getGenres().isEmpty()) { + SqlParameterSource[] batch = newFilm.getGenres().stream() + .map(genre -> new MapSqlParameterSource() + .addValue("film_id", filmId) + .addValue("genre_id", genre.getId())) + .toArray(SqlParameterSource[]::new); + jdbc.batchUpdate(SQL_UPDATE_GENRES, batch); + } + + return newFilm; + } + + /** + * Поиск фильма по идентификатору + * + * @param id - идентификатор фильма + * @return - объект описания фильма + */ + @Override + public Optional getFilmById(Integer id) { + try { + Film film = jdbc.query(SQL_FIND_FILM_BY_ID, + new MapSqlParameterSource() + .addValue("id", id), + new ResultSetExtractor() { + @Override + public Film extractData(ResultSet rs) throws SQLException, DataAccessException { + rs.next(); + Film filmRs = filmMapper.mapRow(rs, 1); + Integer mpaId = rs.getInt("mpa_id"); + if(mpaId != null) { + Mpa mpa = new Mpa(); + mpa.setId(mpaId); + mpa.setName(rs.getString("mpa_name")); + filmRs.setMpa(mpa); + } + do { + Integer genreId = rs.getInt("genre_id"); + if (genreId != null) { + Genre genre = new Genre(); + genre.setId(genreId); + genre.setName(rs.getString("genre_name")); + filmRs.addGenre(genre); + } + } while (rs.next()); + return filmRs; + } + } + ); + return Optional.ofNullable(film); + + } catch (EmptyResultDataAccessException ignored) { + return Optional.empty(); + } + } + + /** + * Поиск всех фильмов + * + * @return - список фильмов + */ + @Override + public Collection findAllFilms() { + try { + // Загружаем из базы данных все фильмы + Map filmsMap = new HashMap<>(); + filmsMap = jdbc.query("SELECT f.*, mpa.name as mpa_name FROM films AS f INNER JOIN mpa ON f.mpa_id = mpa.id", + new ResultSetExtractor>() { + @Override + public Map extractData(ResultSet rs) + throws SQLException, DataAccessException { + Map fMap = new HashMap<>(); + while (rs.next()) { + Film film = filmMapper.mapRow(rs, 1); + Integer mpaId = rs.getInt("mpa_id"); + if(mpaId != null) { + Mpa mpa = new Mpa(); + mpa.setId(mpaId); + mpa.setName(rs.getString("mpa_name")); + film.setMpa(mpa); + } + fMap.put(film.getId(), film); + } + return fMap; + } + }); + + // загружаем из базы данных все ссылки на жанры + List filmsGenres; + filmsGenres = jdbc.query( + "SELECT fg.*, g.name AS genre_name FROM films_genres AS fg INNER JOIN genres AS g ON fg.GENRE_ID = g.ID", + filmGenreRowMapper); + + // пополням фильмы сведениями о жанрах + for (FilmGenre filmGenre : filmsGenres) { + Genre genre = new Genre(); + int filmId = filmGenre.getFilmId(); + genre.setId(filmGenre.getGenreId()); + genre.setName(filmGenre.getGenreName()); + filmsMap.get(filmId).addGenre(genre); + } + + return filmsMap.values(); + + } catch (EmptyResultDataAccessException ignored) { + return List.of(); + } + + } + + /** + * Поиск популярных фильмов + * + * @param count + * @return + */ + @Override + public Collection findPopularFilms(int count) { + String sql = "SELECT f.*, mpa.name AS mpa_name, popular.count_film\n" + + "FROM (films AS f INNER JOIN mpa ON f.MPA_ID = mpa.ID\n)" + + " LEFT OUTER JOIN\n" + + " (SELECT film_id, count(film_id) as count_film\n" + + " FROM LIKES GROUP BY film_id) AS popular\n" + + " ON f.id = popular.film_id\n" + + "ORDER BY popular.count_film DESC\n" + + "LIMIT :count"; + + Map popularFilms; + try { + popularFilms = jdbc.query(sql, new MapSqlParameterSource() + .addValue("count", count), + new ResultSetExtractor>() { + @Override + public Map extractData(ResultSet rs) + throws SQLException, DataAccessException { + Map mapFilm = new LinkedHashMap<>(); + while (rs.next()) { + Film film = filmMapper.mapRow(rs, 1); + Integer mpaId = rs.getInt("mpa_id"); + if(mpaId != null) { + Mpa mpa = new Mpa(); + mpa.setId(mpaId); + mpa.setName(rs.getString("mpa_name")); + film.setMpa(mpa); + } + mapFilm.put(film.getId(), film); + } + return mapFilm; + } + }); + + // загружаем из базы данных соответствующие ссылки на жанры + List ids = new ArrayList<>(popularFilms.keySet()); + List filmsGenres; + filmsGenres = jdbc.query( + "SELECT fg.*, " + + "g.name AS genre_name " + + "FROM films_genres AS fg INNER JOIN genres AS g ON fg.genre_id = g.id " + + "WHERE fg.film_id IN (:values)", + new MapSqlParameterSource() + .addValue("values", ids), + filmGenreRowMapper); + + // пополням фильмы сведениями о жанрах + for (FilmGenre filmGenre : filmsGenres) { + Genre genre = new Genre(); + int filmId = filmGenre.getFilmId(); + genre.setId(filmGenre.getGenreId()); + genre.setName(filmGenre.getGenreName()); + popularFilms.get(filmId).addGenre(genre); + } + return popularFilms.values(); + + } catch (EmptyResultDataAccessException ignored) { + return List.of(); + } + } + + /** + * Обновление сведений о фильме + * + * @param updFilm - обновленный объект + */ + @Override + public void updateFilm(Film updFilm) { + // задаем параметры SQL запоса + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("name", updFilm.getName()); + params.addValue("description", updFilm.getDescription()); + params.addValue("releasedate", updFilm.getReleaseDate(), Types.DATE); + params.addValue("len_min", updFilm.getDuration()); + params.addValue("mpa_id", updFilm.getMpa().getId()); + params.addValue("id", updFilm.getId()); + + // обновляем информацию о фильме + int rowsUpdated = jdbc.update(SQL_UPDATE_FILM, params); + if (rowsUpdated == 0) { + throw new InternalServerException("Не удалось обновить информацию о фильие"); + } + + // Удаляем все жанры которые были определены для фильма + int filmId = updFilm.getId(); + jdbc.update("DELETE FROM films_genres WHERE film_id = :filmId", + new MapSqlParameterSource() + .addValue("filmId", filmId)); + + // добавляем жанры Фильма если определены новые + if (updFilm.getGenres().size() > 0) { + SqlParameterSource[] batch = updFilm.getGenres().stream() + .map(genre -> new MapSqlParameterSource() + .addValue("film_id", updFilm.getId()) + .addValue("genre_id", genre.getId())) + .toArray(SqlParameterSource[]::new); + jdbc.batchUpdate(SQL_UPDATE_GENRES, batch); + } + } + + /** + * Добавление "лайка" к фильму. + * + * @param filmId - идентификатор фильма + * @param userId - идентификатор пользователя + * @return - число "лайков" + */ + @Override + public Integer addNewLike(Integer filmId, Integer userId) { + int rowsUpdated = jdbc.update(SQL_ADD_LIKE, new MapSqlParameterSource() + .addValue("userId", userId) + .addValue("filmId", filmId) + ); + if (rowsUpdated == 0) { + throw new InternalServerException("Не удалось обновить данные"); + } + return getFilmRank(filmId); + } + + /** + * Удаление "лайка" у фильма. + * + * @param filmId - идентификатор фильма + * @param userId - идентификатор пользователя + * @return - число "лайков" + */ + @Override + public Integer removeLike(Integer filmId, Integer userId) { + jdbc.update(SQL_REMOVE_LIKE, new MapSqlParameterSource() + .addValue("userId", userId) + .addValue("filmId", filmId) + ); + return getFilmRank(filmId); + } + + /** + * Подсчет "лайков" фильма. + * + * @param filmId - идентификатор фильма + * @return - число "лайков" + */ + @Override + public Integer getFilmRank(Integer filmId) { + try { + return jdbc.queryForObject("SELECT count(film_id) FROM likes WHERE film_id = :filmId", + new MapSqlParameterSource() + .addValue("filmId", filmId), + Integer.class); + } catch (EmptyResultDataAccessException ignored) { + throw new NotFoundException("Информация о популярности фильма не найдена. id:" + filmId); + } + } + + @Override + public void removeAllFilms() { + jdbc.update("DELETE FROM likes", new MapSqlParameterSource() + .addValue("table", "likes")); + jdbc.update("DELETE FROM films_genres", new MapSqlParameterSource() + .addValue("table", "films_genres")); + jdbc.update("DELETE FROM films", new MapSqlParameterSource() + .addValue("table", "films")); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java index f6bde3d..143df61 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java @@ -1,11 +1,12 @@ package ru.yandex.practicum.filmorate.storage.film; import org.springframework.stereotype.Component; +import org.springframework.stereotype.Repository; import ru.yandex.practicum.filmorate.model.Film; import java.util.*; -@Component +@Repository("inMemoryFilmStorage") public class InMemoryFilmStorage implements FilmStorage { private final Map films = new HashMap<>(); diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/genre/GenreDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/genre/GenreDbStorage.java new file mode 100644 index 0000000..9260271 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/genre/GenreDbStorage.java @@ -0,0 +1,60 @@ +package ru.yandex.practicum.filmorate.storage.genre; + +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.mapper.GenreRowMapper; +import ru.yandex.practicum.filmorate.model.Genre; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +@Repository +public class GenreDbStorage implements GenreStorage { + + private static final String SQL_GET_ALL_GENRES = "SELECT * FROM genres"; + private static final String SQL_GET_GENRE = "SELECT * FROM genres WHERE id = :id"; + + private final NamedParameterJdbcTemplate jdbc; + private final GenreRowMapper mapper; + + public GenreDbStorage(NamedParameterJdbcTemplate jdbc, GenreRowMapper mapper) { + this.jdbc = jdbc; + this.mapper = mapper; + } + + /** + * Чтение всех жанров в справочнике + * + * @return + */ + @Override + public Collection getAllGenres() { + try { + return jdbc.query(SQL_GET_ALL_GENRES, mapper); + } catch (EmptyResultDataAccessException ignored) { + return List.of(); + } + } + + /** + * чтение жанра по идентификатору + * + * @param id - идентификатор жанра + * @return - объект Optional + */ + @Override + public Optional getGenre(Integer id) { + try { + Genre genre = jdbc.queryForObject(SQL_GET_GENRE, + new MapSqlParameterSource() + .addValue("id", id), + mapper); + return Optional.ofNullable(genre); + } catch (EmptyResultDataAccessException ignored) { + return Optional.empty(); + } + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/genre/GenreStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/genre/GenreStorage.java new file mode 100644 index 0000000..f873aa8 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/genre/GenreStorage.java @@ -0,0 +1,11 @@ +package ru.yandex.practicum.filmorate.storage.genre; + +import ru.yandex.practicum.filmorate.model.Genre; + +import java.util.Collection; +import java.util.Optional; + +public interface GenreStorage { + Collection getAllGenres(); + Optional getGenre(Integer id); +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/mpa/MpaDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/mpa/MpaDbStorage.java new file mode 100644 index 0000000..f48c243 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/mpa/MpaDbStorage.java @@ -0,0 +1,48 @@ +package ru.yandex.practicum.filmorate.storage.mpa; + +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.mapper.MpaRowMapper; +import ru.yandex.practicum.filmorate.model.Mpa; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +@Repository +public class MpaDbStorage implements MpaStorage { + private static final String SQL_GET_ALL_MPA = "SELECT * FROM mpa"; + private static final String SQL_GET_MPA = "SELECT * FROM mpa WHERE id = :id"; + + private final NamedParameterJdbcTemplate jdbc; + private final MpaRowMapper mapper; + + public MpaDbStorage(NamedParameterJdbcTemplate jdbc, MpaRowMapper mapper) { + this.jdbc = jdbc; + this.mapper = mapper; + } + + @Override + public Collection findAllMpa() { + try { + return jdbc.query(SQL_GET_ALL_MPA, mapper); + } catch (EmptyResultDataAccessException ignored) { + return List.of(); + } + } + + @Override + public Optional findMpa(Integer id) { + try { + Mpa mpa = jdbc.queryForObject(SQL_GET_MPA, + new MapSqlParameterSource() + .addValue("id", id), + mapper); + return Optional.ofNullable(mpa); + } catch (EmptyResultDataAccessException ignored) { + return Optional.empty(); + } + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/mpa/MpaStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/mpa/MpaStorage.java new file mode 100644 index 0000000..a009238 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/mpa/MpaStorage.java @@ -0,0 +1,11 @@ +package ru.yandex.practicum.filmorate.storage.mpa; + +import ru.yandex.practicum.filmorate.model.Mpa; + +import java.util.Collection; +import java.util.Optional; + +public interface MpaStorage { + Collection findAllMpa(); + Optional findMpa(Integer id); +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java index dcfae97..034a8a1 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java @@ -1,12 +1,12 @@ package ru.yandex.practicum.filmorate.storage.user; -import lombok.extern.slf4j.Slf4j; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.jdbc.support.GeneratedKeyHolder; import org.springframework.stereotype.Repository; import ru.yandex.practicum.filmorate.exception.InternalServerException; +import ru.yandex.practicum.filmorate.mapper.UserRowMapper; import ru.yandex.practicum.filmorate.model.User; import java.sql.Types; @@ -14,7 +14,6 @@ import java.util.List; import java.util.Optional; -@Slf4j @Repository("userDbStorage") public class UserDbStorage implements UserStorage { private static final String SQL_INSERT_USER = "INSERT INTO users (email, login, name, birthday) VALUES (:email, :login, :name, :birthday)"; @@ -117,9 +116,12 @@ public void updateUser(User updUser) { */ @Override public void removeAllUsers() { - jdbc.update(SQL_DELETE_USERS, new MapSqlParameterSource() - .addValue("id", 0) - ); + jdbc.update("DELETE FROM likes", new MapSqlParameterSource() + .addValue("table", "likes")); + jdbc.update("DELETE FROM friends", new MapSqlParameterSource() + .addValue("table", "friends")); + jdbc.update("DELETE FROM users", new MapSqlParameterSource() + .addValue("table", "users")); } /** diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index 555ada7..4af8e5e 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -1,16 +1,15 @@ -- Заполняем справочник жанров MERGE INTO genres (id, name) - VALUES ( 1, 'Комедия.'), - (2, 'Драма.'), - (3, 'Драма.'), - (4, 'Мультфильм.'), - (5, 'Триллер.'), - (6, 'Документальный.'), - (7, 'Боевик.'); + VALUES ( 1, 'Комедия'), + (2, 'Драма'), + (3, 'Мультфильм'), + (4, 'Триллер'), + (5, 'Документальный'), + (6, 'Боевик'); -- Заполняем справочник рейтингов MPA -MERGE INTO MPA (id, code, description) - VALUES ( 1, 'G', 'у фильма нет возрастных ограничений'), +MERGE INTO MPA (id, name, description) + VALUES (1, 'G', 'у фильма нет возрастных ограничений'), (2, 'PG', 'детям рекомендуется смотреть фильм с родителями'), (3, 'PG-13', 'детям до 13 лет просмотр не желателен'), (4, 'R', 'лицам до 17 лет просматривать фильм можно только в присутствии взрослого'), diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index e2d1aa6..3724252 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -24,7 +24,7 @@ CREATE TABLE IF NOT EXISTS genres ( -- Создаем справочник рейтинга MPA CREATE TABLE IF NOT EXISTS MPA ( id INTEGER PRIMARY KEY, - code VARCHAR(8) NOT NULL, + name VARCHAR(8) NOT NULL, description VARCHAR(80) ); From 1b7aecb420ee1c836271170d75babbdbfce8baf4 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Sun, 16 Feb 2025 20:39:20 +0700 Subject: [PATCH 10/19] Postman - OK --- pom.xml | 2 +- schema.png | Bin 49447 -> 50388 bytes .../practicum/filmorate/model/Film.java | 3 +- .../filmorate/service/FilmService.java | 9 +++-- .../filmorate/service/GenreService.java | 6 ++-- .../filmorate/storage/film/FilmDbStorage.java | 11 +++--- .../storage/genre/GenreDbStorage.java | 4 +-- .../filmorate/storage/genre/GenreStorage.java | 4 +-- .../filmorate/storage/user/UserDbStorage.java | 12 +++++-- src/main/resources/application.properties | 3 +- .../storage/user/UserDbStorageTest.java | 33 ++++++++++++++++++ src/test/resources/application.properties | 8 +++++ src/test/resources/test-data.sql | 3 ++ 13 files changed, 75 insertions(+), 23 deletions(-) create mode 100644 src/test/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorageTest.java create mode 100644 src/test/resources/application.properties create mode 100644 src/test/resources/test-data.sql diff --git a/pom.xml b/pom.xml index eb4b913..d88bb46 100644 --- a/pom.xml +++ b/pom.xml @@ -49,7 +49,7 @@ org.springframework.boot spring-boot-starter-test - test + 3.4.2 diff --git a/schema.png b/schema.png index 31ffa589147858b4139a85a8a7180766375eae8f..5263bd649ca7e8b20f2e7bffaf172515a5aa0401 100644 GIT binary patch literal 50388 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 jOh-Fg*Z)7k{3%_SPC3enyt existingFilm = films.findAllFilms().stream() + .filter(film1 -> film1.equals(film)) + .findFirst(); + if (existingFilm.isPresent()) { + throw new ValidationException("Фильм уже существует: " + existingFilm.get()); } return films.addNewFilm(film); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/GenreService.java b/src/main/java/ru/yandex/practicum/filmorate/service/GenreService.java index d40c2fc..d51c661 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/GenreService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/GenreService.java @@ -16,11 +16,11 @@ public GenreService(GenreStorage genereStorage) { } public Collection getAllGenres() { - return genereStorage.getAllGenres(); + return genereStorage.findAllGenres(); } public Genre getGenreById(int id) { - return genereStorage.getGenre(id).orElseThrow(() -> - new NotFoundException("Не найден фильм id=" + id)); + return genereStorage.findGenre(id).orElseThrow(() -> + new NotFoundException("Не найден жанр id=" + id)); } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java index 16f052c..cad5336 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java @@ -97,7 +97,8 @@ public Film addNewFilm(Film newFilm) { jdbc.batchUpdate(SQL_UPDATE_GENRES, batch); } - return newFilm; + return getFilmById(filmId).orElseThrow(() -> + new InternalServerException("Ошибка при добавлении фильма.")); } /** @@ -126,7 +127,7 @@ public Film extractData(ResultSet rs) throws SQLException, DataAccessException { } do { Integer genreId = rs.getInt("genre_id"); - if (genreId != null) { + if (genreId != 0) { Genre genre = new Genre(); genre.setId(genreId); genre.setName(rs.getString("genre_name")); @@ -139,7 +140,7 @@ public Film extractData(ResultSet rs) throws SQLException, DataAccessException { ); return Optional.ofNullable(film); - } catch (EmptyResultDataAccessException ignored) { + } catch (DataAccessException ignored) { return Optional.empty(); } } @@ -163,7 +164,7 @@ public Map extractData(ResultSet rs) while (rs.next()) { Film film = filmMapper.mapRow(rs, 1); Integer mpaId = rs.getInt("mpa_id"); - if(mpaId != null) { + if(mpaId != 0) { Mpa mpa = new Mpa(); mpa.setId(mpaId); mpa.setName(rs.getString("mpa_name")); @@ -227,7 +228,7 @@ public Map extractData(ResultSet rs) while (rs.next()) { Film film = filmMapper.mapRow(rs, 1); Integer mpaId = rs.getInt("mpa_id"); - if(mpaId != null) { + if(mpaId != 0) { Mpa mpa = new Mpa(); mpa.setId(mpaId); mpa.setName(rs.getString("mpa_name")); diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/genre/GenreDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/genre/GenreDbStorage.java index 9260271..86e1333 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/genre/GenreDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/genre/GenreDbStorage.java @@ -31,7 +31,7 @@ public GenreDbStorage(NamedParameterJdbcTemplate jdbc, GenreRowMapper mapper) { * @return */ @Override - public Collection getAllGenres() { + public Collection findAllGenres() { try { return jdbc.query(SQL_GET_ALL_GENRES, mapper); } catch (EmptyResultDataAccessException ignored) { @@ -46,7 +46,7 @@ public Collection getAllGenres() { * @return - объект Optional */ @Override - public Optional getGenre(Integer id) { + public Optional findGenre(Integer id) { try { Genre genre = jdbc.queryForObject(SQL_GET_GENRE, new MapSqlParameterSource() diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/genre/GenreStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/genre/GenreStorage.java index f873aa8..1d0a804 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/genre/GenreStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/genre/GenreStorage.java @@ -6,6 +6,6 @@ import java.util.Optional; public interface GenreStorage { - Collection getAllGenres(); - Optional getGenre(Integer id); + Collection findAllGenres(); + Optional findGenre(Integer id); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java index 034a8a1..035f69b 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java @@ -1,5 +1,6 @@ package ru.yandex.practicum.filmorate.storage.user; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; @@ -24,14 +25,19 @@ public class UserDbStorage implements UserStorage { private static final String SQL_ADD_FRIEND = "MERGE INTO friends (user_id, friend_id, confirmed) VALUES (:userId, :friendId, FALSE)"; private static final String SQL_REMOVE_FRIEND = "DELETE FROM friends WHERE (user_id = :userId) AND (friend_id = :friendId)"; + @Autowired + private NamedParameterJdbcTemplate jdbc; - private final NamedParameterJdbcTemplate jdbc; - private final UserRowMapper mapper; + @Autowired + private UserRowMapper mapper; - public UserDbStorage(NamedParameterJdbcTemplate jdbc, UserRowMapper mapper) { + /* + public UserDbStorage(NamedParameterJdbcTemplate jdbc, + UserRowMapper mapper) { this.jdbc = jdbc; this.mapper = mapper; } +*/ /** * Добавление в базу нового пользователя diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index bfe0776..22f0136 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,8 +1,7 @@ server.port=8080 -logging.level.org.zalando.logbook: TRACE +logging.level.org.zalando.logbook: TRACE 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/test/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorageTest.java new file mode 100644 index 0000000..d23470d --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorageTest.java @@ -0,0 +1,33 @@ +package ru.yandex.practicum.filmorate.storage.user; + +import lombok.RequiredArgsConstructor; +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 java.util.Optional; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@JdbcTest +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@Import({UserDbStorage.class}) +class UserDbStorageTest { + public static final int TEST_USER_ID = 1; + private final UserDbStorage userDbStorage; + + @Test + void getUserById() { + Optional userOptional = userDbStorage.getUserById(TEST_USER_ID); + + assertThat(userOptional) + .isPresent() + .hasValueSatisfying(user -> + assertThat(user).hasFieldOrPropertyWithValue("id", 1) + ); + } + +} \ No newline at end of file diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties new file mode 100644 index 0000000..bfe0776 --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1,8 @@ +server.port=8080 +logging.level.org.zalando.logbook: TRACE + +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/test/resources/test-data.sql b/src/test/resources/test-data.sql new file mode 100644 index 0000000..9841e1a --- /dev/null +++ b/src/test/resources/test-data.sql @@ -0,0 +1,3 @@ +INSERT INTO users (email, login, name, birthday) +VALUES ( 'test@test.com', 'testLogin', 'testName', '2001-9-22' ); + From cdc56f14ba4055bcbe66006f57ed6cf7a547936c Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Wed, 19 Feb 2025 00:15:01 +0700 Subject: [PATCH 11/19] =?UTF-8?q?=D0=9F=D0=B8=D1=88=D0=B5=D0=BC=20=D1=82?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D1=8B=201?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 13 ++--- .../filmorate/controller/UserController.java | 6 +++ .../filmorate/service/UserService.java | 6 +++ .../storage/user/InMemoryUserStorage.java | 4 ++ .../filmorate/storage/user/UserDbStorage.java | 39 ++++++-------- .../filmorate/storage/user/UserStorage.java | 2 + src/main/resources/application.properties | 1 + .../storage/user/UserDbStorageTest.java | 13 +++-- src/test/resources/application.properties | 5 +- src/test/resources/application.yaml | 7 +++ src/test/resources/test-data.sql | 17 ++++++ src/test/resources/test-schema.sql | 53 +++++++++++++++++++ 12 files changed, 129 insertions(+), 37 deletions(-) create mode 100644 src/test/resources/application.yaml create mode 100644 src/test/resources/test-schema.sql diff --git a/pom.xml b/pom.xml index d88bb46..a4c8f5c 100644 --- a/pom.xml +++ b/pom.xml @@ -17,6 +17,7 @@ 21 + com.google.code.gson gson @@ -28,18 +29,18 @@ provided - - org.springframework.boot - spring-boot-starter-web - 3.4.1 - - com.h2database h2 2.3.232 + + org.springframework.boot + spring-boot-starter-web + 3.4.1 + + org.springframework.boot spring-boot-starter-jdbc 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 51e2f8d..1c4b264 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java @@ -147,4 +147,10 @@ public String deleteAllUsers() { return service.removeAllUsers(); } + + @GetMapping("/dbinfo") + public String getDbInfo() { + return service.getDdInfo(); + } + } 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 ad4ad1f..d690787 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java @@ -6,6 +6,7 @@ import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.exception.ValidationException; import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.storage.user.UserDbStorage; import ru.yandex.practicum.filmorate.storage.user.UserStorage; import java.util.Collection; @@ -166,4 +167,9 @@ public Collection getCommonFriends(Integer id1, Integer id2) { return users.getCommonFriends(id1, id2); } + + public String getDdInfo() { + return users.getDbInfo(); + } + } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java index 9d3cf13..ecb2c11 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java @@ -74,4 +74,8 @@ public Collection getCommonFriends(Integer id1, Integer id2) { } return dtoFriends; } + + public String getDbInfo() { + return "*"; + } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java index 035f69b..748d965 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java @@ -17,27 +17,18 @@ @Repository("userDbStorage") public class UserDbStorage implements UserStorage { + /* private static final String SQL_INSERT_USER = "INSERT INTO users (email, login, name, birthday) VALUES (:email, :login, :name, :birthday)"; private static final String SQL_UPDATE_USER = "UPDATE users SET email = :email, login = :login, name = :name, birthday = :birthday WHERE id = :id"; private static final String SQL_FIND_USER = "SELECT * FROM users WHERE id = :id"; - private static final String SQL_FIND_ALL_USERS = "SELECT * FROM users"; private static final String SQL_DELETE_USERS = "DELETE FROM users WHERE id <> :id"; private static final String SQL_ADD_FRIEND = "MERGE INTO friends (user_id, friend_id, confirmed) VALUES (:userId, :friendId, FALSE)"; private static final String SQL_REMOVE_FRIEND = "DELETE FROM friends WHERE (user_id = :userId) AND (friend_id = :friendId)"; - @Autowired - private NamedParameterJdbcTemplate jdbc; + */ @Autowired - private UserRowMapper mapper; - - /* - public UserDbStorage(NamedParameterJdbcTemplate jdbc, - UserRowMapper mapper) { - this.jdbc = jdbc; - this.mapper = mapper; - } -*/ + private NamedParameterJdbcTemplate jdbc; /** * Добавление в базу нового пользователя @@ -50,7 +41,7 @@ public User addNewUser(User newUser) { // для доступа к сгенерированому ключу новой записи создаем объект GeneratedKeyHolder GeneratedKeyHolder generatedKeyHolder = new GeneratedKeyHolder(); - jdbc.update(SQL_INSERT_USER, + jdbc.update("INSERT INTO users (email, login, name, birthday) VALUES (:email, :login, :name, :birthday)", new MapSqlParameterSource() .addValue("email", newUser.getEmail()) .addValue("login", newUser.getLogin()) @@ -73,10 +64,10 @@ public User addNewUser(User newUser) { @Override public Optional getUserById(Integer id) { try { - User user = jdbc.queryForObject(SQL_FIND_USER, + User user = jdbc.queryForObject("SELECT * FROM users WHERE id = :id", new MapSqlParameterSource() .addValue("id", id), - mapper); + new UserRowMapper()); return Optional.ofNullable(user); } catch (EmptyResultDataAccessException ignored) { return Optional.empty(); @@ -91,7 +82,7 @@ public Optional getUserById(Integer id) { @Override public Collection findAllUsers() { try { - return jdbc.query(SQL_FIND_ALL_USERS, mapper); + return jdbc.query("SELECT * FROM users", new UserRowMapper()); } catch (EmptyResultDataAccessException ignored) { return List.of(); } @@ -111,7 +102,7 @@ public void updateUser(User updUser) { params.addValue("birthday", updUser.getBirthday(), Types.DATE); params.addValue("id", updUser.getId()); - int rowsUpdated = jdbc.update(SQL_UPDATE_USER, params); + int rowsUpdated = jdbc.update("UPDATE users SET email = :email, login = :login, name = :name, birthday = :birthday WHERE id = :id", params); if (rowsUpdated == 0) { throw new InternalServerException("Не удалось обновить данные"); } @@ -138,7 +129,7 @@ public void removeAllUsers() { */ @Override public void addFriend(Integer userId, Integer friendId) { - jdbc.update(SQL_ADD_FRIEND, new MapSqlParameterSource() + jdbc.update("MERGE INTO friends (user_id, friend_id, confirmed) VALUES (:userId, :friendId, FALSE)", new MapSqlParameterSource() .addValue("userId", userId) .addValue("friendId", friendId) ); @@ -152,7 +143,7 @@ public void addFriend(Integer userId, Integer friendId) { */ @Override public void breakUpFriends(Integer userId, Integer friendsId) { - jdbc.update(SQL_REMOVE_FRIEND, new MapSqlParameterSource() + jdbc.update("DELETE FROM friends WHERE (user_id = :userId) AND (friend_id = :friendId)", new MapSqlParameterSource() .addValue("userId", userId) .addValue("friendId", friendsId) ); @@ -171,7 +162,7 @@ public Collection getUserFriends(Integer userId) { try { return jdbc.query(sql, new MapSqlParameterSource() .addValue("userId", userId), - mapper + new UserRowMapper() ); } catch (EmptyResultDataAccessException ignored) { return List.of(); @@ -195,10 +186,14 @@ public Collection getCommonFriends(Integer id1, Integer id2) { return jdbc.query(sql, new MapSqlParameterSource() .addValue("userId1", id1) .addValue("userId2", id2), - mapper - ); + new UserRowMapper()); } catch (EmptyResultDataAccessException ignored) { return List.of(); } } + + @Override + public String getDbInfo() { + return jdbc.getJdbcTemplate().getDataSource().toString(); + } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java index 094492d..4e9d221 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java @@ -26,4 +26,6 @@ public interface UserStorage { Collection getUserFriends(Integer userId); Collection getCommonFriends(Integer id1, Integer id2); + + String getDbInfo(); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 22f0136..bacffeb 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,3 +1,4 @@ +spring.sql.init.mode=always server.port=8080 logging.level.org.zalando.logbook: TRACE diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorageTest.java index d23470d..87f5935 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorageTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorageTest.java @@ -5,24 +5,27 @@ 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.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; import ru.yandex.practicum.filmorate.model.User; import java.util.Optional; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; - @JdbcTest @AutoConfigureTestDatabase -@RequiredArgsConstructor(onConstructor_ = @Autowired) +// @RequiredArgsConstructor(onConstructor_ = @Autowired) @Import({UserDbStorage.class}) class UserDbStorageTest { - public static final int TEST_USER_ID = 1; - private final UserDbStorage userDbStorage; + // public static final int TEST_USER_ID = 1; + + @Autowired + private UserDbStorage userDbStorage; @Test void getUserById() { - Optional userOptional = userDbStorage.getUserById(TEST_USER_ID); + Optional userOptional = userDbStorage.getUserById(1); + System.out.println("-----" + userDbStorage.getDbInfo()); assertThat(userOptional) .isPresent() .hasValueSatisfying(user -> diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index bfe0776..f0e14c4 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -1,8 +1,5 @@ -server.port=8080 -logging.level.org.zalando.logbook: TRACE - +spring.sql.init.mode=always 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/test/resources/application.yaml b/src/test/resources/application.yaml new file mode 100644 index 0000000..8ed081e --- /dev/null +++ b/src/test/resources/application.yaml @@ -0,0 +1,7 @@ +spring: + sql.init.mode: "always" + datasource: + url: "jdbc:h2:file:./db/filmorate" + driverClassName: "org.h2.Driver" + username: "sa" + password: "password" diff --git a/src/test/resources/test-data.sql b/src/test/resources/test-data.sql index 9841e1a..0d2dd27 100644 --- a/src/test/resources/test-data.sql +++ b/src/test/resources/test-data.sql @@ -1,3 +1,20 @@ +-- Заполняем справочник жанров +MERGE INTO genres (id, name) + VALUES ( 1, 'Комедия'), + (2, 'Драма'), + (3, 'Мультфильм'), + (4, 'Триллер'), + (5, 'Документальный'), + (6, 'Боевик'); + +-- Заполняем справочник рейтингов MPA +MERGE INTO MPA (id, name, description) + VALUES (1, 'G', 'у фильма нет возрастных ограничений'), + (2, 'PG', 'детям рекомендуется смотреть фильм с родителями'), + (3, 'PG-13', 'детям до 13 лет просмотр не желателен'), + (4, 'R', 'лицам до 17 лет просматривать фильм можно только в присутствии взрослого'), + (5, 'NC-17', 'лицам до 18 лет просмотр запрещён'); + INSERT INTO users (email, login, name, birthday) VALUES ( 'test@test.com', 'testLogin', 'testName', '2001-9-22' ); diff --git a/src/test/resources/test-schema.sql b/src/test/resources/test-schema.sql new file mode 100644 index 0000000..3724252 --- /dev/null +++ b/src/test/resources/test-schema.sql @@ -0,0 +1,53 @@ +-- Создаем таблицу пользователей +CREATE TABLE IF NOT EXISTS users ( + id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + email VARCHAR(255) NOT NULL, + login VARCHAR(40) NOT NULL, + name VARCHAR(40) NOT NULL, + birthday DATE NOT NULL + ); + +-- Создаем таблицу друзей +CREATE TABLE IF NOT EXISTS friends ( + user_id INTEGER NOT NULL REFERENCES users(id), + friend_id INTEGER NOT NULL REFERENCES users(id), + confirmed BOOLEAN NOT NULL DEFAULT FALSE, + PRIMARY KEY (user_id, friend_id) + ); + +-- Создаем справочник жанров фильма +CREATE TABLE IF NOT EXISTS genres ( + id INTEGER PRIMARY KEY, + name VARCHAR(40) NOT NULL + ); + +-- Создаем справочник рейтинга MPA +CREATE TABLE IF NOT EXISTS MPA ( + id INTEGER PRIMARY KEY, + name VARCHAR(8) NOT NULL, + description VARCHAR(80) + ); + +-- Создаем таблицу описания фильма +CREATE TABLE IF NOT EXISTS films ( + id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name VARCHAR(40) NOT NULL, + description VARCHAR(200), + releaseDate DATE, + len_min INTEGER, + MPA_id INTEGER NOT NULL REFERENCES MPA(id) + ); + +-- Создаем таблицу описания жанра фильма +CREATE TABLE IF NOT EXISTS films_genres ( + film_id INTEGER NOT NULL REFERENCES films(id), + genre_id INTEGER NOT NULL REFERENCES genres(id), + PRIMARY KEY (film_id, genre_id) + ); + +-- Создаем таблицу "лайков" к фильмам +CREATE TABLE IF NOT EXISTS likes ( + user_id INTEGER NOT NULL REFERENCES users(id), + film_id INTEGER NOT NULL REFERENCES films(id), + PRIMARY KEY (user_id, film_id) + ); From ba38e62248725c965724c3b67ed3a07bc01d1ae9 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Wed, 19 Feb 2025 22:44:12 +0700 Subject: [PATCH 12/19] =?UTF-8?q?=D0=9F=D0=B8=D1=88=D0=B5=D0=BC=20=D1=82?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D1=8B=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../filmorate/controller/UserController.java | 6 -- .../filmorate/service/UserService.java | 4 -- .../storage/user/InMemoryUserStorage.java | 3 - .../filmorate/storage/user/UserDbStorage.java | 4 -- .../filmorate/storage/user/UserStorage.java | 2 - src/main/resources/application.properties | 6 +- .../storage/user/UserDbStorageTest.java | 59 ++++++++++++++++--- src/test/resources/application.properties | 3 + src/test/resources/application.yaml | 7 --- .../resources/{test-data.sql => data.sql} | 1 + .../resources/{test-schema.sql => schema.sql} | 0 11 files changed, 57 insertions(+), 38 deletions(-) delete mode 100644 src/test/resources/application.yaml rename src/test/resources/{test-data.sql => data.sql} (94%) rename src/test/resources/{test-schema.sql => schema.sql} (100%) 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 1c4b264..51e2f8d 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/UserController.java @@ -147,10 +147,4 @@ public String deleteAllUsers() { return service.removeAllUsers(); } - - @GetMapping("/dbinfo") - public String getDbInfo() { - return service.getDdInfo(); - } - } 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 d690787..e501188 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java @@ -168,8 +168,4 @@ public Collection getCommonFriends(Integer id1, Integer id2) { return users.getCommonFriends(id1, id2); } - public String getDdInfo() { - return users.getDbInfo(); - } - } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java index ecb2c11..564b438 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java @@ -75,7 +75,4 @@ public Collection getCommonFriends(Integer id1, Integer id2) { return dtoFriends; } - public String getDbInfo() { - return "*"; - } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java index 748d965..00160ce 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java @@ -192,8 +192,4 @@ public Collection getCommonFriends(Integer id1, Integer id2) { } } - @Override - public String getDbInfo() { - return jdbc.getJdbcTemplate().getDataSource().toString(); - } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java index 4e9d221..094492d 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserStorage.java @@ -26,6 +26,4 @@ public interface UserStorage { Collection getUserFriends(Integer userId); Collection getCommonFriends(Integer id1, Integer id2); - - String getDbInfo(); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index bacffeb..71a0274 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,8 +1,8 @@ spring.sql.init.mode=always -server.port=8080 -logging.level.org.zalando.logbook: TRACE - spring.datasource.url=jdbc:h2:file:./db/filmorate spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password=password + +server.port=8080 +logging.level.org.zalando.logbook: TRACE diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorageTest.java index 87f5935..d1b0619 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorageTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorageTest.java @@ -5,32 +5,73 @@ 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.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; import ru.yandex.practicum.filmorate.model.User; +import java.time.LocalDate; import java.util.Optional; + import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * Тестирование Хранилища пользователей в базе данных + */ @JdbcTest @AutoConfigureTestDatabase -// @RequiredArgsConstructor(onConstructor_ = @Autowired) +@RequiredArgsConstructor(onConstructor_ = @Autowired) @Import({UserDbStorage.class}) class UserDbStorageTest { - // public static final int TEST_USER_ID = 1; + private final UserDbStorage userDbStorage; + public static final int TEST_USER_ID = 1; - @Autowired - private UserDbStorage userDbStorage; + /** + * Генерация тестового пользователя + * + * @return - Optional информация о пользователе если найден + */ + static User getTestUser() { + User user = new User(); + user.setId(TEST_USER_ID); + user.setEmail("test@test.com"); + user.setLogin("testLogin"); + user.setName("testName"); + user.setBirthday(LocalDate.of(2001, 9, 22)); + return user; + } + /** + * Тест чтения информации о тестовом пользователе. + * Данные должны быть подготовлены заранее, + * при инициализации базы данных скриптом data.sql + */ @Test void getUserById() { + User user = getTestUser(); Optional userOptional = userDbStorage.getUserById(1); - System.out.println("-----" + userDbStorage.getDbInfo()); assertThat(userOptional) .isPresent() - .hasValueSatisfying(user -> - assertThat(user).hasFieldOrPropertyWithValue("id", 1) - ); + .get() + .usingRecursiveComparison() + .isEqualTo(user); + + } + + /** + * Тест добавления в базу данных нового пользователя + */ + @Test + void addNewUser() { + User user = new User(); + user.setEmail("test@user.test"); + user.setName("TesstUserName"); + user.setLogin("TestUserLogin"); + user.setBirthday(LocalDate.of(2001, 7, 22)); + + User userDb = userDbStorage.addNewUser(user); + assertNotNull(userDb, "UserDb should not be null"); + assertNotNull(userDb.getId(), "UserDb id should not be null"); } } \ No newline at end of file diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index f0e14c4..71a0274 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -3,3 +3,6 @@ spring.datasource.url=jdbc:h2:file:./db/filmorate spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password=password + +server.port=8080 +logging.level.org.zalando.logbook: TRACE diff --git a/src/test/resources/application.yaml b/src/test/resources/application.yaml deleted file mode 100644 index 8ed081e..0000000 --- a/src/test/resources/application.yaml +++ /dev/null @@ -1,7 +0,0 @@ -spring: - sql.init.mode: "always" - datasource: - url: "jdbc:h2:file:./db/filmorate" - driverClassName: "org.h2.Driver" - username: "sa" - password: "password" diff --git a/src/test/resources/test-data.sql b/src/test/resources/data.sql similarity index 94% rename from src/test/resources/test-data.sql rename to src/test/resources/data.sql index 0d2dd27..9f1f29f 100644 --- a/src/test/resources/test-data.sql +++ b/src/test/resources/data.sql @@ -15,6 +15,7 @@ MERGE INTO MPA (id, name, description) (4, 'R', 'лицам до 17 лет просматривать фильм можно только в присутствии взрослого'), (5, 'NC-17', 'лицам до 18 лет просмотр запрещён'); +-- Создаем тестового пользователя INSERT INTO users (email, login, name, birthday) VALUES ( 'test@test.com', 'testLogin', 'testName', '2001-9-22' ); diff --git a/src/test/resources/test-schema.sql b/src/test/resources/schema.sql similarity index 100% rename from src/test/resources/test-schema.sql rename to src/test/resources/schema.sql From cdf3648ae76a89adea78622cb9ac3cd668f3f22b Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Thu, 20 Feb 2025 22:29:21 +0700 Subject: [PATCH 13/19] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=20=D0=BA=D0=BB=D0=B0=D1=81=D1=81:=20Us?= =?UTF-8?q?erDbStorageTest.java?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../filmorate/storage/user/UserDbStorage.java | 17 +- .../storage/film/FilmDbStorageTest.java | 44 +++++ .../storage/user/UserDbStorageTest.java | 152 +++++++++++++++++- src/test/resources/data.sql | 7 +- 4 files changed, 202 insertions(+), 18 deletions(-) create mode 100644 src/test/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorageTest.java diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java index 00160ce..2763eb2 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java @@ -17,16 +17,13 @@ @Repository("userDbStorage") public class UserDbStorage implements UserStorage { - /* + private static final String SQL_INSERT_USER = "INSERT INTO users (email, login, name, birthday) VALUES (:email, :login, :name, :birthday)"; - private static final String SQL_UPDATE_USER = "UPDATE users SET email = :email, login = :login, name = :name, birthday = :birthday WHERE id = :id"; private static final String SQL_FIND_USER = "SELECT * FROM users WHERE id = :id"; - private static final String SQL_DELETE_USERS = "DELETE FROM users WHERE id <> :id"; + private static final String SQL_UPDATE_USER = "UPDATE users SET email = :email, login = :login, name = :name, birthday = :birthday WHERE id = :id"; private static final String SQL_ADD_FRIEND = "MERGE INTO friends (user_id, friend_id, confirmed) VALUES (:userId, :friendId, FALSE)"; private static final String SQL_REMOVE_FRIEND = "DELETE FROM friends WHERE (user_id = :userId) AND (friend_id = :friendId)"; - */ - @Autowired private NamedParameterJdbcTemplate jdbc; @@ -41,7 +38,7 @@ public User addNewUser(User newUser) { // для доступа к сгенерированому ключу новой записи создаем объект GeneratedKeyHolder GeneratedKeyHolder generatedKeyHolder = new GeneratedKeyHolder(); - jdbc.update("INSERT INTO users (email, login, name, birthday) VALUES (:email, :login, :name, :birthday)", + jdbc.update(SQL_INSERT_USER, new MapSqlParameterSource() .addValue("email", newUser.getEmail()) .addValue("login", newUser.getLogin()) @@ -64,7 +61,7 @@ public User addNewUser(User newUser) { @Override public Optional getUserById(Integer id) { try { - User user = jdbc.queryForObject("SELECT * FROM users WHERE id = :id", + User user = jdbc.queryForObject(SQL_FIND_USER, new MapSqlParameterSource() .addValue("id", id), new UserRowMapper()); @@ -102,7 +99,7 @@ public void updateUser(User updUser) { params.addValue("birthday", updUser.getBirthday(), Types.DATE); params.addValue("id", updUser.getId()); - int rowsUpdated = jdbc.update("UPDATE users SET email = :email, login = :login, name = :name, birthday = :birthday WHERE id = :id", params); + int rowsUpdated = jdbc.update(SQL_UPDATE_USER, params); if (rowsUpdated == 0) { throw new InternalServerException("Не удалось обновить данные"); } @@ -129,7 +126,7 @@ public void removeAllUsers() { */ @Override public void addFriend(Integer userId, Integer friendId) { - jdbc.update("MERGE INTO friends (user_id, friend_id, confirmed) VALUES (:userId, :friendId, FALSE)", new MapSqlParameterSource() + jdbc.update(SQL_ADD_FRIEND, new MapSqlParameterSource() .addValue("userId", userId) .addValue("friendId", friendId) ); @@ -143,7 +140,7 @@ public void addFriend(Integer userId, Integer friendId) { */ @Override public void breakUpFriends(Integer userId, Integer friendsId) { - jdbc.update("DELETE FROM friends WHERE (user_id = :userId) AND (friend_id = :friendId)", new MapSqlParameterSource() + jdbc.update(SQL_REMOVE_FRIEND, new MapSqlParameterSource() .addValue("userId", userId) .addValue("friendId", friendsId) ); diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorageTest.java new file mode 100644 index 0000000..23d04ff --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorageTest.java @@ -0,0 +1,44 @@ +package ru.yandex.practicum.filmorate.storage.film; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class FilmDbStorageTest { + + @Test + void addNewFilm() { + } + + @Test + void getFilmById() { + } + + @Test + void findAllFilms() { + } + + @Test + void findPopularFilms() { + } + + @Test + void updateFilm() { + } + + @Test + void addNewLike() { + } + + @Test + void removeLike() { + } + + @Test + void getFilmRank() { + } + + @Test + void removeAllFilms() { + } +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorageTest.java index d1b0619..8eac04b 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorageTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorageTest.java @@ -9,13 +9,19 @@ import ru.yandex.practicum.filmorate.model.User; import java.time.LocalDate; +import java.util.Collection; import java.util.Optional; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Тестирование Хранилища пользователей в базе данных + * + * Для успешного выполнения тестов, при инициализации базы данных + * должна быть подготовлена информация о четырех тестовых пользователях. + * Файл первоначальных данных ./src/test/resources/data.sql */ @JdbcTest @AutoConfigureTestDatabase @@ -41,9 +47,7 @@ static User getTestUser() { } /** - * Тест чтения информации о тестовом пользователе. - * Данные должны быть подготовлены заранее, - * при инициализации базы данных скриптом data.sql + * Чтение информации о тестовом пользователе по заданному идентификатору. */ @Test void getUserById() { @@ -55,7 +59,6 @@ void getUserById() { .get() .usingRecursiveComparison() .isEqualTo(user); - } /** @@ -70,8 +73,145 @@ void addNewUser() { user.setBirthday(LocalDate.of(2001, 7, 22)); User userDb = userDbStorage.addNewUser(user); - assertNotNull(userDb, "UserDb should not be null"); - assertNotNull(userDb.getId(), "UserDb id should not be null"); + assertNotNull(userDb.getId(), + "addNewUser() - При добавлении пользователя в базу должен быть присвоен не нулевой идентификатор"); + + Optional userOptional = userDbStorage.getUserById(userDb.getId()); + + assertThat(userOptional) + .isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(user); } + /** + * Тестирование поиска информации о всех пользователях в базе. + */ + @Test + void findAllUsers() { + Collection users = userDbStorage.findAllUsers(); + + assertTrue(users.size() > 0, + "findAllUsers() - В базе данных отсутствует информация о пользователях."); + } + + /** + * Тестирование обновления информации о тестовом пользователе. + */ + @Test + void updateUser() { + User userUpdate = getTestUser(); + userUpdate.setEmail("updated_user@test.com"); + + userDbStorage.updateUser(userUpdate); + + Optional userOptional = userDbStorage.getUserById(userUpdate.getId()); + + assertThat(userOptional) + .isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(userUpdate); + } + + /** + * Тестирование добавления "друга" + */ + @Test + void addFriend() { + final int userId = TEST_USER_ID; + final int friendId = 2; + + Optional userOptional = userDbStorage.getUserById(userId); + assertThat(userOptional) + .withFailMessage("addFriend() - Не определен пользователь id=%s для создания \"друзей\".", userId) + .isPresent(); + if (userOptional.isEmpty()) return; + + userOptional = userDbStorage.getUserById(friendId); + assertThat(userOptional) + .withFailMessage("addFriend() - Не определен пользователь id=%s в качестве нового \"друга\".", friendId) + .isPresent(); + if (userOptional.isEmpty()) return; + User friend = userOptional.get(); + + userDbStorage.addFriend(userId, friendId); + Collection friends = userDbStorage.getUserFriends(userId); + + assertTrue(friends.contains(friend), + "addFriend() - Пользователь id=" + userId + ". не удалось добавить друга id=" + friendId); + } + + /** + * Тестирование списка друзей заданного пользователя + */ + @Test + void getUserFriends() { + userDbStorage.addFriend(TEST_USER_ID, 2); + userDbStorage.addFriend(TEST_USER_ID, 3); + userDbStorage.addFriend(TEST_USER_ID, 4); + + Collection friends = userDbStorage.getUserFriends(TEST_USER_ID); + assertTrue(friends.size() > 0, + "getUserFriends() - Список друзй пользователя id=" + + TEST_USER_ID + " не найден."); + assertTrue(friends.size() == 3, + "getUserFriends() - Количество друзй пользователя id=" + + TEST_USER_ID + " не соответствует ожидаемому."); + } + + /** + * Тестирование удаления пользователя из друзей + */ + @Test + void removeFriend() { + final int deletedFriendId = 3; + getUserFriends(); + + userDbStorage.breakUpFriends(TEST_USER_ID, deletedFriendId); + Collection friends = userDbStorage.getUserFriends(TEST_USER_ID); + + Optional friend = friends.stream() + .filter(user -> user.getId() == deletedFriendId) + .findFirst(); + + assertThat(friend) + .withFailMessage("removeFriend() - пользователь id=%s не был удален из \"друзей\"%s.", + deletedFriendId, TEST_USER_ID) + .isEmpty(); + } + + /** + * Тестирование поиска общих друзей + */ + @Test + void findCommonFriends() { + getUserFriends(); + userDbStorage.addFriend(3, 1); + userDbStorage.addFriend(3, 4); + + Collection friends = userDbStorage.getCommonFriends(TEST_USER_ID, 3); + Optional commonFriend = friends.stream().findFirst(); + + assertThat(commonFriend) + .isPresent() + .hasValueSatisfying(user -> + assertThat(user) + .hasFieldOrPropertyWithValue("id", 4)); + } + + /** + * Тестирование удаления пользователей + */ + @Test + void removeAllUsers() { + addNewUser(); + + userDbStorage.removeAllUsers(); + Collection users = userDbStorage.findAllUsers(); + + assertTrue(users.size() == 0, + "removeAllUsers() - Не удалось удалить всех пользователей."); + } } \ No newline at end of file diff --git a/src/test/resources/data.sql b/src/test/resources/data.sql index 9f1f29f..472c2e8 100644 --- a/src/test/resources/data.sql +++ b/src/test/resources/data.sql @@ -15,7 +15,10 @@ MERGE INTO MPA (id, name, description) (4, 'R', 'лицам до 17 лет просматривать фильм можно только в присутствии взрослого'), (5, 'NC-17', 'лицам до 18 лет просмотр запрещён'); --- Создаем тестового пользователя +-- Создаем тестовоых пользователей INSERT INTO users (email, login, name, birthday) -VALUES ( 'test@test.com', 'testLogin', 'testName', '2001-9-22' ); +VALUES ( 'test@test.com', 'testLogin', 'testName', '2001-9-22' ), + ('user1@test.com', 'userTest1', 'userNane1', '2001-01-01'), + ('user2@test.com', 'userTest2', 'userNane2', '2002-02-02'), + ('user3@test.com', 'userTest3', 'userNane3', '2003-03-03'); From 8e6bf03239cd5e4e9c676c432ccc8b3485d98ee2 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Sun, 23 Feb 2025 23:17:58 +0700 Subject: [PATCH 14/19] =?UTF-8?q?=D0=9F=D0=B8=D1=88=D0=B5=D0=BC=20=D1=82?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D1=8B=203.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../practicum/filmorate/model/Film.java | 3 +- .../practicum/filmorate/model/Genre.java | 6 + .../yandex/practicum/filmorate/model/Mpa.java | 10 + .../practicum/filmorate/model/User.java | 11 + .../filmorate/storage/film/FilmDbStorage.java | 578 +++++++++--------- .../storage/genre/GenreDbStorage.java | 14 +- .../filmorate/storage/mpa/MpaDbStorage.java | 14 +- .../filmorate/storage/user/UserDbStorage.java | 3 +- src/main/resources/application.properties | 2 +- .../controller/FilmControllerTest.java | 73 +++ .../controller/UserControllerTest.java | 295 +++++++++ .../practicum/filmorate/model/FilmTest.java | 126 ++++ .../practicum/filmorate/model/UserTest.java | 95 +++ .../storage/film/FilmDbStorageTest.java | 173 +++++- .../storage/genre/GenreDbStorageTest.java | 77 +++ .../storage/mpa/MpaDbStorageTest.java | 75 +++ .../storage/user/UserDbStorageTest.java | 16 +- src/test/resources/data.sql | 10 + 18 files changed, 1259 insertions(+), 322 deletions(-) create mode 100644 src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/model/FilmTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/model/UserTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/storage/genre/GenreDbStorageTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/storage/mpa/MpaDbStorageTest.java 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 c58b820..7f48505 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java @@ -6,11 +6,12 @@ import jakarta.validation.constraints.Size; import lombok.Data; import lombok.EqualsAndHashCode; -import org.springframework.validation.annotation.Validated; +import lombok.NoArgsConstructor; import ru.yandex.practicum.filmorate.validator.LegalFilmDate; import ru.yandex.practicum.filmorate.validator.Marker; import java.time.LocalDate; +import java.util.HashSet; import java.util.LinkedHashSet; /** diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Genre.java b/src/main/java/ru/yandex/practicum/filmorate/model/Genre.java index 5eb2277..4a7ed25 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Genre.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Genre.java @@ -1,8 +1,14 @@ package ru.yandex.practicum.filmorate.model; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; @Data +@EqualsAndHashCode(of = {"id"}) +@NoArgsConstructor +@AllArgsConstructor 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 index 25022e3..ea7f17a 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Mpa.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Mpa.java @@ -1,13 +1,23 @@ package ru.yandex.practicum.filmorate.model; import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; @Data +@EqualsAndHashCode(of = {"id"}) +@NoArgsConstructor +@AllArgsConstructor public class Mpa { private int id; private String name; @JsonIgnore private String description; + + public Mpa(int id) { + this.id = id; + } } 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 5d7c350..3bdf6af 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/User.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/User.java @@ -1,8 +1,10 @@ package ru.yandex.practicum.filmorate.model; import jakarta.validation.constraints.*; +import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; import org.springframework.validation.annotation.Validated; import ru.yandex.practicum.filmorate.validator.Marker; @@ -13,6 +15,8 @@ */ @Data @EqualsAndHashCode(of = {"email"}) +@NoArgsConstructor +@AllArgsConstructor @Validated public class User { @@ -34,4 +38,11 @@ public class User { @PastOrPresent(message = "Дата рождения не может быть в будущем.", groups = {Marker.OnBasic.class, Marker.OnUpdate.class}) private LocalDate birthday; + + public User(String email, String login, String name, LocalDate birthday) { + this.email = email; + this.login = login; + this.name = name; + this.birthday = birthday; + } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java index cad5336..42637c7 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java @@ -1,5 +1,6 @@ package ru.yandex.practicum.filmorate.storage.film; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.ResultSetExtractor; @@ -10,7 +11,6 @@ import org.springframework.stereotype.Repository; import ru.yandex.practicum.filmorate.exception.InternalServerException; import ru.yandex.practicum.filmorate.exception.NotFoundException; -import ru.yandex.practicum.filmorate.exception.ValidationException; import ru.yandex.practicum.filmorate.mapper.FilmGenreRowMapper; import ru.yandex.practicum.filmorate.mapper.FilmRowMapper; import ru.yandex.practicum.filmorate.model.Film; @@ -40,332 +40,318 @@ public class FilmDbStorage implements FilmStorage { private static final String SQL_ADD_LIKE = "MERGE INTO likes (user_id, film_id) VALUES (:userId, :filmId)"; private static final String SQL_REMOVE_LIKE = "DELETE FROM likes WHERE user_id = :userId AND film_id = :filmId"; - private final NamedParameterJdbcTemplate jdbc; - private final FilmRowMapper filmMapper; - private final FilmGenreRowMapper filmGenreRowMapper; - - /** - * Основной конструктор - * - * @param jdbc - объект работы с базой данных - * @param filmMapper - объект преобразования строк базы данных в объекты класса Film - */ - public FilmDbStorage(NamedParameterJdbcTemplate jdbc, FilmRowMapper filmMapper, - FilmGenreRowMapper filmGenreRowMapper) { - this.jdbc = jdbc; - this.filmMapper = filmMapper; - this.filmGenreRowMapper = filmGenreRowMapper; + @Autowired + private NamedParameterJdbcTemplate jdbc; + +/** + * Добавление информации о фильме + * + * @param newFilm - объект для добавления + * @return - подтвержденный объект + */ +@Override +public Film addNewFilm(Film newFilm) { + GeneratedKeyHolder generatedKeyHolder = new GeneratedKeyHolder(); + + // сохраняем информацию о фильме в базу данных + try { + jdbc.update(SQL_INSERT_FILM, + new MapSqlParameterSource() + .addValue("name", newFilm.getName()) + .addValue("description", newFilm.getDescription()) + .addValue("releasedate", newFilm.getReleaseDate(), Types.DATE) + .addValue("len_min", newFilm.getDuration()) + .addValue("mpa_id", newFilm.getMpa().getId()), + generatedKeyHolder + ); + } catch (DataAccessException e) { + throw new NotFoundException("Получены недопустимые параметры запроса: " + + e.getMessage()); } - /** - * Добавление информации о фильме - * - * @param newFilm - объект для добавления - * @return - подтвержденный объект - */ - @Override - public Film addNewFilm(Film newFilm) { - GeneratedKeyHolder generatedKeyHolder = new GeneratedKeyHolder(); - - // сохраняем информацию о фильме в базу данных - try { - jdbc.update(SQL_INSERT_FILM, - new MapSqlParameterSource() - .addValue("name", newFilm.getName()) - .addValue("description", newFilm.getDescription()) - .addValue("releasedate", newFilm.getReleaseDate(), Types.DATE) - .addValue("len_min", newFilm.getDuration()) - .addValue("mpa_id", newFilm.getMpa().getId()), - generatedKeyHolder - ); - } catch (DataAccessException e) { - throw new NotFoundException("Получены недопустимые параметры запроса: " + - e.getMessage()); - } + // получаем идентификатор фильма + final Integer filmId = generatedKeyHolder.getKey().intValue(); + newFilm.setId(filmId); + + // добавляем жанры Фильма Если определены + if (!newFilm.getGenres().isEmpty()) { + SqlParameterSource[] batch = newFilm.getGenres().stream() + .map(genre -> new MapSqlParameterSource() + .addValue("film_id", filmId) + .addValue("genre_id", genre.getId())) + .toArray(SqlParameterSource[]::new); + jdbc.batchUpdate(SQL_UPDATE_GENRES, batch); + } - // получаем идентификатор фильма - final Integer filmId = generatedKeyHolder.getKey().intValue(); - newFilm.setId(filmId); + return getFilmById(filmId).orElseThrow(() -> + new InternalServerException("Ошибка при добавлении фильма.")); +} - // добавляем жанры Фильма Если определены - if (!newFilm.getGenres().isEmpty()) { - SqlParameterSource[] batch = newFilm.getGenres().stream() - .map(genre -> new MapSqlParameterSource() - .addValue("film_id", filmId) - .addValue("genre_id", genre.getId())) - .toArray(SqlParameterSource[]::new); - jdbc.batchUpdate(SQL_UPDATE_GENRES, batch); - } +/** + * Поиск фильма по идентификатору + * + * @param id - идентификатор фильма + * @return - объект описания фильма + */ +@Override +public Optional getFilmById(Integer id) { + try { + Film film = jdbc.query(SQL_FIND_FILM_BY_ID, + new MapSqlParameterSource() + .addValue("id", id), + new ResultSetExtractor() { + @Override + public Film extractData(ResultSet rs) throws SQLException, DataAccessException { + rs.next(); + Film filmRs = new FilmRowMapper().mapRow(rs, 1); + Integer mpaId = rs.getInt("mpa_id"); + if (mpaId != null) { + Mpa mpa = new Mpa(); + mpa.setId(mpaId); + mpa.setName(rs.getString("mpa_name")); + filmRs.setMpa(mpa); + } + do { + Integer genreId = rs.getInt("genre_id"); + if (genreId != 0) { + Genre genre = new Genre(); + genre.setId(genreId); + genre.setName(rs.getString("genre_name")); + filmRs.addGenre(genre); + } + } while (rs.next()); + return filmRs; + } + } + ); + return Optional.ofNullable(film); - return getFilmById(filmId).orElseThrow(() -> - new InternalServerException("Ошибка при добавлении фильма.")); + } catch (DataAccessException ignored) { + return Optional.empty(); } +} - /** - * Поиск фильма по идентификатору - * - * @param id - идентификатор фильма - * @return - объект описания фильма - */ - @Override - public Optional getFilmById(Integer id) { - try { - Film film = jdbc.query(SQL_FIND_FILM_BY_ID, - new MapSqlParameterSource() - .addValue("id", id), - new ResultSetExtractor() { - @Override - public Film extractData(ResultSet rs) throws SQLException, DataAccessException { - rs.next(); - Film filmRs = filmMapper.mapRow(rs, 1); +/** + * Поиск всех фильмов + * + * @return - список фильмов + */ +@Override +public Collection findAllFilms() { + try { + // Загружаем из базы данных все фильмы + Map filmsMap = new HashMap<>(); + filmsMap = jdbc.query("SELECT f.*, mpa.name as mpa_name FROM films AS f INNER JOIN mpa ON f.mpa_id = mpa.id", + new ResultSetExtractor>() { + @Override + public Map extractData(ResultSet rs) + throws SQLException, DataAccessException { + Map fMap = new HashMap<>(); + while (rs.next()) { + Film film = new FilmRowMapper().mapRow(rs, 1); Integer mpaId = rs.getInt("mpa_id"); - if(mpaId != null) { + if (mpaId != 0) { Mpa mpa = new Mpa(); mpa.setId(mpaId); mpa.setName(rs.getString("mpa_name")); - filmRs.setMpa(mpa); + film.setMpa(mpa); } - do { - Integer genreId = rs.getInt("genre_id"); - if (genreId != 0) { - Genre genre = new Genre(); - genre.setId(genreId); - genre.setName(rs.getString("genre_name")); - filmRs.addGenre(genre); - } - } while (rs.next()); - return filmRs; + fMap.put(film.getId(), film); } + return fMap; } - ); - return Optional.ofNullable(film); - - } catch (DataAccessException ignored) { - return Optional.empty(); + }); + + // загружаем из базы данных все ссылки на жанры + List filmsGenres; + filmsGenres = jdbc.query( + "SELECT fg.*, g.name AS genre_name FROM films_genres AS fg INNER JOIN genres AS g ON fg.GENRE_ID = g.ID", + new FilmGenreRowMapper()); + + // пополням фильмы сведениями о жанрах + for (FilmGenre filmGenre : filmsGenres) { + Genre genre = new Genre(); + int filmId = filmGenre.getFilmId(); + genre.setId(filmGenre.getGenreId()); + genre.setName(filmGenre.getGenreName()); + filmsMap.get(filmId).addGenre(genre); } - } - - /** - * Поиск всех фильмов - * - * @return - список фильмов - */ - @Override - public Collection findAllFilms() { - try { - // Загружаем из базы данных все фильмы - Map filmsMap = new HashMap<>(); - filmsMap = jdbc.query("SELECT f.*, mpa.name as mpa_name FROM films AS f INNER JOIN mpa ON f.mpa_id = mpa.id", - new ResultSetExtractor>() { - @Override - public Map extractData(ResultSet rs) - throws SQLException, DataAccessException { - Map fMap = new HashMap<>(); - while (rs.next()) { - Film film = filmMapper.mapRow(rs, 1); - Integer mpaId = rs.getInt("mpa_id"); - if(mpaId != 0) { - Mpa mpa = new Mpa(); - mpa.setId(mpaId); - mpa.setName(rs.getString("mpa_name")); - film.setMpa(mpa); - } - fMap.put(film.getId(), film); - } - return fMap; - } - }); - - // загружаем из базы данных все ссылки на жанры - List filmsGenres; - filmsGenres = jdbc.query( - "SELECT fg.*, g.name AS genre_name FROM films_genres AS fg INNER JOIN genres AS g ON fg.GENRE_ID = g.ID", - filmGenreRowMapper); - - // пополням фильмы сведениями о жанрах - for (FilmGenre filmGenre : filmsGenres) { - Genre genre = new Genre(); - int filmId = filmGenre.getFilmId(); - genre.setId(filmGenre.getGenreId()); - genre.setName(filmGenre.getGenreName()); - filmsMap.get(filmId).addGenre(genre); - } - return filmsMap.values(); - - } catch (EmptyResultDataAccessException ignored) { - return List.of(); - } + return filmsMap.values(); + } catch (EmptyResultDataAccessException ignored) { + return List.of(); } - /** - * Поиск популярных фильмов - * - * @param count - * @return - */ - @Override - public Collection findPopularFilms(int count) { - String sql = "SELECT f.*, mpa.name AS mpa_name, popular.count_film\n" + - "FROM (films AS f INNER JOIN mpa ON f.MPA_ID = mpa.ID\n)" + - " LEFT OUTER JOIN\n" + - " (SELECT film_id, count(film_id) as count_film\n" + - " FROM LIKES GROUP BY film_id) AS popular\n" + - " ON f.id = popular.film_id\n" + - "ORDER BY popular.count_film DESC\n" + - "LIMIT :count"; +} - Map popularFilms; - try { - popularFilms = jdbc.query(sql, new MapSqlParameterSource() - .addValue("count", count), - new ResultSetExtractor>() { - @Override - public Map extractData(ResultSet rs) - throws SQLException, DataAccessException { - Map mapFilm = new LinkedHashMap<>(); - while (rs.next()) { - Film film = filmMapper.mapRow(rs, 1); - Integer mpaId = rs.getInt("mpa_id"); - if(mpaId != 0) { - Mpa mpa = new Mpa(); - mpa.setId(mpaId); - mpa.setName(rs.getString("mpa_name")); - film.setMpa(mpa); - } - mapFilm.put(film.getId(), film); +/** + * Поиск популярных фильмов + * + * @param count + * @return + */ +@Override +public Collection findPopularFilms(int count) { + String sql = "SELECT f.*, mpa.name AS mpa_name, popular.count_film\n" + + "FROM (films AS f INNER JOIN mpa ON f.MPA_ID = mpa.ID\n)" + + " LEFT OUTER JOIN\n" + + " (SELECT film_id, count(film_id) as count_film\n" + + " FROM LIKES GROUP BY film_id) AS popular\n" + + " ON f.id = popular.film_id\n" + + "ORDER BY popular.count_film DESC\n" + + "LIMIT :count"; + + Map popularFilms; + try { + popularFilms = jdbc.query(sql, new MapSqlParameterSource() + .addValue("count", count), + new ResultSetExtractor>() { + @Override + public Map extractData(ResultSet rs) + throws SQLException, DataAccessException { + Map mapFilm = new LinkedHashMap<>(); + while (rs.next()) { + Film film = new FilmRowMapper().mapRow(rs, 1); + Integer mpaId = rs.getInt("mpa_id"); + if (mpaId != 0) { + Mpa mpa = new Mpa(); + mpa.setId(mpaId); + mpa.setName(rs.getString("mpa_name")); + film.setMpa(mpa); } - return mapFilm; + mapFilm.put(film.getId(), film); } - }); - - // загружаем из базы данных соответствующие ссылки на жанры - List ids = new ArrayList<>(popularFilms.keySet()); - List filmsGenres; - filmsGenres = jdbc.query( - "SELECT fg.*, " + - "g.name AS genre_name " + - "FROM films_genres AS fg INNER JOIN genres AS g ON fg.genre_id = g.id " + - "WHERE fg.film_id IN (:values)", - new MapSqlParameterSource() - .addValue("values", ids), - filmGenreRowMapper); - - // пополням фильмы сведениями о жанрах - for (FilmGenre filmGenre : filmsGenres) { - Genre genre = new Genre(); - int filmId = filmGenre.getFilmId(); - genre.setId(filmGenre.getGenreId()); - genre.setName(filmGenre.getGenreName()); - popularFilms.get(filmId).addGenre(genre); - } - return popularFilms.values(); - - } catch (EmptyResultDataAccessException ignored) { - return List.of(); - } - } - - /** - * Обновление сведений о фильме - * - * @param updFilm - обновленный объект - */ - @Override - public void updateFilm(Film updFilm) { - // задаем параметры SQL запоса - MapSqlParameterSource params = new MapSqlParameterSource(); - params.addValue("name", updFilm.getName()); - params.addValue("description", updFilm.getDescription()); - params.addValue("releasedate", updFilm.getReleaseDate(), Types.DATE); - params.addValue("len_min", updFilm.getDuration()); - params.addValue("mpa_id", updFilm.getMpa().getId()); - params.addValue("id", updFilm.getId()); - - // обновляем информацию о фильме - int rowsUpdated = jdbc.update(SQL_UPDATE_FILM, params); - if (rowsUpdated == 0) { - throw new InternalServerException("Не удалось обновить информацию о фильие"); - } - - // Удаляем все жанры которые были определены для фильма - int filmId = updFilm.getId(); - jdbc.update("DELETE FROM films_genres WHERE film_id = :filmId", + return mapFilm; + } + }); + + // загружаем из базы данных соответствующие ссылки на жанры + List ids = new ArrayList<>(popularFilms.keySet()); + List filmsGenres; + filmsGenres = jdbc.query( + "SELECT fg.*, " + + "g.name AS genre_name " + + "FROM films_genres AS fg INNER JOIN genres AS g ON fg.genre_id = g.id " + + "WHERE fg.film_id IN (:values)", new MapSqlParameterSource() - .addValue("filmId", filmId)); - - // добавляем жанры Фильма если определены новые - if (updFilm.getGenres().size() > 0) { - SqlParameterSource[] batch = updFilm.getGenres().stream() - .map(genre -> new MapSqlParameterSource() - .addValue("film_id", updFilm.getId()) - .addValue("genre_id", genre.getId())) - .toArray(SqlParameterSource[]::new); - jdbc.batchUpdate(SQL_UPDATE_GENRES, batch); + .addValue("values", ids), + new FilmGenreRowMapper()); + + // пополням фильмы сведениями о жанрах + for (FilmGenre filmGenre : filmsGenres) { + Genre genre = new Genre(); + int filmId = filmGenre.getFilmId(); + genre.setId(filmGenre.getGenreId()); + genre.setName(filmGenre.getGenreName()); + popularFilms.get(filmId).addGenre(genre); } + return popularFilms.values(); + + } catch (EmptyResultDataAccessException ignored) { + return List.of(); } +} - /** - * Добавление "лайка" к фильму. - * - * @param filmId - идентификатор фильма - * @param userId - идентификатор пользователя - * @return - число "лайков" - */ - @Override - public Integer addNewLike(Integer filmId, Integer userId) { - int rowsUpdated = jdbc.update(SQL_ADD_LIKE, new MapSqlParameterSource() - .addValue("userId", userId) - .addValue("filmId", filmId) - ); - if (rowsUpdated == 0) { - throw new InternalServerException("Не удалось обновить данные"); - } - return getFilmRank(filmId); +/** + * Обновление сведений о фильме + * + * @param updFilm - обновленный объект + */ +@Override +public void updateFilm(Film updFilm) { + // задаем параметры SQL запоса + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("name", updFilm.getName()); + params.addValue("description", updFilm.getDescription()); + params.addValue("releasedate", updFilm.getReleaseDate(), Types.DATE); + params.addValue("len_min", updFilm.getDuration()); + params.addValue("mpa_id", updFilm.getMpa().getId()); + params.addValue("id", updFilm.getId()); + + // обновляем информацию о фильме + int rowsUpdated = jdbc.update(SQL_UPDATE_FILM, params); + if (rowsUpdated == 0) { + throw new InternalServerException("Не удалось обновить информацию о фильие"); } - /** - * Удаление "лайка" у фильма. - * - * @param filmId - идентификатор фильма - * @param userId - идентификатор пользователя - * @return - число "лайков" - */ - @Override - public Integer removeLike(Integer filmId, Integer userId) { - jdbc.update(SQL_REMOVE_LIKE, new MapSqlParameterSource() - .addValue("userId", userId) - .addValue("filmId", filmId) - ); - return getFilmRank(filmId); + // Удаляем все жанры которые были определены для фильма + int filmId = updFilm.getId(); + jdbc.update("DELETE FROM films_genres WHERE film_id = :filmId", + new MapSqlParameterSource() + .addValue("filmId", filmId)); + + // добавляем жанры Фильма если определены новые + if (updFilm.getGenres().size() > 0) { + SqlParameterSource[] batch = updFilm.getGenres().stream() + .map(genre -> new MapSqlParameterSource() + .addValue("film_id", updFilm.getId()) + .addValue("genre_id", genre.getId())) + .toArray(SqlParameterSource[]::new); + jdbc.batchUpdate(SQL_UPDATE_GENRES, batch); } +} - /** - * Подсчет "лайков" фильма. - * - * @param filmId - идентификатор фильма - * @return - число "лайков" - */ - @Override - public Integer getFilmRank(Integer filmId) { - try { - return jdbc.queryForObject("SELECT count(film_id) FROM likes WHERE film_id = :filmId", - new MapSqlParameterSource() - .addValue("filmId", filmId), - Integer.class); - } catch (EmptyResultDataAccessException ignored) { - throw new NotFoundException("Информация о популярности фильма не найдена. id:" + filmId); - } +/** + * Добавление "лайка" к фильму. + * + * @param filmId - идентификатор фильма + * @param userId - идентификатор пользователя + * @return - число "лайков" + */ +@Override +public Integer addNewLike(Integer filmId, Integer userId) { + int rowsUpdated = jdbc.update(SQL_ADD_LIKE, new MapSqlParameterSource() + .addValue("userId", userId) + .addValue("filmId", filmId) + ); + if (rowsUpdated == 0) { + throw new InternalServerException("Не удалось обновить данные"); } + return getFilmRank(filmId); +} - @Override - public void removeAllFilms() { - jdbc.update("DELETE FROM likes", new MapSqlParameterSource() - .addValue("table", "likes")); - jdbc.update("DELETE FROM films_genres", new MapSqlParameterSource() - .addValue("table", "films_genres")); - jdbc.update("DELETE FROM films", new MapSqlParameterSource() - .addValue("table", "films")); +/** + * Удаление "лайка" у фильма. + * + * @param filmId - идентификатор фильма + * @param userId - идентификатор пользователя + * @return - число "лайков" + */ +@Override +public Integer removeLike(Integer filmId, Integer userId) { + jdbc.update(SQL_REMOVE_LIKE, new MapSqlParameterSource() + .addValue("userId", userId) + .addValue("filmId", filmId) + ); + return getFilmRank(filmId); +} + +/** + * Подсчет "лайков" фильма. + * + * @param filmId - идентификатор фильма + * @return - число "лайков" + */ +@Override +public Integer getFilmRank(Integer filmId) { + try { + return jdbc.queryForObject("SELECT count(film_id) FROM likes WHERE film_id = :filmId", + new MapSqlParameterSource() + .addValue("filmId", filmId), + Integer.class); + } catch (EmptyResultDataAccessException ignored) { + throw new NotFoundException("Информация о популярности фильма не найдена. id:" + filmId); } } + +@Override +public void removeAllFilms() { + jdbc.update("DELETE FROM likes", new MapSqlParameterSource() + .addValue("table", "likes")); + jdbc.update("DELETE FROM films_genres", new MapSqlParameterSource() + .addValue("table", "films_genres")); + jdbc.update("DELETE FROM films", new MapSqlParameterSource() + .addValue("table", "films")); +} +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/genre/GenreDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/genre/GenreDbStorage.java index 86e1333..f7e4f37 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/genre/GenreDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/genre/GenreDbStorage.java @@ -1,5 +1,6 @@ package ru.yandex.practicum.filmorate.storage.genre; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; @@ -17,13 +18,8 @@ public class GenreDbStorage implements GenreStorage { private static final String SQL_GET_ALL_GENRES = "SELECT * FROM genres"; private static final String SQL_GET_GENRE = "SELECT * FROM genres WHERE id = :id"; - private final NamedParameterJdbcTemplate jdbc; - private final GenreRowMapper mapper; - - public GenreDbStorage(NamedParameterJdbcTemplate jdbc, GenreRowMapper mapper) { - this.jdbc = jdbc; - this.mapper = mapper; - } + @Autowired + private NamedParameterJdbcTemplate jdbc; /** * Чтение всех жанров в справочнике @@ -33,7 +29,7 @@ public GenreDbStorage(NamedParameterJdbcTemplate jdbc, GenreRowMapper mapper) { @Override public Collection findAllGenres() { try { - return jdbc.query(SQL_GET_ALL_GENRES, mapper); + return jdbc.query(SQL_GET_ALL_GENRES, new GenreRowMapper()); } catch (EmptyResultDataAccessException ignored) { return List.of(); } @@ -51,7 +47,7 @@ public Optional findGenre(Integer id) { Genre genre = jdbc.queryForObject(SQL_GET_GENRE, new MapSqlParameterSource() .addValue("id", id), - mapper); + new GenreRowMapper()); return Optional.ofNullable(genre); } catch (EmptyResultDataAccessException ignored) { return Optional.empty(); diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/mpa/MpaDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/mpa/MpaDbStorage.java index f48c243..c51f4ec 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/mpa/MpaDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/mpa/MpaDbStorage.java @@ -1,5 +1,6 @@ package ru.yandex.practicum.filmorate.storage.mpa; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; @@ -16,18 +17,13 @@ public class MpaDbStorage implements MpaStorage { private static final String SQL_GET_ALL_MPA = "SELECT * FROM mpa"; private static final String SQL_GET_MPA = "SELECT * FROM mpa WHERE id = :id"; - private final NamedParameterJdbcTemplate jdbc; - private final MpaRowMapper mapper; - - public MpaDbStorage(NamedParameterJdbcTemplate jdbc, MpaRowMapper mapper) { - this.jdbc = jdbc; - this.mapper = mapper; - } + @Autowired + private NamedParameterJdbcTemplate jdbc; @Override public Collection findAllMpa() { try { - return jdbc.query(SQL_GET_ALL_MPA, mapper); + return jdbc.query(SQL_GET_ALL_MPA, new MpaRowMapper()); } catch (EmptyResultDataAccessException ignored) { return List.of(); } @@ -39,7 +35,7 @@ public Optional findMpa(Integer id) { Mpa mpa = jdbc.queryForObject(SQL_GET_MPA, new MapSqlParameterSource() .addValue("id", id), - mapper); + new MpaRowMapper()); return Optional.ofNullable(mpa); } catch (EmptyResultDataAccessException ignored) { return Optional.empty(); diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java index 2763eb2..452ab29 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java @@ -1,6 +1,7 @@ package ru.yandex.practicum.filmorate.storage.user; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataAccessException; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; @@ -66,7 +67,7 @@ public Optional getUserById(Integer id) { .addValue("id", id), new UserRowMapper()); return Optional.ofNullable(user); - } catch (EmptyResultDataAccessException ignored) { + } catch (DataAccessException ignored) { return Optional.empty(); } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 71a0274..aa40b46 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,5 +1,5 @@ spring.sql.init.mode=always -spring.datasource.url=jdbc:h2:file:./db/filmorate +spring.datasource.url=jdbc:h2:mem:filmorate spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password=password diff --git a/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java new file mode 100644 index 0000000..8d2b2c4 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java @@ -0,0 +1,73 @@ +package ru.yandex.practicum.filmorate.controller; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import lombok.RequiredArgsConstructor; +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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.validator.LocalDateAdapter; + +import java.time.LocalDate; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +class FilmControllerTest { + @Autowired + MockMvc mvc; + + Gson gson = new GsonBuilder() + .setPrettyPrinting() + .registerTypeAdapter(LocalDate.class, new LocalDateAdapter()) + .create(); + + /** + * Тестируем поиск всех фильмов + */ + @Test + void findAllFilms() throws Exception { + MvcResult result = mvc.perform(get("/films")) + .andExpect(status().isOk()) // ожидается код статус 200 + .andReturn(); + List films = gson.fromJson(result.getResponse().getContentAsString(), List.class); + assertTrue(!films.isEmpty(), + "Список фильмов пуст."); + } + + @Test + void findFilm() { + } + + @Test + void findPopularFilms() { + } + + @Test + void addNewFilm() { + } + + @Test + void updateFilm() { + } + + @Test + void addLike() { + } + + @Test + void removeLike() { + } +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java new file mode 100644 index 0000000..8ce766c --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java @@ -0,0 +1,295 @@ +package ru.yandex.practicum.filmorate.controller; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import lombok.RequiredArgsConstructor; +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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.validator.LocalDateAdapter; + +import java.time.LocalDate; +import java.util.List; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Тестируем контроллер запросов работы с данными о пользователях + *

+ * Для успешного выполнения тестов, при инициализации базы данных + * должна быть подготовлена информация о четырех тестовых пользователях. + * Файл первоначальных данных ./src/test/resources/data.sql + */ +@SpringBootTest +@AutoConfigureMockMvc +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +class UserControllerTest { + @Autowired + MockMvc mvc; + + Gson gson = new GsonBuilder() + .setPrettyPrinting() + .registerTypeAdapter(LocalDate.class, new LocalDateAdapter()) + .create(); + + /** + * Тестируем чтение списка пользователей + */ + @Test + void findAllUser() throws Exception { + MvcResult result = mvc.perform(get("/users")) + .andExpect(status().isOk()) // ожидается код статус 200 + .andReturn(); + List users = gson.fromJson(result.getResponse().getContentAsString(), List.class); + assertTrue(!users.isEmpty(), + "Список пользователей пуст."); + } + + /** + * Тестируем добавление нового пользователя + */ + @Test + void addNewUser() throws Exception { + User user = new User(); + user.setLogin("user1234"); + user.setName("testUserName"); + user.setBirthday(LocalDate.now().minusYears(22)); + + user.setEmail("User1234_domain@"); + String jsonString = gson.toJson(user); + // При добавлении пользователя некорректным Email + // должен возвращаться статус 400 "BadRequest" + mvc.perform(post("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + + user.setEmail("User1234@domain"); + jsonString = gson.toJson(user); + // При успешном добавлении пользователя + // должен возвращаться статус 201 "Created" + MvcResult result = mvc.perform(post("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andReturn(); + // Сохраняем созданного пользователя + User userDb = gson.fromJson(result.getResponse().getContentAsString(), User.class); + assertNotNull(userDb.getId(), + "При добавлении пользователя должен быть присвоен ненулевой идентификатор"); + + // Повторное добавление пользователя + // должно возвращать статус 400 "BadRequest" + mvc.perform(post("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + /** + * Тестирование обновления сведений о пользователе + */ + @Test + void updateUser() throws Exception { + User user = new User(); + user.setEmail("UserUpdate@domain"); + user.setLogin("userUpdate"); + user.setName("testUpdateUserName"); + user.setBirthday(LocalDate.now().minusYears(22)); + String jsonString = gson.toJson(user); + + // При успешном добавлении пользователя + // должен возвращаться статус 201 "Created" + MvcResult result = mvc.perform(post("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andReturn(); + // Сохраняем созданного пользователя + User userDb = gson.fromJson(result.getResponse().getContentAsString(), User.class); + + // готовим данные для обновления + user.setLogin("userUpd12345"); + user.setName("Updated user."); + user.setBirthday(LocalDate.now().minusYears(22)); + + jsonString = gson.toJson(user); + // Обновление записи без идентификатора пользователя + // должно возвращать статус 400 "BadRequest" + mvc.perform(put("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + + user.setId(1000); + jsonString = gson.toJson(user); + // Обновление записи c несуществющим идентификатором + // должно возвращать статус 404 "NotFound" + mvc.perform(put("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + + user.setId(userDb.getId()); + jsonString = gson.toJson(user); + // Успешное обновление записи + // должно возвращать статус 200 "Ok" + result = mvc.perform(put("/users") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn(); + // Сохраняем пользователя из ответа после обновления + userDb = gson.fromJson(result.getResponse().getContentAsString(), User.class); + + assertThat(userDb) + .usingRecursiveComparison() + .isEqualTo(user); + } + + /** + * Тестирование добавления "друга" + * + * @throws Exception + */ + @Test + void addFriends() throws Exception { + // Объявление в "друзья" несуществующего пользователя + // должно возвращать статус 404 "NotFound" + mvc.perform(put("/users/1000/friends/1") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + + // Объявление в "друзья" несуществующего друга + // должно возвращать статус 404 "NotFound()" + mvc.perform(put("/users/1/friends/1000") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + + // Объявление в "друзья" сущществующих пользователей + // должно возвращать статус 200 "ok" + mvc.perform(put("/users/1/friends/2") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } + + /** + * Тестирование удаления пользователя из друзей + * + * @throws Exception + */ + @Test + void breakUpFriends() throws Exception { + // Добавление в "друзья" пользователея + // должно возвращать статус 200 "ok" + mvc.perform(put("/users/1/friends/2") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + // Удаление из "друзьей" не сущществующих пользователей + // должно возвращать статус 404 "NotFound" + mvc.perform(delete("/users/1/friends/1000") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + + // Удаление из "друзьей" сущществующих пользователей + // должно возвращать статус 200 "Ok" + mvc.perform(delete("/users/1/friends/2") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } + + /** + * Тестирование поиска друзей пользователя + * + * @throws Exception + */ + @Test + void findUsersFriends() throws Exception { + // Добавление в "друзья" пользователея + // должно возвращать статус 200 "ok" + mvc.perform(put("/users/4/friends/2") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + // Добавление в "друзья" пользователея + // должно возвращать статус 200 "ok" + mvc.perform(put("/users/4/friends/3") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + // читаем список "друзей", несуществующего пользователя + // должно возвращать статус 404 "NotFound" + mvc.perform(get("/users/2000/friends") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + + // читаем список "друзей" + // должно возвращать статус 200 "ok" + MvcResult result = mvc.perform(get("/users/4/friends") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn(); + List users = gson.fromJson(result.getResponse().getContentAsString(), List.class); + assertTrue(users.size() == 2, + "Количество найденых \"друзей\" не соответствует ожидаемому."); + } + + /** + * Тестирование поиска общих друзей у пользователей + * + * @throws Exception + */ + @Test + void findCommonFriends() throws Exception { + mvc.perform(put("/users/1/friends/2") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + mvc.perform(put("/users/1/friends/3") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + mvc.perform(put("/users/1/friends/4") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + mvc.perform(put("/users/3/friends/2") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + mvc.perform(put("/users/3/friends/1") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + mvc.perform(put("/users/4/friends/2") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + mvc.perform(put("/users/4/friends/3") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + // читаем список общих "друзей" + // должно возвращать статус 200 "ok" + MvcResult result = mvc.perform(get("/users/1/friends/common/4") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn(); + List users = gson.fromJson(result.getResponse().getContentAsString(), List.class); + assertTrue(users.size() == 2, + "Количество общих \"друзей\" не соответствует ожидаемому."); + } + +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/model/FilmTest.java b/src/test/java/ru/yandex/practicum/filmorate/model/FilmTest.java new file mode 100644 index 0000000..93f3ada --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/model/FilmTest.java @@ -0,0 +1,126 @@ + +package ru.yandex.practicum.filmorate.model; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import ru.yandex.practicum.filmorate.validator.Marker; + +import java.time.LocalDate; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Тестирование ограничений на значения полей класса Film + * Автономный тест (Junit). + */ +class FilmTest { + private Validator validator; + + /** + * Перед каждым тестом готовим Validator + */ + @BeforeEach + void setUp() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + } + + /** + * Проверка непустого названия фильма. + */ + @Test + void testName() { + Film film = new Film(); + film.setName(""); + film.setDescription("Testing film"); + film.setReleaseDate(LocalDate.now().minusYears(10)); + film.setDuration(60); + film.setMpa(new Mpa(1)); + + Set> violations = validator.validate(film, Marker.OnBasic.class); + assertFalse(violations.isEmpty()); + } + + /** + * Проверка допусимого размера описания. + */ + @Test + void testDescription() { + Film film = new Film(); + film.setName("Film"); + film.setDescription("12345678901234567890123456789012345678901234567890" + + "12345678901234567890123456789012345678901234567890" + + "12345678901234567890123456789012345678901234567890" + + "12345678901234567890123456789012345678901234567890" + + "12345678901234567890123456789012345678901234567890"); + film.setReleaseDate(LocalDate.now().minusYears(10)); + film.setDuration(60); + film.setMpa(new Mpa(1)); + + Set> violations = validator.validate(film, Marker.OnBasic.class); + assertFalse(violations.isEmpty()); + } + + /** + * Проверка допустимой даты выпуска фильма + */ + @Test + void testReleaseDate() { + Film film = new Film(); + film.setName("Film"); + film.setDescription("Testing film ReleaseDate"); + film.setReleaseDate(LocalDate.now().plusDays(1)); + film.setDuration(60); + film.setMpa(new Mpa(1)); + + // Проверяем на контроль даты в будущем + Set> violations = validator.validate(film, Marker.OnBasic.class); + assertFalse(violations.isEmpty()); + + // Проверяем на контроль минимальной даты выхода фильма + film.setReleaseDate(LocalDate.of(1895, 12, 27)); + + violations.clear(); + violations = validator.validate(film, Marker.OnBasic.class); + assertFalse(violations.isEmpty()); + } + + /** + * Проверка допустимой длительности фильма + */ + @Test + void testDuration() { + Film film = new Film(); + film.setName("Film"); + film.setDescription("Testing film ReleaseDate"); + film.setReleaseDate(LocalDate.now().minusYears(10)); + film.setDuration(0); + film.setMpa(new Mpa(1)); + + Set> violations = validator.validate(film, Marker.OnBasic.class); + assertFalse(violations.isEmpty()); + } + + /** + * Тестируем отсутствие ограничений при корректном создании фильма + */ + @Test + void testFilmOk() { + Film film = new Film(); + film.setName("Film"); + film.setDescription("Testing film ReleaseDate"); + film.setReleaseDate(LocalDate.now().minusYears(10)); + film.setDuration(60); + film.setMpa(new Mpa(1)); + + Set> violations = validator.validate(film, Marker.OnBasic.class); + assertTrue(violations.isEmpty(), violations.toString()); + } + +} diff --git a/src/test/java/ru/yandex/practicum/filmorate/model/UserTest.java b/src/test/java/ru/yandex/practicum/filmorate/model/UserTest.java new file mode 100644 index 0000000..b762318 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/model/UserTest.java @@ -0,0 +1,95 @@ + +package ru.yandex.practicum.filmorate.model; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import ru.yandex.practicum.filmorate.validator.Marker; + +import java.time.LocalDate; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Тестирование ограничений на значения полей класса User. + * Автономный тест. + */ +class UserTest { + private Validator validator; + + /** + * Перед каждым тестом готовим Validator + */ + @BeforeEach + void setUp() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + } + + /** + * Тестирование email пользователя + */ + @Test + void testInvalidEmail() throws Exception { + User user = new User("", + "userTest", + "Testing user", + LocalDate.now().minusYears(22)); + + Set> violations = validator.validate(user, Marker.OnBasic.class); + assertFalse(violations.isEmpty()); + } + + /** + * Тестирование login пользователя + */ + @Test + void testInvalidLogin() throws Exception { + User user = new User("user1234@test", + "", // login не должен быть пустым + "Testing user", + LocalDate.now().minusYears(32)); + + Set> violations = validator.validate(user, Marker.OnBasic.class); + assertFalse(violations.isEmpty()); + + // login должен содержать только буквы и цифры + user.setLogin("yu%3242 @#"); + violations.clear(); + violations = validator.validate(user, Marker.OnBasic.class); + assertFalse(violations.isEmpty()); + } + + /** + * Тестируем корректность даты рождения + */ + @Test + void testInvalidBirthday() throws Exception { + User user = new User("user1234@test", + "user1234", + "Testing user", + LocalDate.now().plusDays(1)); // Дата рождения в будущем + + Set> violations = validator.validate(user, Marker.OnBasic.class); + assertFalse(violations.isEmpty()); + } + + /** + * Тестируем отсутствие ошибок при корректном заполнение полей. + */ + @Test + void testUserOk() { + User user = new User("user1234@test", + "user1234", + "Testing user", + LocalDate.now().minusYears(18)); + + Set> violations = validator.validate(user, Marker.OnBasic.class); + assertTrue(violations.isEmpty(), violations.toString()); + } +} diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorageTest.java index 23d04ff..362fc38 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorageTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorageTest.java @@ -1,44 +1,211 @@ package ru.yandex.practicum.filmorate.storage.film; +import lombok.RequiredArgsConstructor; 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.Film; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.model.Mpa; +import java.time.LocalDate; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.jupiter.api.Assertions.*; +/** + * Тестирование Хранилища Фильмов в базе данных + *

+ * Для успешного выполнения тестов, при инициализации базы данных + * должна быть подготовлена информация о четырех тестовых фильмах. + * Для фильма с id=TEST_FILM_ID нужно создать запись в таблице жанров. + * Файл первоначальных данных ./src/test/resources/data.sql + */ +@JdbcTest +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@Import({FilmDbStorage.class}) class FilmDbStorageTest { + public static final int TEST_FILM_ID = 1; + + private final FilmDbStorage filmDbStorage; + /** + * Генерация информации о тестовом фильме + * Поля должны соответствовать содержимому базы данных для фильма TEST_FILM_ID + * + * @return - объект, который ожидается для TEST_FILM_ID + */ + static Film getTestFilm() { + Film film = new Film(); + film.setId(TEST_FILM_ID); + film.setName("TestFilmName"); + film.setDescription("TestFilmDescription"); + film.setReleaseDate(LocalDate.of(2001, 2, 3)); + film.setDuration(51); + film.setMpa(new Mpa(1)); + film.addGenre(new Genre(1, "Комедия")); + return film; + } + + /** + * Тестирование добавления информации о новом фильме + */ @Test void addNewFilm() { + Film film = new Film(); + film.setName("Фильм! Фильм! Фильм!"); + film.setDescription("Юмористический рссказ о том, как делают кино."); + film.setReleaseDate(LocalDate.of(1968, 9, 1)); + film.setDuration(20); + film.setMpa(new Mpa(1)); + film.addGenre(new Genre(1, "Комедия")); + film.addGenre(new Genre(3, "Мультфильм")); + + Film filmDb = filmDbStorage.addNewFilm(film); + assertNotNull(filmDb.getId(), + "При добавлении нового фильа должен быть присвоен ненулевой идентификатор."); + + // Сравниваем исходный фильм с сохраненным по всем полям + Optional filmOptional = filmDbStorage.getFilmById(filmDb.getId()); + assertThat(filmOptional) + .isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(filmDb); } + /** + * Тестирование чтения информации о фильме по заданному идентификатору + */ @Test void getFilmById() { + Film film = getTestFilm(); + Optional filmOptional; + + // Попытка чтения несушествующего фильма + filmOptional = filmDbStorage.getFilmById(10000); + assertThat(filmOptional) + .withFailMessage("При чтении несуществующего фильма должен возвращаться пустой объект") + .isEmpty(); + + filmOptional = filmDbStorage.getFilmById(film.getId()); + assertThat(filmOptional) + .isPresent() + .get() + .isEqualTo(film); } + /** + * Тестирование списка фильмов + */ @Test void findAllFilms() { + Collection films = filmDbStorage.findAllFilms(); + assertTrue(films.size() > 0, + "findAllFilms() - В базе данных отсутствует информация о фильмах."); } + /** + * Тестирование расчета популярности фильмов + */ @Test void findPopularFilms() { + // задаем "лайки" к фильмам + filmDbStorage.addNewLike(1, 1); + filmDbStorage.addNewLike(2, 1); + filmDbStorage.addNewLike(2, 2); + filmDbStorage.addNewLike(3, 1); + filmDbStorage.addNewLike(3, 2); + filmDbStorage.addNewLike(3, 3); + filmDbStorage.addNewLike(3, 4); + filmDbStorage.addNewLike(4, 4); + filmDbStorage.addNewLike(4, 2); + filmDbStorage.addNewLike(4, 3); + + Collection films = filmDbStorage.findPopularFilms(2); + List popular = new LinkedList<>(films); + assertEquals(popular.get(0), filmDbStorage.getFilmById(3).get(), + "Самый популярный фильм расчитан неверно."); + + assertEquals(popular.get(1), filmDbStorage.getFilmById(4).get(), + "Второй по популярности фильм расчитан неверно."); } + /** + * Тестирование обновления информации о фильме + */ @Test void updateFilm() { + Film film = getTestFilm(); + film.setName("filmNameUpdated"); + film.setReleaseDate(LocalDate.of(1999, 12, 31)); + film.addGenre(new Genre(6, "Боевик")); + + filmDbStorage.updateFilm(film); + + Optional filmOptional = filmDbStorage.getFilmById(film.getId()); + assertThat(filmOptional) + .isPresent() + .get() + .isEqualTo(film); } + /** + * Тестирование добавления "лайка" к фильму + */ @Test void addNewLike() { + Film film = getTestFilm(); + film.setId(null); + film.setName("TestLikeFilmName"); + film.setDescription("TestLikeFilmDescription"); + + Film filmDb = filmDbStorage.addNewFilm(film); + + Integer rank = filmDbStorage.addNewLike(filmDb.getId(), 1); + assertEquals(1, rank, "При добавлении \"лйка\" произошла ошибка."); + + rank = filmDbStorage.addNewLike(filmDb.getId(), 3); + assertEquals(2, rank, "При подсчете \"лйков\" произошла ошибка."); + + rank = filmDbStorage.addNewLike(filmDb.getId(), 3); + assertEquals(2, rank, "При добавлении повторного \"лйка\" произошла ошибка счетчика."); } + /** + * Тестирование удаления "лайка у фильма" + */ @Test void removeLike() { - } + final int userId = 1; - @Test - void getFilmRank() { + Integer expectedRank = filmDbStorage.addNewLike(TEST_FILM_ID, userId) - 1; + Integer rank = filmDbStorage.removeLike(TEST_FILM_ID, userId); + + assertEquals(expectedRank, rank, + "При удалении \"лайка\" произошла ошибка."); } + /** + * Тестирование Удаления информации о всех фильмах + */ @Test void removeAllFilms() { + Film film = getTestFilm(); + film.setId(null); + film.setName("TestFilmNameForRmove"); + filmDbStorage.addNewFilm(film); + + filmDbStorage.removeAllFilms(); + Collection films = filmDbStorage.findAllFilms(); + assertTrue(films.size() == 0, + "При удалении фильмов произошла ошибка."); } } \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/genre/GenreDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/genre/GenreDbStorageTest.java new file mode 100644 index 0000000..3d4d00b --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/genre/GenreDbStorageTest.java @@ -0,0 +1,77 @@ +package ru.yandex.practicum.filmorate.storage.genre; + +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.BeforeAll; +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.Genre; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Тестирование справочника жанров фильа + * + * Для успешного выполнения тестов, при инициализации базы данных + * должндолжен быть полностью заполнен справочник жанров. + * Файл первоначальных данных ./src/test/resources/data.sql + */ +@JdbcTest +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@Import({GenreDbStorage.class}) +class GenreDbStorageTest { + + private final GenreDbStorage genreDbStorage; + private static List testGenres = new ArrayList<>(); + + /** + * Инициализация эталонного списка жанров. + */ + @BeforeAll + static void setUp() { + testGenres.add(new Genre(1, "Комедия")); + testGenres.add(new Genre(2, "Драма")); + testGenres.add(new Genre(3, "Мультфильм")); + testGenres.add(new Genre(4, "Триллер")); + testGenres.add(new Genre(5, "Документальный")); + testGenres.add(new Genre(6, "Боевик")); + } + + /** + * Тестирование списка жанров + */ + @Test + void findAllGenres() { + Collection genres = genreDbStorage.findAllGenres(); + for (Genre genre : testGenres) { + assertTrue(genres.contains(genre), + "В базе данных отсутствует " + genre.toString()); + } + } + + /** + * Тестирование поиска жанров по идентификатору + */ + @Test + void findGenre() { + for (Genre genre : testGenres) { + Optional genreOptional = genreDbStorage.findGenre(genre.getId()); + + assertThat(genreOptional) + .isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(genre); + } + } + +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/mpa/MpaDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/mpa/MpaDbStorageTest.java new file mode 100644 index 0000000..2d0f02d --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/mpa/MpaDbStorageTest.java @@ -0,0 +1,75 @@ +package ru.yandex.practicum.filmorate.storage.mpa; + +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.BeforeAll; +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.Mpa; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Тестирование справочника рейтингов фильа + * + * Для успешного выполнения тестов, при инициализации базы данных + * должндолжен быть полностью заполнен справочник рейтингов MPA. + * Файл первоначальных данных ./src/test/resources/data.sql + */ +@JdbcTest +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +@Import({MpaDbStorage.class}) +class MpaDbStorageTest { + + private final MpaDbStorage mpaDbStorage; + private static List testMpaList = new ArrayList<>(); + + /** + * Инициализация эталонного списка рейтингов. + */ + @BeforeAll + static void setUp() { + testMpaList.add(new Mpa(1, "G", "у фильма нет возрастных ограничений")); + testMpaList.add(new Mpa(2, "PG", "детям рекомендуется смотреть фильм с родителями")); + testMpaList.add(new Mpa(3, "PG-13", "детям до 13 лет просмотр не желателен")); + testMpaList.add(new Mpa(4, "R", "лицам до 17 лет просматривать фильм можно только в присутствии взрослого")); + testMpaList.add(new Mpa(5, "NC-17", "лицам до 18 лет просмотр запрещён")); + } + + /** + * Тестирование списка рейтингов + */ + @Test + void findAllMpa() { + Collection genres = mpaDbStorage.findAllMpa(); + for (Mpa mpa : testMpaList) { + assertTrue(genres.contains(mpa), + "В базе данных отсутствует " + mpa.toString()); + } + } + + /** + * Тестирование поиска рейтинга по идентификатору + */ + @Test + void findMpaById() { + for (Mpa mpa : testMpaList) { + Optional mpaOptional = mpaDbStorage.findMpa(mpa.getId()); + assertThat(mpaOptional) + .isPresent() + .get() + .usingRecursiveComparison() + .isEqualTo(mpa); + } + } + +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorageTest.java index 8eac04b..f20c530 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorageTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorageTest.java @@ -28,9 +28,10 @@ @RequiredArgsConstructor(onConstructor_ = @Autowired) @Import({UserDbStorage.class}) class UserDbStorageTest { - private final UserDbStorage userDbStorage; public static final int TEST_USER_ID = 1; + private final UserDbStorage userDbStorage; + /** * Генерация тестового пользователя * @@ -77,7 +78,6 @@ void addNewUser() { "addNewUser() - При добавлении пользователя в базу должен быть присвоен не нулевой идентификатор"); Optional userOptional = userDbStorage.getUserById(userDb.getId()); - assertThat(userOptional) .isPresent() .get() @@ -159,6 +159,18 @@ void getUserFriends() { assertTrue(friends.size() == 3, "getUserFriends() - Количество друзй пользователя id=" + TEST_USER_ID + " не соответствует ожидаемому."); + + User testFriend = userDbStorage.getUserById(2).get(); + assertTrue(friends.contains(testFriend), + "getUserFriends() - В списке друзей не найден " + testFriend.toString()); + + testFriend = userDbStorage.getUserById(3).get(); + assertTrue(friends.contains(testFriend), + "getUserFriends() - В списке друзей не найден " + testFriend.toString()); + + testFriend = userDbStorage.getUserById(4).get(); + assertTrue(friends.contains(testFriend), + "getUserFriends() - В списке друзей не найден " + testFriend.toString()); } /** diff --git a/src/test/resources/data.sql b/src/test/resources/data.sql index 472c2e8..b070a66 100644 --- a/src/test/resources/data.sql +++ b/src/test/resources/data.sql @@ -22,3 +22,13 @@ VALUES ( 'test@test.com', 'testLogin', 'testName', '2001-9-22' ), ('user2@test.com', 'userTest2', 'userNane2', '2002-02-02'), ('user3@test.com', 'userTest3', 'userNane3', '2003-03-03'); +-- Создаем тестовые фильмы +INSERT INTO films (name, description, releasedate, len_min, mpa_id) +VALUES ( 'TestFilmName', 'TestFilmDescription', '2001-02-03', 51, 1), + ( 'TestFilmName2', 'TestFilmDescription2', '2002-03-04', 62, 2), + ( 'TestFilmName3', 'TestFilmDescription3', '2003-04-05', 73, 3), + ( 'TestFilmName4', 'TestFilmDescription4', '2004-05-06', 92, 4); + +INSERT INTO films_genres (film_id, genre_id) +VALUES ( 1, 1 ); + From 027cfbb258014432bdea0eea1975e1533e20d7cc Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Mon, 24 Feb 2025 23:37:35 +0700 Subject: [PATCH 15/19] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=D1=8B=20=D1=82=D0=B5=D1=81=D1=82=D1=8B?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- .../filmorate/FilmorateApplication.java | 16 +- .../filmorate/controller/FilmController.java | 2 +- .../filmorate/controller/GenreController.java | 2 - .../practicum/filmorate/model/Film.java | 2 - .../filmorate/service/UserService.java | 1 - .../filmorate/storage/film/FilmDbStorage.java | 548 +++++++++--------- .../storage/film/InMemoryFilmStorage.java | 1 - .../filmorate/storage/genre/GenreStorage.java | 1 + .../filmorate/storage/mpa/MpaStorage.java | 1 + src/main/resources/schema.sql | 66 +-- .../filmorate/FilmorateApplicationTests.java | 10 +- .../controller/FilmControllerTest.java | 191 +++++- .../controller/GenreControllerTest.java | 94 +++ .../controller/MpaControllerTest.java | 98 ++++ .../controller/UserControllerTest.java | 11 +- .../practicum/filmorate/model/FilmTest.java | 1 - .../practicum/filmorate/model/UserTest.java | 1 - .../storage/genre/GenreDbStorageTest.java | 4 +- .../storage/mpa/MpaDbStorageTest.java | 2 +- .../storage/user/UserDbStorageTest.java | 2 +- 21 files changed, 709 insertions(+), 349 deletions(-) create mode 100644 src/test/java/ru/yandex/practicum/filmorate/controller/GenreControllerTest.java create mode 100644 src/test/java/ru/yandex/practicum/filmorate/controller/MpaControllerTest.java diff --git a/README.md b/README.md index a2883fd..762899d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Учебный проект. Созданиие приложений на основе шаблона "Spring"
-## Спринт 12 (часть 1) +## Спринт 12 Добавляем базу данных. ![схема базы данных](/schema.png) @@ -33,7 +33,7 @@ 4. **MPA** - таблица описания рейтингов Ассоциации кинокомпаний (MPA).
поля: - первичный ключ *id* - идентификатор рейтинга; - - *code* - буквенный код рейтинга (G, PG, PG-13, R, NC-17); + - *name* - буквенный код рейтинга (G, PG, PG-13, R, NC-17); - *description* - описание рейтинга;
diff --git a/src/main/java/ru/yandex/practicum/filmorate/FilmorateApplication.java b/src/main/java/ru/yandex/practicum/filmorate/FilmorateApplication.java index 643c0c2..615ea3a 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/FilmorateApplication.java +++ b/src/main/java/ru/yandex/practicum/filmorate/FilmorateApplication.java @@ -9,12 +9,12 @@ @SpringBootApplication public class FilmorateApplication { - /** - * Запуск приложения. - * - * @param args - параметры запуска. - */ - public static void main(final String[] args) { - SpringApplication.run(FilmorateApplication.class, args); - } + /** + * Запуск приложения. + * + * @param args - параметры запуска. + */ + public static void main(final String[] args) { + SpringApplication.run(FilmorateApplication.class, args); + } } 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 06fccc5..eb598a1 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -6,8 +6,8 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import ru.yandex.practicum.filmorate.model.Film; -import ru.yandex.practicum.filmorate.validator.Marker; import ru.yandex.practicum.filmorate.service.FilmService; +import ru.yandex.practicum.filmorate.validator.Marker; import java.util.Collection; import java.util.Map; diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java index de38ece..d9c870d 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/GenreController.java @@ -4,9 +4,7 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; import ru.yandex.practicum.filmorate.model.Genre; -import ru.yandex.practicum.filmorate.model.User; import ru.yandex.practicum.filmorate.service.GenreService; -import ru.yandex.practicum.filmorate.service.UserService; import java.util.Collection; 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 7f48505..06792cc 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java @@ -6,12 +6,10 @@ import jakarta.validation.constraints.Size; import lombok.Data; import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; import ru.yandex.practicum.filmorate.validator.LegalFilmDate; import ru.yandex.practicum.filmorate.validator.Marker; import java.time.LocalDate; -import java.util.HashSet; import java.util.LinkedHashSet; /** 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 e501188..fc664b8 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java @@ -6,7 +6,6 @@ import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.exception.ValidationException; import ru.yandex.practicum.filmorate.model.User; -import ru.yandex.practicum.filmorate.storage.user.UserDbStorage; import ru.yandex.practicum.filmorate.storage.user.UserStorage; import java.util.Collection; diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java index 42637c7..0b868c6 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java @@ -43,315 +43,315 @@ public class FilmDbStorage implements FilmStorage { @Autowired private NamedParameterJdbcTemplate jdbc; -/** - * Добавление информации о фильме - * - * @param newFilm - объект для добавления - * @return - подтвержденный объект - */ -@Override -public Film addNewFilm(Film newFilm) { - GeneratedKeyHolder generatedKeyHolder = new GeneratedKeyHolder(); + /** + * Добавление информации о фильме + * + * @param newFilm - объект для добавления + * @return - подтвержденный объект + */ + @Override + public Film addNewFilm(Film newFilm) { + GeneratedKeyHolder generatedKeyHolder = new GeneratedKeyHolder(); - // сохраняем информацию о фильме в базу данных - try { - jdbc.update(SQL_INSERT_FILM, - new MapSqlParameterSource() - .addValue("name", newFilm.getName()) - .addValue("description", newFilm.getDescription()) - .addValue("releasedate", newFilm.getReleaseDate(), Types.DATE) - .addValue("len_min", newFilm.getDuration()) - .addValue("mpa_id", newFilm.getMpa().getId()), - generatedKeyHolder - ); - } catch (DataAccessException e) { - throw new NotFoundException("Получены недопустимые параметры запроса: " + - e.getMessage()); - } - - // получаем идентификатор фильма - final Integer filmId = generatedKeyHolder.getKey().intValue(); - newFilm.setId(filmId); + // сохраняем информацию о фильме в базу данных + try { + jdbc.update(SQL_INSERT_FILM, + new MapSqlParameterSource() + .addValue("name", newFilm.getName()) + .addValue("description", newFilm.getDescription()) + .addValue("releasedate", newFilm.getReleaseDate(), Types.DATE) + .addValue("len_min", newFilm.getDuration()) + .addValue("mpa_id", newFilm.getMpa().getId()), + generatedKeyHolder + ); + } catch (DataAccessException e) { + throw new NotFoundException("Получены недопустимые параметры запроса: " + + e.getMessage()); + } - // добавляем жанры Фильма Если определены - if (!newFilm.getGenres().isEmpty()) { - SqlParameterSource[] batch = newFilm.getGenres().stream() - .map(genre -> new MapSqlParameterSource() - .addValue("film_id", filmId) - .addValue("genre_id", genre.getId())) - .toArray(SqlParameterSource[]::new); - jdbc.batchUpdate(SQL_UPDATE_GENRES, batch); - } + // получаем идентификатор фильма + final Integer filmId = generatedKeyHolder.getKey().intValue(); + newFilm.setId(filmId); - return getFilmById(filmId).orElseThrow(() -> - new InternalServerException("Ошибка при добавлении фильма.")); -} - -/** - * Поиск фильма по идентификатору - * - * @param id - идентификатор фильма - * @return - объект описания фильма - */ -@Override -public Optional getFilmById(Integer id) { - try { - Film film = jdbc.query(SQL_FIND_FILM_BY_ID, - new MapSqlParameterSource() - .addValue("id", id), - new ResultSetExtractor() { - @Override - public Film extractData(ResultSet rs) throws SQLException, DataAccessException { - rs.next(); - Film filmRs = new FilmRowMapper().mapRow(rs, 1); - Integer mpaId = rs.getInt("mpa_id"); - if (mpaId != null) { - Mpa mpa = new Mpa(); - mpa.setId(mpaId); - mpa.setName(rs.getString("mpa_name")); - filmRs.setMpa(mpa); - } - do { - Integer genreId = rs.getInt("genre_id"); - if (genreId != 0) { - Genre genre = new Genre(); - genre.setId(genreId); - genre.setName(rs.getString("genre_name")); - filmRs.addGenre(genre); - } - } while (rs.next()); - return filmRs; - } - } - ); - return Optional.ofNullable(film); + // добавляем жанры Фильма Если определены + if (!newFilm.getGenres().isEmpty()) { + SqlParameterSource[] batch = newFilm.getGenres().stream() + .map(genre -> new MapSqlParameterSource() + .addValue("film_id", filmId) + .addValue("genre_id", genre.getId())) + .toArray(SqlParameterSource[]::new); + jdbc.batchUpdate(SQL_UPDATE_GENRES, batch); + } - } catch (DataAccessException ignored) { - return Optional.empty(); + return getFilmById(filmId).orElseThrow(() -> + new InternalServerException("Ошибка при добавлении фильма.")); } -} -/** - * Поиск всех фильмов - * - * @return - список фильмов - */ -@Override -public Collection findAllFilms() { - try { - // Загружаем из базы данных все фильмы - Map filmsMap = new HashMap<>(); - filmsMap = jdbc.query("SELECT f.*, mpa.name as mpa_name FROM films AS f INNER JOIN mpa ON f.mpa_id = mpa.id", - new ResultSetExtractor>() { - @Override - public Map extractData(ResultSet rs) - throws SQLException, DataAccessException { - Map fMap = new HashMap<>(); - while (rs.next()) { - Film film = new FilmRowMapper().mapRow(rs, 1); + /** + * Поиск фильма по идентификатору + * + * @param id - идентификатор фильма + * @return - объект описания фильма + */ + @Override + public Optional getFilmById(Integer id) { + try { + Film film = jdbc.query(SQL_FIND_FILM_BY_ID, + new MapSqlParameterSource() + .addValue("id", id), + new ResultSetExtractor() { + @Override + public Film extractData(ResultSet rs) throws SQLException, DataAccessException { + rs.next(); + Film filmRs = new FilmRowMapper().mapRow(rs, 1); Integer mpaId = rs.getInt("mpa_id"); - if (mpaId != 0) { + if (mpaId != null) { Mpa mpa = new Mpa(); mpa.setId(mpaId); mpa.setName(rs.getString("mpa_name")); - film.setMpa(mpa); + filmRs.setMpa(mpa); } - fMap.put(film.getId(), film); + do { + Integer genreId = rs.getInt("genre_id"); + if (genreId != 0) { + Genre genre = new Genre(); + genre.setId(genreId); + genre.setName(rs.getString("genre_name")); + filmRs.addGenre(genre); + } + } while (rs.next()); + return filmRs; } - return fMap; } - }); + ); + return Optional.ofNullable(film); - // загружаем из базы данных все ссылки на жанры - List filmsGenres; - filmsGenres = jdbc.query( - "SELECT fg.*, g.name AS genre_name FROM films_genres AS fg INNER JOIN genres AS g ON fg.GENRE_ID = g.ID", - new FilmGenreRowMapper()); - - // пополням фильмы сведениями о жанрах - for (FilmGenre filmGenre : filmsGenres) { - Genre genre = new Genre(); - int filmId = filmGenre.getFilmId(); - genre.setId(filmGenre.getGenreId()); - genre.setName(filmGenre.getGenreName()); - filmsMap.get(filmId).addGenre(genre); + } catch (DataAccessException ignored) { + return Optional.empty(); } + } - return filmsMap.values(); + /** + * Поиск всех фильмов + * + * @return - список фильмов + */ + @Override + public Collection findAllFilms() { + try { + // Загружаем из базы данных все фильмы + Map filmsMap = new HashMap<>(); + filmsMap = jdbc.query("SELECT f.*, mpa.name as mpa_name FROM films AS f INNER JOIN mpa ON f.mpa_id = mpa.id", + new ResultSetExtractor>() { + @Override + public Map extractData(ResultSet rs) + throws SQLException, DataAccessException { + Map fMap = new HashMap<>(); + while (rs.next()) { + Film film = new FilmRowMapper().mapRow(rs, 1); + Integer mpaId = rs.getInt("mpa_id"); + if (mpaId != 0) { + Mpa mpa = new Mpa(); + mpa.setId(mpaId); + mpa.setName(rs.getString("mpa_name")); + film.setMpa(mpa); + } + fMap.put(film.getId(), film); + } + return fMap; + } + }); - } catch (EmptyResultDataAccessException ignored) { - return List.of(); - } + // загружаем из базы данных все ссылки на жанры + List filmsGenres; + filmsGenres = jdbc.query( + "SELECT fg.*, g.name AS genre_name FROM films_genres AS fg INNER JOIN genres AS g ON fg.GENRE_ID = g.ID", + new FilmGenreRowMapper()); -} + // пополням фильмы сведениями о жанрах + for (FilmGenre filmGenre : filmsGenres) { + Genre genre = new Genre(); + int filmId = filmGenre.getFilmId(); + genre.setId(filmGenre.getGenreId()); + genre.setName(filmGenre.getGenreName()); + filmsMap.get(filmId).addGenre(genre); + } -/** - * Поиск популярных фильмов - * - * @param count - * @return - */ -@Override -public Collection findPopularFilms(int count) { - String sql = "SELECT f.*, mpa.name AS mpa_name, popular.count_film\n" + - "FROM (films AS f INNER JOIN mpa ON f.MPA_ID = mpa.ID\n)" + - " LEFT OUTER JOIN\n" + - " (SELECT film_id, count(film_id) as count_film\n" + - " FROM LIKES GROUP BY film_id) AS popular\n" + - " ON f.id = popular.film_id\n" + - "ORDER BY popular.count_film DESC\n" + - "LIMIT :count"; + return filmsMap.values(); - Map popularFilms; - try { - popularFilms = jdbc.query(sql, new MapSqlParameterSource() - .addValue("count", count), - new ResultSetExtractor>() { - @Override - public Map extractData(ResultSet rs) - throws SQLException, DataAccessException { - Map mapFilm = new LinkedHashMap<>(); - while (rs.next()) { - Film film = new FilmRowMapper().mapRow(rs, 1); - Integer mpaId = rs.getInt("mpa_id"); - if (mpaId != 0) { - Mpa mpa = new Mpa(); - mpa.setId(mpaId); - mpa.setName(rs.getString("mpa_name")); - film.setMpa(mpa); + } catch (EmptyResultDataAccessException ignored) { + return List.of(); + } + + } + + /** + * Поиск популярных фильмов + * + * @param count + * @return + */ + @Override + public Collection findPopularFilms(int count) { + String sql = "SELECT f.*, mpa.name AS mpa_name, popular.count_film\n" + + "FROM (films AS f INNER JOIN mpa ON f.MPA_ID = mpa.ID\n)" + + " LEFT OUTER JOIN\n" + + " (SELECT film_id, count(film_id) as count_film\n" + + " FROM LIKES GROUP BY film_id) AS popular\n" + + " ON f.id = popular.film_id\n" + + "ORDER BY popular.count_film DESC\n" + + "LIMIT :count"; + + Map popularFilms; + try { + popularFilms = jdbc.query(sql, new MapSqlParameterSource() + .addValue("count", count), + new ResultSetExtractor>() { + @Override + public Map extractData(ResultSet rs) + throws SQLException, DataAccessException { + Map mapFilm = new LinkedHashMap<>(); + while (rs.next()) { + Film film = new FilmRowMapper().mapRow(rs, 1); + Integer mpaId = rs.getInt("mpa_id"); + if (mpaId != 0) { + Mpa mpa = new Mpa(); + mpa.setId(mpaId); + mpa.setName(rs.getString("mpa_name")); + film.setMpa(mpa); + } + mapFilm.put(film.getId(), film); } - mapFilm.put(film.getId(), film); + return mapFilm; } - return mapFilm; - } - }); + }); - // загружаем из базы данных соответствующие ссылки на жанры - List ids = new ArrayList<>(popularFilms.keySet()); - List filmsGenres; - filmsGenres = jdbc.query( - "SELECT fg.*, " + - "g.name AS genre_name " + - "FROM films_genres AS fg INNER JOIN genres AS g ON fg.genre_id = g.id " + - "WHERE fg.film_id IN (:values)", - new MapSqlParameterSource() - .addValue("values", ids), - new FilmGenreRowMapper()); + // загружаем из базы данных соответствующие ссылки на жанры + List ids = new ArrayList<>(popularFilms.keySet()); + List filmsGenres; + filmsGenres = jdbc.query( + "SELECT fg.*, " + + "g.name AS genre_name " + + "FROM films_genres AS fg INNER JOIN genres AS g ON fg.genre_id = g.id " + + "WHERE fg.film_id IN (:values)", + new MapSqlParameterSource() + .addValue("values", ids), + new FilmGenreRowMapper()); - // пополням фильмы сведениями о жанрах - for (FilmGenre filmGenre : filmsGenres) { - Genre genre = new Genre(); - int filmId = filmGenre.getFilmId(); - genre.setId(filmGenre.getGenreId()); - genre.setName(filmGenre.getGenreName()); - popularFilms.get(filmId).addGenre(genre); - } - return popularFilms.values(); + // пополням фильмы сведениями о жанрах + for (FilmGenre filmGenre : filmsGenres) { + Genre genre = new Genre(); + int filmId = filmGenre.getFilmId(); + genre.setId(filmGenre.getGenreId()); + genre.setName(filmGenre.getGenreName()); + popularFilms.get(filmId).addGenre(genre); + } + return popularFilms.values(); - } catch (EmptyResultDataAccessException ignored) { - return List.of(); + } catch (EmptyResultDataAccessException ignored) { + return List.of(); + } } -} -/** - * Обновление сведений о фильме - * - * @param updFilm - обновленный объект - */ -@Override -public void updateFilm(Film updFilm) { - // задаем параметры SQL запоса - MapSqlParameterSource params = new MapSqlParameterSource(); - params.addValue("name", updFilm.getName()); - params.addValue("description", updFilm.getDescription()); - params.addValue("releasedate", updFilm.getReleaseDate(), Types.DATE); - params.addValue("len_min", updFilm.getDuration()); - params.addValue("mpa_id", updFilm.getMpa().getId()); - params.addValue("id", updFilm.getId()); + /** + * Обновление сведений о фильме + * + * @param updFilm - обновленный объект + */ + @Override + public void updateFilm(Film updFilm) { + // задаем параметры SQL запоса + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("name", updFilm.getName()); + params.addValue("description", updFilm.getDescription()); + params.addValue("releasedate", updFilm.getReleaseDate(), Types.DATE); + params.addValue("len_min", updFilm.getDuration()); + params.addValue("mpa_id", updFilm.getMpa().getId()); + params.addValue("id", updFilm.getId()); - // обновляем информацию о фильме - int rowsUpdated = jdbc.update(SQL_UPDATE_FILM, params); - if (rowsUpdated == 0) { - throw new InternalServerException("Не удалось обновить информацию о фильие"); - } + // обновляем информацию о фильме + int rowsUpdated = jdbc.update(SQL_UPDATE_FILM, params); + if (rowsUpdated == 0) { + throw new InternalServerException("Не удалось обновить информацию о фильие"); + } - // Удаляем все жанры которые были определены для фильма - int filmId = updFilm.getId(); - jdbc.update("DELETE FROM films_genres WHERE film_id = :filmId", - new MapSqlParameterSource() - .addValue("filmId", filmId)); + // Удаляем все жанры которые были определены для фильма + int filmId = updFilm.getId(); + jdbc.update("DELETE FROM films_genres WHERE film_id = :filmId", + new MapSqlParameterSource() + .addValue("filmId", filmId)); - // добавляем жанры Фильма если определены новые - if (updFilm.getGenres().size() > 0) { - SqlParameterSource[] batch = updFilm.getGenres().stream() - .map(genre -> new MapSqlParameterSource() - .addValue("film_id", updFilm.getId()) - .addValue("genre_id", genre.getId())) - .toArray(SqlParameterSource[]::new); - jdbc.batchUpdate(SQL_UPDATE_GENRES, batch); + // добавляем жанры Фильма если определены новые + if (updFilm.getGenres().size() > 0) { + SqlParameterSource[] batch = updFilm.getGenres().stream() + .map(genre -> new MapSqlParameterSource() + .addValue("film_id", updFilm.getId()) + .addValue("genre_id", genre.getId())) + .toArray(SqlParameterSource[]::new); + jdbc.batchUpdate(SQL_UPDATE_GENRES, batch); + } } -} -/** - * Добавление "лайка" к фильму. - * - * @param filmId - идентификатор фильма - * @param userId - идентификатор пользователя - * @return - число "лайков" - */ -@Override -public Integer addNewLike(Integer filmId, Integer userId) { - int rowsUpdated = jdbc.update(SQL_ADD_LIKE, new MapSqlParameterSource() - .addValue("userId", userId) - .addValue("filmId", filmId) - ); - if (rowsUpdated == 0) { - throw new InternalServerException("Не удалось обновить данные"); + /** + * Добавление "лайка" к фильму. + * + * @param filmId - идентификатор фильма + * @param userId - идентификатор пользователя + * @return - число "лайков" + */ + @Override + public Integer addNewLike(Integer filmId, Integer userId) { + int rowsUpdated = jdbc.update(SQL_ADD_LIKE, new MapSqlParameterSource() + .addValue("userId", userId) + .addValue("filmId", filmId) + ); + if (rowsUpdated == 0) { + throw new InternalServerException("Не удалось обновить данные"); + } + return getFilmRank(filmId); } - return getFilmRank(filmId); -} -/** - * Удаление "лайка" у фильма. - * - * @param filmId - идентификатор фильма - * @param userId - идентификатор пользователя - * @return - число "лайков" - */ -@Override -public Integer removeLike(Integer filmId, Integer userId) { - jdbc.update(SQL_REMOVE_LIKE, new MapSqlParameterSource() - .addValue("userId", userId) - .addValue("filmId", filmId) - ); - return getFilmRank(filmId); -} + /** + * Удаление "лайка" у фильма. + * + * @param filmId - идентификатор фильма + * @param userId - идентификатор пользователя + * @return - число "лайков" + */ + @Override + public Integer removeLike(Integer filmId, Integer userId) { + jdbc.update(SQL_REMOVE_LIKE, new MapSqlParameterSource() + .addValue("userId", userId) + .addValue("filmId", filmId) + ); + return getFilmRank(filmId); + } -/** - * Подсчет "лайков" фильма. - * - * @param filmId - идентификатор фильма - * @return - число "лайков" - */ -@Override -public Integer getFilmRank(Integer filmId) { - try { - return jdbc.queryForObject("SELECT count(film_id) FROM likes WHERE film_id = :filmId", - new MapSqlParameterSource() - .addValue("filmId", filmId), - Integer.class); - } catch (EmptyResultDataAccessException ignored) { - throw new NotFoundException("Информация о популярности фильма не найдена. id:" + filmId); + /** + * Подсчет "лайков" фильма. + * + * @param filmId - идентификатор фильма + * @return - число "лайков" + */ + @Override + public Integer getFilmRank(Integer filmId) { + try { + return jdbc.queryForObject("SELECT count(film_id) FROM likes WHERE film_id = :filmId", + new MapSqlParameterSource() + .addValue("filmId", filmId), + Integer.class); + } catch (EmptyResultDataAccessException ignored) { + throw new NotFoundException("Информация о популярности фильма не найдена. id:" + filmId); + } } -} -@Override -public void removeAllFilms() { - jdbc.update("DELETE FROM likes", new MapSqlParameterSource() - .addValue("table", "likes")); - jdbc.update("DELETE FROM films_genres", new MapSqlParameterSource() - .addValue("table", "films_genres")); - jdbc.update("DELETE FROM films", new MapSqlParameterSource() - .addValue("table", "films")); -} + @Override + public void removeAllFilms() { + jdbc.update("DELETE FROM likes", new MapSqlParameterSource() + .addValue("table", "likes")); + jdbc.update("DELETE FROM films_genres", new MapSqlParameterSource() + .addValue("table", "films_genres")); + jdbc.update("DELETE FROM films", new MapSqlParameterSource() + .addValue("table", "films")); + } } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java index 143df61..a3b0dce 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java @@ -1,6 +1,5 @@ package ru.yandex.practicum.filmorate.storage.film; -import org.springframework.stereotype.Component; import org.springframework.stereotype.Repository; import ru.yandex.practicum.filmorate.model.Film; diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/genre/GenreStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/genre/GenreStorage.java index 1d0a804..5931bd8 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/genre/GenreStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/genre/GenreStorage.java @@ -7,5 +7,6 @@ public interface GenreStorage { Collection findAllGenres(); + Optional findGenre(Integer id); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/mpa/MpaStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/mpa/MpaStorage.java index a009238..4efc2d4 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/mpa/MpaStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/mpa/MpaStorage.java @@ -7,5 +7,6 @@ public interface MpaStorage { Collection findAllMpa(); + Optional findMpa(Integer id); } diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 3724252..874def3 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -1,53 +1,53 @@ -- Создаем таблицу пользователей CREATE TABLE IF NOT EXISTS users ( - id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - email VARCHAR(255) NOT NULL, - login VARCHAR(40) NOT NULL, - name VARCHAR(40) NOT NULL, - birthday DATE NOT NULL - ); + id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + email VARCHAR(255) NOT NULL, + login VARCHAR(40) NOT NULL, + name VARCHAR(40) NOT NULL, + birthday DATE NOT NULL +); -- Создаем таблицу друзей CREATE TABLE IF NOT EXISTS friends ( - user_id INTEGER NOT NULL REFERENCES users(id), - friend_id INTEGER NOT NULL REFERENCES users(id), - confirmed BOOLEAN NOT NULL DEFAULT FALSE, - PRIMARY KEY (user_id, friend_id) - ); + user_id INTEGER NOT NULL REFERENCES users(id), + friend_id INTEGER NOT NULL REFERENCES users(id), + confirmed BOOLEAN NOT NULL DEFAULT FALSE, + PRIMARY KEY (user_id, friend_id) +); -- Создаем справочник жанров фильма CREATE TABLE IF NOT EXISTS genres ( - id INTEGER PRIMARY KEY, - name VARCHAR(40) NOT NULL - ); + id INTEGER PRIMARY KEY, + name VARCHAR(40) NOT NULL +); -- Создаем справочник рейтинга MPA CREATE TABLE IF NOT EXISTS MPA ( - id INTEGER PRIMARY KEY, - name VARCHAR(8) NOT NULL, - description VARCHAR(80) - ); + id INTEGER PRIMARY KEY, + name VARCHAR(8) NOT NULL, + description VARCHAR(80) +); -- Создаем таблицу описания фильма CREATE TABLE IF NOT EXISTS films ( - id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - name VARCHAR(40) NOT NULL, - description VARCHAR(200), - releaseDate DATE, - len_min INTEGER, - MPA_id INTEGER NOT NULL REFERENCES MPA(id) - ); + id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name VARCHAR(40) NOT NULL, + description VARCHAR(200), + releaseDate DATE, + len_min INTEGER, + MPA_id INTEGER NOT NULL REFERENCES MPA(id) +); -- Создаем таблицу описания жанра фильма CREATE TABLE IF NOT EXISTS films_genres ( - film_id INTEGER NOT NULL REFERENCES films(id), - genre_id INTEGER NOT NULL REFERENCES genres(id), - PRIMARY KEY (film_id, genre_id) - ); + film_id INTEGER NOT NULL REFERENCES films(id), + genre_id INTEGER NOT NULL REFERENCES genres(id), + PRIMARY KEY (film_id, genre_id) +); -- Создаем таблицу "лайков" к фильмам CREATE TABLE IF NOT EXISTS likes ( - user_id INTEGER NOT NULL REFERENCES users(id), - film_id INTEGER NOT NULL REFERENCES films(id), - PRIMARY KEY (user_id, film_id) - ); + user_id INTEGER NOT NULL REFERENCES users(id), + film_id INTEGER NOT NULL REFERENCES films(id), + PRIMARY KEY (user_id, film_id) +); diff --git a/src/test/java/ru/yandex/practicum/filmorate/FilmorateApplicationTests.java b/src/test/java/ru/yandex/practicum/filmorate/FilmorateApplicationTests.java index 660412e..02f0bf6 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/FilmorateApplicationTests.java +++ b/src/test/java/ru/yandex/practicum/filmorate/FilmorateApplicationTests.java @@ -1,13 +1,13 @@ package ru.yandex.practicum.filmorate; import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest +@AutoConfigureTestDatabase class FilmorateApplicationTests { - - @Test - void contextLoads() { - } - + @Test + void contextLoads() throws Exception { + } } diff --git a/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java index 8d2b2c4..2b24a07 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/controller/FilmControllerTest.java @@ -2,30 +2,45 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; import lombok.RequiredArgsConstructor; 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.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import ru.yandex.practicum.filmorate.model.Film; -import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.model.Mpa; import ru.yandex.practicum.filmorate.validator.LocalDateAdapter; import java.time.LocalDate; import java.util.List; -import static org.junit.jupiter.api.Assertions.*; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +/** + * Тестируем контроллер запросов работы с данными о фильмах + *

+ * Для успешного выполнения тестов, при инициализации базы данных + * должна быть подготовлена информация о четырех тестовых фильмах и + * о четырех тестовых пользователях. + * Файл первоначальных данных ./src/test/resources/data.sql + */ @SpringBootTest @AutoConfigureMockMvc @AutoConfigureTestDatabase @RequiredArgsConstructor(onConstructor_ = @Autowired) class FilmControllerTest { + private static final int TEST_FILM_ID = 1; + @Autowired MockMvc mvc; @@ -34,40 +49,194 @@ class FilmControllerTest { .registerTypeAdapter(LocalDate.class, new LocalDateAdapter()) .create(); + // Определяем тип сериализации списка + class FilmListTypeToken extends TypeToken> { + } + + /** + * Генерация информации о тестовом фильме + * Поля должны соответствовать содержимому базы данных для фильма TEST_FILM_ID + * + * @return - объект, который ожидается для TEST_FILM_ID + */ + static Film getTestFilm() { + Film film = new Film(); + film.setId(TEST_FILM_ID); + film.setName("TestFilmName"); + film.setDescription("TestFilmDescription"); + film.setReleaseDate(LocalDate.of(2001, 2, 3)); + film.setDuration(51); + film.setMpa(new Mpa(1)); + film.addGenre(new Genre(1, "Комедия")); + return film; + } + /** * Тестируем поиск всех фильмов */ @Test - void findAllFilms() throws Exception { + void findAllFilms() throws Exception { MvcResult result = mvc.perform(get("/films")) .andExpect(status().isOk()) // ожидается код статус 200 .andReturn(); - List films = gson.fromJson(result.getResponse().getContentAsString(), List.class); + List films = gson.fromJson(result.getResponse().getContentAsString(), + new FilmListTypeToken().getType()); assertTrue(!films.isEmpty(), "Список фильмов пуст."); } + /** + * Тестируем поиск фильма по идентификатору + */ @Test - void findFilm() { + void findFilm() throws Exception { + // попытка поиска несуществующего фильма + mvc.perform(get("/films/10000")) + .andExpect(status().isNotFound()); // ожидается код статус 404 + + // поиск тестового фильма + MvcResult result = mvc.perform(get("/films/" + TEST_FILM_ID)) + .andExpect(status().isOk()) // ожидается код статус 200 + .andReturn(); + Film filmDb = gson.fromJson(result.getResponse().getContentAsString(), Film.class); + assertThat(filmDb) + .withFailMessage("Считанный объект не соответствует ожидаемому.") + .isEqualTo(getTestFilm()); } + /** + * Тестируем добавление информации о новом фильме. + */ @Test - void findPopularFilms() { + void addNewFilm() throws Exception { + Film film = getTestFilm(); + film.setId(null); + film.setName("NewTestFilm"); + String jsonString = gson.toJson(film); + + // При успешном добавлении фильма + // должен возвращаться статус 201 "Created" + MvcResult result = mvc.perform(post("/films") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andReturn(); + Film filmDb = gson.fromJson(result.getResponse().getContentAsString(), Film.class); + assertNotNull(filmDb.getId(), + "при добавлении фильма должен присваиваться ненулевой идентификатор."); } + /** + * Тестируем обновление информации о фильме + */ @Test - void addNewFilm() { + void updateFilm() throws Exception { + Film film = getTestFilm(); + film.setId(null); + film.setName("TestFilmForUpdate"); + String jsonString = gson.toJson(film); + + // При успешном добавлении фильма + // должен возвращаться статус 201 "Created" + MvcResult result = mvc.perform(post("/films") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andReturn(); + Film filmDb = gson.fromJson(result.getResponse().getContentAsString(), Film.class); + + // Готовим информацию для обновления + film.setName("TestFilmNameUpdated"); + film.setDescription("Description updated"); + film.addGenre(new Genre(3, "Мультфильм")); + jsonString = gson.toJson(film); + + jsonString = gson.toJson(film); + // При обновлении фильма с отсутствующим id + // должен возвращаться статус 400 "BadRequest" + mvc.perform(put("/films") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + + film.setId(1000); + jsonString = gson.toJson(film); + // При обновлении фильма с неверным id + // должен возвращаться статус 404 "NotFound" + mvc.perform(put("/films") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + + film.setId(filmDb.getId()); + jsonString = gson.toJson(film); + // При обновлении фильма с корректным id + // должен возвращаться статус 200 "Ok" + result = mvc.perform(put("/films") + .content(jsonString) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn(); + filmDb = gson.fromJson(result.getResponse().getContentAsString(), Film.class); + assertThat(filmDb) + .withFailMessage("Обновленный объект не соответствует ожидаемому.") + .isEqualTo(film); } + /** + * Тестируем добавление "лайка" + * + * @throws Exception + */ @Test - void updateFilm() { + void addLike() throws Exception { + // При добавлении "лайка" от несуществующего пользователя + // должен возвращаться статус 404 "NotFound" + mvc.perform(put("/films/1/like/1000") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + + // При добавлении "лайка" + // должен возвращаться статус 200 "Ok" + mvc.perform(put("/films/1/like/1") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); } + /** + * Тестируем удаление "лайка" + */ @Test - void addLike() { + void removeLike() throws Exception { + addLike(); + + // При удалении "лайка" + // должен возвращаться статус 200 "Ok" + mvc.perform(delete("/films/1/like/1") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); } @Test - void removeLike() { + void findPopularFilms() throws Exception { + mvc.perform(put("/films/1/like/1").contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk()); + mvc.perform(put("/films/2/like/1").contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk()); + mvc.perform(put("/films/2/like/2").contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk()); + mvc.perform(put("/films/3/like/1").contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk()); + mvc.perform(put("/films/3/like/3").contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk()); + mvc.perform(put("/films/3/like/2").contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk()); + mvc.perform(put("/films/3/like/4").contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk()); + mvc.perform(put("/films/4/like/2").contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk()); + mvc.perform(put("/films/4/like/3").contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk()); + mvc.perform(put("/films/4/like/3").contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk()); + + MvcResult result = mvc.perform(get("/films/popular?count=2")) + .andExpect(status().isOk()) // ожидается код статус 200 + .andReturn(); + List filmsPopular = gson.fromJson(result.getResponse().getContentAsString(), + new FilmListTypeToken().getType()); + assertTrue(filmsPopular.size() == 2, + "Число популярных фильмов не соответствует ожидаемому"); } + } \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/controller/GenreControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/controller/GenreControllerTest.java new file mode 100644 index 0000000..84245c1 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/controller/GenreControllerTest.java @@ -0,0 +1,94 @@ +package ru.yandex.practicum.filmorate.controller; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.BeforeAll; +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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import ru.yandex.practicum.filmorate.model.Genre; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Тестирование контроллера запросов работы со справочником жанров фильа + *

+ * Для успешного выполнения тестов, при инициализации базы данных + * должндолжен быть полностью заполнен справочник жанров. + * Файл первоначальных данных ./src/test/resources/data.sql + */ +@SpringBootTest +@AutoConfigureMockMvc +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +class GenreControllerTest { + + private static final List testGenres = new ArrayList<>(); + + @Autowired + MockMvc mvc; + + // Определяем тип сериализации списка + class GenreListTypeToken extends TypeToken> { + } + + Gson gson = new GsonBuilder() + .setPrettyPrinting() + .create(); + + /** + * Инициализация эталонного списка жанров. + */ + @BeforeAll + static void setUp() { + testGenres.add(new Genre(1, "Комедия")); + testGenres.add(new Genre(2, "Драма")); + testGenres.add(new Genre(3, "Мультфильм")); + testGenres.add(new Genre(4, "Триллер")); + testGenres.add(new Genre(5, "Документальный")); + testGenres.add(new Genre(6, "Боевик")); + } + + /** + * Тестируем список всех жанров + */ + @Test + void findAllGenres() throws Exception { + MvcResult result = mvc.perform(get("/genres")) + .andExpect(status().isOk()) + .andReturn(); + + List genres = gson.fromJson(result.getResponse().getContentAsString(), + new GenreListTypeToken().getType()); + for (Genre genre : genres) { + assertTrue(testGenres.contains(genre), + "Получен неизвестный жанр: " + genre.toString()); + } + } + + /** + * Тестируем поиск жанра по идентификатору + */ + @Test + void findGenreById() throws Exception { + for (Genre genre : testGenres) { + MvcResult result = mvc.perform(get("/genres/" + genre.getId())) + .andExpect(status().isOk()) + .andReturn(); + Genre genreDb = gson.fromJson(result.getResponse().getContentAsString(), Genre.class); + assertTrue(genre.equals(genreDb), + "Получен неизвестный жанр: " + genreDb.toString()); + } + } +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/controller/MpaControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/controller/MpaControllerTest.java new file mode 100644 index 0000000..602f723 --- /dev/null +++ b/src/test/java/ru/yandex/practicum/filmorate/controller/MpaControllerTest.java @@ -0,0 +1,98 @@ +package ru.yandex.practicum.filmorate.controller; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.BeforeAll; +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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.model.Mpa; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Тестирование контроллера запросов работы со справочником жанров рейтингов MPA + *

+ * Для успешного выполнения тестов, при инициализации базы данных + * должндолжен быть полностью заполнен справочник рейтингов. + * Файл первоначальных данных ./src/test/resources/data.sql + */ +@SpringBootTest +@AutoConfigureMockMvc +@AutoConfigureTestDatabase +@RequiredArgsConstructor(onConstructor_ = @Autowired) +class MpaControllerTest { + private static List testMpaList = new ArrayList<>(); + + private static final List testGenres = new ArrayList<>(); + + @Autowired + MockMvc mvc; + + // Определяем тип сериализации списка + class MpaListTypeToken extends TypeToken> { + } + + Gson gson = new GsonBuilder() + .setPrettyPrinting() + .create(); + + /** + * Инициализация эталонного списка рейтингов. + */ + @BeforeAll + static void setUp() { + testMpaList.add(new Mpa(1, "G", "у фильма нет возрастных ограничений")); + testMpaList.add(new Mpa(2, "PG", "детям рекомендуется смотреть фильм с родителями")); + testMpaList.add(new Mpa(3, "PG-13", "детям до 13 лет просмотр не желателен")); + testMpaList.add(new Mpa(4, "R", "лицам до 17 лет просматривать фильм можно только в присутствии взрослого")); + testMpaList.add(new Mpa(5, "NC-17", "лицам до 18 лет просмотр запрещён")); + } + + /** + * Тестируем список всех рейтингов + */ + @Test + void findAllMpa() throws Exception { + MvcResult result = mvc.perform(get("/mpa")) + .andExpect(status().isOk()) + .andReturn(); + + List mpas = gson.fromJson(result.getResponse().getContentAsString(), + new MpaListTypeToken().getType()); + for (Mpa mpa : mpas) { + assertTrue(testMpaList.contains(mpa), + "Получен неизвестный рейтинг: " + mpa.toString()); + } + } + + /** + * Тестируем поиск рейтинга по идентификаторам + * + * @throws Exception + */ + @Test + void findMpaById() throws Exception { + for (Mpa mpa : testMpaList) { + MvcResult result = mvc.perform(get("/mpa/" + mpa.getId())) + .andExpect(status().isOk()) + .andReturn(); + Mpa mpaDb = gson.fromJson(result.getResponse().getContentAsString(), Mpa.class); + assertTrue(mpa.equals(mpaDb), + "Получен неизвестный рейтинг: " + mpaDb.toString()); + } + } + +} \ No newline at end of file diff --git a/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java b/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java index 8ce766c..2ff733b 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/controller/UserControllerTest.java @@ -2,6 +2,7 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; import lombok.RequiredArgsConstructor; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -43,6 +44,10 @@ class UserControllerTest { .registerTypeAdapter(LocalDate.class, new LocalDateAdapter()) .create(); + // Определяем тип сериализации списка + class UserListTypeToken extends TypeToken> { + } + /** * Тестируем чтение списка пользователей */ @@ -51,7 +56,7 @@ void findAllUser() throws Exception { MvcResult result = mvc.perform(get("/users")) .andExpect(status().isOk()) // ожидается код статус 200 .andReturn(); - List users = gson.fromJson(result.getResponse().getContentAsString(), List.class); + List users = gson.fromJson(result.getResponse().getContentAsString(), new UserListTypeToken().getType()); assertTrue(!users.isEmpty(), "Список пользователей пуст."); } @@ -241,7 +246,7 @@ void findUsersFriends() throws Exception { .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andReturn(); - List users = gson.fromJson(result.getResponse().getContentAsString(), List.class); + List users = gson.fromJson(result.getResponse().getContentAsString(), new UserListTypeToken().getType()); assertTrue(users.size() == 2, "Количество найденых \"друзей\" не соответствует ожидаемому."); } @@ -287,7 +292,7 @@ void findCommonFriends() throws Exception { .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andReturn(); - List users = gson.fromJson(result.getResponse().getContentAsString(), List.class); + List users = gson.fromJson(result.getResponse().getContentAsString(), new UserListTypeToken().getType()); assertTrue(users.size() == 2, "Количество общих \"друзей\" не соответствует ожидаемому."); } diff --git a/src/test/java/ru/yandex/practicum/filmorate/model/FilmTest.java b/src/test/java/ru/yandex/practicum/filmorate/model/FilmTest.java index 93f3ada..621ab84 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/model/FilmTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/model/FilmTest.java @@ -17,7 +17,6 @@ /** * Тестирование ограничений на значения полей класса Film - * Автономный тест (Junit). */ class FilmTest { private Validator validator; diff --git a/src/test/java/ru/yandex/practicum/filmorate/model/UserTest.java b/src/test/java/ru/yandex/practicum/filmorate/model/UserTest.java index b762318..43a2218 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/model/UserTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/model/UserTest.java @@ -17,7 +17,6 @@ /** * Тестирование ограничений на значения полей класса User. - * Автономный тест. */ class UserTest { private Validator validator; diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/genre/GenreDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/genre/GenreDbStorageTest.java index 3d4d00b..27c146b 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/storage/genre/GenreDbStorageTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/genre/GenreDbStorageTest.java @@ -19,7 +19,7 @@ /** * Тестирование справочника жанров фильа - * + *

* Для успешного выполнения тестов, при инициализации базы данных * должндолжен быть полностью заполнен справочник жанров. * Файл первоначальных данных ./src/test/resources/data.sql @@ -31,7 +31,7 @@ class GenreDbStorageTest { private final GenreDbStorage genreDbStorage; - private static List testGenres = new ArrayList<>(); + private static final List testGenres = new ArrayList<>(); /** * Инициализация эталонного списка жанров. diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/mpa/MpaDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/mpa/MpaDbStorageTest.java index 2d0f02d..8fb0bc7 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/storage/mpa/MpaDbStorageTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/mpa/MpaDbStorageTest.java @@ -19,7 +19,7 @@ /** * Тестирование справочника рейтингов фильа - * + *

* Для успешного выполнения тестов, при инициализации базы данных * должндолжен быть полностью заполнен справочник рейтингов MPA. * Файл первоначальных данных ./src/test/resources/data.sql diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorageTest.java index f20c530..0abca78 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorageTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorageTest.java @@ -18,7 +18,7 @@ /** * Тестирование Хранилища пользователей в базе данных - * + *

* Для успешного выполнения тестов, при инициализации базы данных * должна быть подготовлена информация о четырех тестовых пользователях. * Файл первоначальных данных ./src/test/resources/data.sql From 96b6cb3bc296a778dc99531956b23ff70d9d4c1e Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Mon, 24 Feb 2025 23:52:32 +0700 Subject: [PATCH 16/19] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D1=8F=D0=B5=D0=BC=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B8?= =?UTF-8?q?=20=D1=81=D1=82=D0=B8=D0=BB=D1=8F.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yandex/practicum/filmorate/storage/film/FilmDbStorage.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java index 0b868c6..3213ae9 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java @@ -29,7 +29,7 @@ public class FilmDbStorage implements FilmStorage { private static final String SQL_INSERT_FILM = "INSERT INTO films (name, description, releasedate, len_min, mpa_id)" + "VALUES ( :name, :description, :releasedate, :len_min, :mpa_id)"; - private final String SQL_UPDATE_GENRES = "MERGE INTO films_genres (film_id, genre_id) " + + private static final String SQL_UPDATE_GENRES = "MERGE INTO films_genres (film_id, genre_id) " + "VALUES (:film_id, :genre_id)"; private static final String SQL_FIND_FILM_BY_ID = "SELECT f.*, mpa.name as mpa_name, fg.genre_id, g.name AS genre_name\n" + " FROM (films AS f INNER JOIN mpa ON f.MPA_ID = mpa.ID)\n" + From 364aa0188a8a7e9e17e0499537e86a0f7d419a2c Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Wed, 26 Feb 2025 21:35:42 +0700 Subject: [PATCH 17/19] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D1=8F=D0=B5=D0=BC=20=D0=B7=D0=B0=D0=BC=D0=B5=D1=87=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D0=B0.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../filmorate/service/FilmService.java | 137 +----------- .../filmorate/service/FilmServiceImpl.java | 151 +++++++++++++ .../filmorate/service/GenreService.java | 20 +- .../filmorate/service/GenreServiceImpl.java | 28 +++ .../filmorate/service/MpaService.java | 18 +- .../filmorate/service/MpaServiceImpl.java | 28 +++ .../filmorate/service/UserService.java | 164 +------------- .../filmorate/service/UserServiceImpl.java | 179 ++++++++++++++++ .../filmorate/storage/film/FilmDbStorage.java | 202 ++++++++---------- .../storage/film/InMemoryFilmStorage.java | 140 ------------ .../storage/user/InMemoryUserStorage.java | 78 ------- .../filmorate/storage/user/UserDbStorage.java | 2 +- src/main/resources/schema.sql | 2 +- .../storage/user/UserDbStorageTest.java | 13 +- src/test/resources/schema.sql | 2 +- 15 files changed, 512 insertions(+), 652 deletions(-) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/service/FilmServiceImpl.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/service/GenreServiceImpl.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/service/MpaServiceImpl.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/service/UserServiceImpl.java delete mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java delete mode 100644 src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java 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 46bfc81..062e029 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java @@ -1,144 +1,27 @@ package ru.yandex.practicum.filmorate.service; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.stereotype.Service; -import ru.yandex.practicum.filmorate.exception.InternalServerException; -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.storage.film.FilmStorage; -import ru.yandex.practicum.filmorate.storage.user.UserStorage; import java.util.Collection; -import java.util.HashMap; import java.util.Map; -import java.util.Optional; -/** - * Класс реализации запросов к информации о фильмах - */ -@Service -public class FilmService { +public interface FilmService { - private final FilmStorage films; - private final UserStorage users; + Collection findAllFilms(); - public FilmService(@Qualifier("filmDbStorage") FilmStorage filmStorage, - @Qualifier("userDbStorage") UserStorage users) { - this.films = filmStorage; - this.users = users; - } + Film getFilmById(Integer id); - /** - * Метод поиска всех фильмов - * - * @return - список фильмов - */ - public Collection findAllFilms() { - return films.findAllFilms(); - } + Film addNewFilm(Film film); - /** - * Метод поиска фильма по идентификатору - * - * @param id - идентификатор - * @return - найденный фильм - */ - public Film getFilmById(Integer id) { - return films.getFilmById(id).orElseThrow(() -> - new NotFoundException("Не найден фильм id=" + id)); - } + Film updateFilm(Film updFilm); - /** - * Метод добавления нового фильма. - * - * @param film - объект для добавления - * @return - подтверждение добавленного объекта - */ - public Film addNewFilm(Film film) { - Optional existingFilm = films.findAllFilms().stream() - .filter(film1 -> film1.equals(film)) - .findFirst(); - if (existingFilm.isPresent()) { - throw new ValidationException("Фильм уже существует: " + existingFilm.get()); - } - return films.addNewFilm(film); - } + String onDelete(); - /** - * Метод обновления информации о фильме. - * - * @param updFilm - объект с обновленной информацией о фильме - * @return - подтверждение обновленного объекта - */ - public Film updateFilm(Film updFilm) { - Integer id = updFilm.getId(); - Film film = films.getFilmById(id).orElseThrow(() -> - new NotFoundException("Не найден фильм id=" + id)); + Integer addNewLike(Integer filmId, Integer userId); - if (updFilm.getName() != null) { - film.setName(updFilm.getName()); - } - if (updFilm.getDescription() != null) { - film.setDescription(updFilm.getDescription()); - } - if (updFilm.getReleaseDate() != null) { - film.setReleaseDate(updFilm.getReleaseDate()); - } - if (updFilm.getDuration() > 0) { - film.setDuration(updFilm.getDuration()); - } - if (updFilm.getMpa() != null) { - film.setMpa(updFilm.getMpa()); - } - if (updFilm.getGenres().size() > 0) { - film.setGenres(updFilm.getGenres()); - } - films.updateFilm(film); + Integer removeLike(Integer filmId, Integer userId); - return films.getFilmById(id).orElseThrow(() -> - new InternalServerException("Ошибка обновления фильма id=" + id)); - } + Collection findPopularFilms(int count); - /** - * Удаление всех фильмов - * - * @return - сообщение о выполнении - */ - public String onDelete() { - films.removeAllFilms(); - return "Все фильмы удалены."; - } - - public Integer addNewLike(Integer filmId, Integer userId) { - Film film = films.getFilmById(filmId).orElseThrow(() -> - new NotFoundException("Не найден фильм id=" + filmId)); - users.getUserById(userId).orElseThrow(() -> - new NotFoundException("Не найден пользователь id=" + userId)); - - return films.addNewLike(filmId, userId); - } - - public Integer removeLike(Integer filmId, Integer userId) { - Film film = films.getFilmById(filmId).orElseThrow(() -> - new NotFoundException("Не найден фильм id=" + filmId)); - users.getUserById(userId).orElseThrow(() -> - new NotFoundException("Не найден пользователь id=" + userId)); - - return films.removeLike(filmId, userId); - } - - public Collection findPopularFilms(int count) { - return films.findPopularFilms(count); - } - - public Map getFilmRank(Integer filmId) { - Film film = films.getFilmById(filmId).orElseThrow(() -> - new NotFoundException("Не найден фильм id=" + filmId)); - - Map response = new HashMap<>(); - response.put("Фильм ", film.getName()); - response.put("лайков", films.getFilmRank(filmId).toString()); - return response; - } + Map getFilmRank(Integer filmId); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/FilmServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/FilmServiceImpl.java new file mode 100644 index 0000000..9563115 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmServiceImpl.java @@ -0,0 +1,151 @@ +package ru.yandex.practicum.filmorate.service; + +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.exception.InternalServerException; +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.storage.film.FilmStorage; +import ru.yandex.practicum.filmorate.storage.user.UserStorage; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * Класс реализации запросов к информации о фильмах + */ +@Service +public class FilmServiceImpl implements FilmService { + + private final FilmStorage films; + private final UserStorage users; + + public FilmServiceImpl(FilmStorage filmStorage, UserStorage users) { + this.films = filmStorage; + this.users = users; + } + + /** + * Метод поиска всех фильмов + * + * @return - список фильмов + */ + @Override + public Collection findAllFilms() { + return films.findAllFilms(); + } + + /** + * Метод поиска фильма по идентификатору + * + * @param id - идентификатор + * @return - найденный фильм + */ + @Override + public Film getFilmById(Integer id) { + return films.getFilmById(id) + .orElseThrow(() -> new NotFoundException("Не найден фильм id=" + id)); + } + + /** + * Метод добавления нового фильма. + * + * @param film - объект для добавления + * @return - подтверждение добавленного объекта + */ + @Override + public Film addNewFilm(Film film) { + Optional existingFilm = films.findAllFilms().stream() + .filter(film1 -> film1.equals(film)) + .findFirst(); + if (existingFilm.isPresent()) { + throw new ValidationException("Фильм уже существует: " + existingFilm.get()); + } + return films.addNewFilm(film); + } + + /** + * Метод обновления информации о фильме. + * + * @param updFilm - объект с обновленной информацией о фильме + * @return - подтверждение обновленного объекта + */ + @Override + public Film updateFilm(Film updFilm) { + Integer id = updFilm.getId(); + Film film = films.getFilmById(id) + .orElseThrow(() -> new NotFoundException("Не найден фильм id=" + id)); + + if (updFilm.getName() != null) { + film.setName(updFilm.getName()); + } + if (updFilm.getDescription() != null) { + film.setDescription(updFilm.getDescription()); + } + if (updFilm.getReleaseDate() != null) { + film.setReleaseDate(updFilm.getReleaseDate()); + } + if (updFilm.getDuration() > 0) { + film.setDuration(updFilm.getDuration()); + } + if (updFilm.getMpa() != null) { + film.setMpa(updFilm.getMpa()); + } + if (updFilm.getGenres().size() > 0) { + film.setGenres(updFilm.getGenres()); + } + films.updateFilm(film); + + return films.getFilmById(id).orElseThrow(() -> + new InternalServerException("Ошибка обновления фильма id=" + id)); + } + + /** + * Удаление всех фильмов + * + * @return - сообщение о выполнении + */ + @Override + public String onDelete() { + films.removeAllFilms(); + return "Все фильмы удалены."; + } + + @Override + public Integer addNewLike(Integer filmId, Integer userId) { + films.getFilmById(filmId).orElseThrow(() -> + new NotFoundException("Не найден фильм id=" + filmId)); + users.getUserById(userId).orElseThrow(() -> + new NotFoundException("Не найден пользователь id=" + userId)); + + return films.addNewLike(filmId, userId); + } + + @Override + public Integer removeLike(Integer filmId, Integer userId) { + Film film = films.getFilmById(filmId).orElseThrow(() -> + new NotFoundException("Не найден фильм id=" + filmId)); + users.getUserById(userId).orElseThrow(() -> + new NotFoundException("Не найден пользователь id=" + userId)); + + return films.removeLike(filmId, userId); + } + + @Override + public Collection findPopularFilms(int count) { + return films.findPopularFilms(count); + } + + @Override + public Map getFilmRank(Integer filmId) { + Film film = films.getFilmById(filmId).orElseThrow(() -> + new NotFoundException("Не найден фильм id=" + filmId)); + + Map response = new HashMap<>(); + response.put("Фильм ", film.getName()); + response.put("лайков", films.getFilmRank(filmId).toString()); + return response; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/GenreService.java b/src/main/java/ru/yandex/practicum/filmorate/service/GenreService.java index d51c661..b76126a 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/GenreService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/GenreService.java @@ -1,26 +1,12 @@ package ru.yandex.practicum.filmorate.service; -import org.springframework.stereotype.Service; -import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.model.Genre; -import ru.yandex.practicum.filmorate.storage.genre.GenreStorage; import java.util.Collection; -@Service -public class GenreService { - private final GenreStorage genereStorage; +public interface GenreService { - public GenreService(GenreStorage genereStorage) { - this.genereStorage = genereStorage; - } + Collection getAllGenres(); - public Collection getAllGenres() { - return genereStorage.findAllGenres(); - } - - public Genre getGenreById(int id) { - return genereStorage.findGenre(id).orElseThrow(() -> - new NotFoundException("Не найден жанр id=" + id)); - } + Genre getGenreById(int id); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/GenreServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/GenreServiceImpl.java new file mode 100644 index 0000000..b4d27a4 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/GenreServiceImpl.java @@ -0,0 +1,28 @@ +package ru.yandex.practicum.filmorate.service; + +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.storage.genre.GenreStorage; + +import java.util.Collection; + +@Service +public class GenreServiceImpl implements GenreService { + private final GenreStorage genereStorage; + + public GenreServiceImpl(GenreStorage genereStorage) { + this.genereStorage = genereStorage; + } + + @Override + public Collection getAllGenres() { + return genereStorage.findAllGenres(); + } + + @Override + public Genre getGenreById(int id) { + return genereStorage.findGenre(id).orElseThrow(() -> + new NotFoundException("Не найден жанр id=" + id)); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/MpaService.java b/src/main/java/ru/yandex/practicum/filmorate/service/MpaService.java index c065418..4c27704 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/MpaService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/MpaService.java @@ -1,26 +1,14 @@ package ru.yandex.practicum.filmorate.service; import org.springframework.stereotype.Service; -import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.model.Mpa; -import ru.yandex.practicum.filmorate.storage.mpa.MpaStorage; import java.util.Collection; @Service -public class MpaService { - private final MpaStorage mpaStorage; +public interface MpaService { - public MpaService(MpaStorage mpaStorage) { - this.mpaStorage = mpaStorage; - } + Collection filndAllMpa(); - public Collection filndAllMpa() { - return mpaStorage.findAllMpa(); - } - - public Mpa findMpa(Integer id) { - return mpaStorage.findMpa(id).orElseThrow(() -> - new NotFoundException("Не найден рейтинг id=" + id)); - } + Mpa findMpa(Integer id); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/MpaServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/MpaServiceImpl.java new file mode 100644 index 0000000..44064d6 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/MpaServiceImpl.java @@ -0,0 +1,28 @@ +package ru.yandex.practicum.filmorate.service; + +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.model.Mpa; +import ru.yandex.practicum.filmorate.storage.mpa.MpaStorage; + +import java.util.Collection; + +@Service +public class MpaServiceImpl implements MpaService { + private final MpaStorage mpaStorage; + + public MpaServiceImpl(MpaStorage mpaStorage) { + this.mpaStorage = mpaStorage; + } + + @Override + public Collection filndAllMpa() { + return mpaStorage.findAllMpa(); + } + + @Override + public Mpa findMpa(Integer id) { + return mpaStorage.findMpa(id).orElseThrow(() -> + new NotFoundException("Не найден рейтинг id=" + id)); + } +} 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 fc664b8..ab230c7 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserService.java @@ -1,170 +1,26 @@ package ru.yandex.practicum.filmorate.service; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Qualifier; -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.User; -import ru.yandex.practicum.filmorate.storage.user.UserStorage; import java.util.Collection; -/** - * Класс реализации запросов к информации о пользователях - */ -@Slf4j -@Service -public class UserService { +public interface UserService { + Collection findAllUsers(); - private final UserStorage users; + User addNewUser(User user); - public UserService(@Qualifier("userDbStorage") UserStorage users) { - this.users = users; - } + User getUserById(Integer id); - /** - * Метод поиска всех пользователей - * - * @return - список пользователей - */ - public Collection findAllUsers() { - return users.findAllUsers(); - } + User updateUser(User updUser); - /** - * Метод добавления нового пользователя. - * - * @param user - объект для добавления - * @return - подтверждение добавленного объекта - */ - public User addNewUser(User user) { - // "имя для отображения может быть пустым - // — в таком случае будет использован логин" (ТЗ-№10) - if (user.getName() == null | user.getName().isBlank()) { - user.setName(user.getLogin()); - } - if (users.findAllUsers().contains(user)) { - throw new ValidationException("Пользователь уже существует " - + user.getEmail()); - } - return users.addNewUser(user); - } + String removeAllUsers(); - /** - * Метод чтения информации о пользователе по заданному идентификатору - * - * @param id - идентификатор пользователя - * @return - найденный объект - */ - public User getUserById(Integer id) { - User user = users.getUserById(id).orElseThrow(() -> - new NotFoundException("Не найден пользователь id=" + id)); - return user; - } + void addFriends(Integer id1, Integer id2); - /** - * Метод обновления информации о пользователе. - * При вызове метода промзводится проверка аннотаций только для маркера OnUpdate.class. - * Кроме id любой другой параметр может отсутствовать - * - * @param updUser - объект с обновленной информацией о пользователе - * @return - обновленный объект - */ - public User updateUser(User updUser) { - Integer id = updUser.getId(); - User user = users.getUserById(id).orElseThrow(() -> - new NotFoundException("Не найден пользователь id=" + id)); + void breakUpFriends(Integer id1, Integer id2); - // Обновляем информаию во временном объекте - if (updUser.getEmail() != null) { - user.setEmail(updUser.getEmail()); - } - if (updUser.getLogin() != null) { - user.setLogin(updUser.getLogin()); - } - if (updUser.getName() != null) { - user.setName(updUser.getName()); - } - if (updUser.getBirthday() != null) { - user.setBirthday(updUser.getBirthday()); - } + Collection getUserFriends(Integer userId); - users.updateUser(user); - return user; - } - - /** - * Удаление всех пользователей - * - * @return - сообщение о выполнении - */ - public String removeAllUsers() { - log.debug("Sevice: Удаляем всех пользователей."); - users.removeAllUsers(); - return "Все пользователи удалены."; - } - - /** - * Медод добавления пользователей в друзья - * добавление в друзья происходит взаимное без подтверждений - * - * @param id1 - идентификатор пользователя - * @param id2 - идентификатор друга - */ - public void addFriends(Integer id1, Integer id2) { - users.getUserById(id1).orElseThrow(() -> - new NotFoundException("Не найден пользователь id=" + id1)); - users.getUserById(id2).orElseThrow(() -> - new NotFoundException("Не найден пользователь id=" + id2)); - - // Добавление в друзья - users.addFriend(id1, id2); - } - - /** - * Метод удаления пользователя из "друзей" - * - * @param id1 - идентификатор пользователя - * @param id2 - идентификатор друга - * @return - сообщение о подтверждении - */ - public void breakUpFriends(Integer id1, Integer id2) { - users.getUserById(id1).orElseThrow(() -> - new NotFoundException("Не найден пользователь id=" + id1)); - users.getUserById(id2).orElseThrow(() -> - new NotFoundException("Не найден пользователь id=" + id2)); - - users.breakUpFriends(id1, id2); - } - - /** - * Поиск всех друзей пользователя - * - * @param userId - идентификатор пользователя - * @return - список друзей - */ - public Collection getUserFriends(Integer userId) { - users.getUserById(userId).orElseThrow(() -> - new NotFoundException("Не найден пользователь id=" + userId)); - - return users.getUserFriends(userId); - } - - /** - * Поиск общих друзей пользователей - * - * @param id1 - идентификатор пользователя - * @param id2 - идентификатор другого пользователя - * @return - список общих друзей - */ - public Collection getCommonFriends(Integer id1, Integer id2) { - users.getUserById(id1).orElseThrow(() -> - new NotFoundException("Не найден пользователь id=" + id1)); - users.getUserById(id2).orElseThrow(() -> - new NotFoundException("Не найден пользователь id=" + id2)); - - return users.getCommonFriends(id1, id2); - } + Collection getCommonFriends(Integer id1, Integer id2); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/UserServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/UserServiceImpl.java new file mode 100644 index 0000000..2d5b2bb --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserServiceImpl.java @@ -0,0 +1,179 @@ +package ru.yandex.practicum.filmorate.service; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; +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.User; +import ru.yandex.practicum.filmorate.storage.user.UserStorage; + +import java.util.Collection; + +/** + * Класс реализации запросов к информации о пользователях + */ +@Slf4j +@Service +public class UserServiceImpl implements UserService { + + private final UserStorage users; + + public UserServiceImpl(UserStorage users) { + this.users = users; + } + + /** + * Метод поиска всех пользователей + * + * @return - список пользователей + */ + @Override + public Collection findAllUsers() { + return users.findAllUsers(); + } + + /** + * Метод добавления нового пользователя. + * + * @param user - объект для добавления + * @return - подтверждение добавленного объекта + */ + @Override + public User addNewUser(User user) { + // "имя для отображения может быть пустым + // — в таком случае будет использован логин" (ТЗ-№10) + if (user.getName() == null | user.getName().isBlank()) { + user.setName(user.getLogin()); + } + if (users.findAllUsers().contains(user)) { + throw new ValidationException("Пользователь уже существует " + + user.getEmail()); + } + return users.addNewUser(user); + } + + /** + * Метод чтения информации о пользователе по заданному идентификатору + * + * @param id - идентификатор пользователя + * @return - найденный объект + */ + @Override + public User getUserById(Integer id) { + User user = users.getUserById(id).orElseThrow(() -> + new NotFoundException("Не найден пользователь id=" + id)); + return user; + } + + /** + * Метод обновления информации о пользователе. + * При вызове метода промзводится проверка аннотаций только для маркера OnUpdate.class. + * Кроме id любой другой параметр может отсутствовать + * + * @param updUser - объект с обновленной информацией о пользователе + * @return - обновленный объект + */ + @Override + public User updateUser(User updUser) { + Integer id = updUser.getId(); + User user = users.getUserById(id).orElseThrow(() -> + new NotFoundException("Не найден пользователь id=" + id)); + + // Обновляем информаию во временном объекте + if (updUser.getEmail() != null) { + user.setEmail(updUser.getEmail()); + } + if (updUser.getLogin() != null) { + user.setLogin(updUser.getLogin()); + } + if (updUser.getName() != null) { + user.setName(updUser.getName()); + } + if (updUser.getBirthday() != null) { + user.setBirthday(updUser.getBirthday()); + } + + users.updateUser(user); + return user; + } + + /** + * Удаление всех пользователей + * + * @return - сообщение о выполнении + */ + @Override + public String removeAllUsers() { + log.debug("Sevice: Удаляем всех пользователей."); + users.removeAllUsers(); + return "Все пользователи удалены."; + } + + /** + * Медод добавления пользователей в друзья + * добавление в друзья происходит взаимное без подтверждений + * + * @param id1 - идентификатор пользователя + * @param id2 - идентификатор друга + */ + @Override + public void addFriends(Integer id1, Integer id2) { + users.getUserById(id1).orElseThrow(() -> + new NotFoundException("Не найден пользователь id=" + id1)); + users.getUserById(id2).orElseThrow(() -> + new NotFoundException("Не найден пользователь id=" + id2)); + + // Добавление в друзья + users.addFriend(id1, id2); + } + + /** + * Метод удаления пользователя из "друзей" + * + * @param id1 - идентификатор пользователя + * @param id2 - идентификатор друга + * @return - сообщение о подтверждении + */ + @Override + public void breakUpFriends(Integer id1, Integer id2) { + users.getUserById(id1).orElseThrow(() -> + new NotFoundException("Не найден пользователь id=" + id1)); + users.getUserById(id2).orElseThrow(() -> + new NotFoundException("Не найден пользователь id=" + id2)); + + users.breakUpFriends(id1, id2); + } + + /** + * Поиск всех друзей пользователя + * + * @param userId - идентификатор пользователя + * @return - список друзей + */ + @Override + public Collection getUserFriends(Integer userId) { + users.getUserById(userId).orElseThrow(() -> + new NotFoundException("Не найден пользователь id=" + userId)); + + return users.getUserFriends(userId); + } + + /** + * Поиск общих друзей пользователей + * + * @param id1 - идентификатор пользователя + * @param id2 - идентификатор другого пользователя + * @return - список общих друзей + */ + @Override + public Collection getCommonFriends(Integer id1, Integer id2) { + users.getUserById(id1).orElseThrow(() -> + new NotFoundException("Не найден пользователь id=" + id1)); + users.getUserById(id2).orElseThrow(() -> + new NotFoundException("Не найден пользователь id=" + id2)); + + return users.getCommonFriends(id1, id2); + } + +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java index 3213ae9..ff52362 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java @@ -23,22 +23,31 @@ import java.sql.Types; import java.util.*; -@Repository("filmDbStorage") +@Repository public class FilmDbStorage implements FilmStorage { - private static final String SQL_INSERT_FILM = - "INSERT INTO films (name, description, releasedate, len_min, mpa_id)" + - "VALUES ( :name, :description, :releasedate, :len_min, :mpa_id)"; + private static final String SQL_INSERT_FILM = "INSERT INTO films (name, description, releasedate, len_min, mpa_id)" + + " VALUES ( :name, :description, :releasedate, :len_min, :mpa_id)"; private static final String SQL_UPDATE_GENRES = "MERGE INTO films_genres (film_id, genre_id) " + - "VALUES (:film_id, :genre_id)"; - private static final String SQL_FIND_FILM_BY_ID = "SELECT f.*, mpa.name as mpa_name, fg.genre_id, g.name AS genre_name\n" + - " FROM (films AS f INNER JOIN mpa ON f.MPA_ID = mpa.ID)\n" + - " LEFT JOIN (films_genres AS fg INNER JOIN genres AS g ON fg.GENRE_ID = g.ID) ON fg.film_id = f.id\n" + - " WHERE f.id = :id"; + " VALUES (:film_id, :genre_id)"; + private static final String SQL_UPDATE_FILM = "UPDATE films SET name = :name, description = :description, " + "releasedate = :releasedate, len_min = :len_min, mpa_id = :mpa_id WHERE id = :id"; private static final String SQL_ADD_LIKE = "MERGE INTO likes (user_id, film_id) VALUES (:userId, :filmId)"; private static final String SQL_REMOVE_LIKE = "DELETE FROM likes WHERE user_id = :userId AND film_id = :filmId"; + private static final String SQL_FIND_ALL_FILMS = "SELECT f.*, mpa.name as mpa_name FROM films AS f " + + " INNER JOIN mpa ON f.mpa_id = mpa.id"; + private static final String SQL_FIND_FILM_BY_ID = "SELECT f.*, mpa.name as mpa_name, fg.genre_id, g.name AS genre_name\n" + + " FROM (films AS f INNER JOIN mpa ON f.MPA_ID = mpa.ID)\n" + + " LEFT JOIN (films_genres AS fg INNER JOIN genres AS g ON fg.GENRE_ID = g.ID) ON fg.film_id = f.id\n" + + " WHERE f.id = :id"; + private static final String SQL_FIND_POPULAR_FILMS = "SELECT f.*, mpa.name AS mpa_name, popular.count_film\n" + + "FROM (films AS f INNER JOIN mpa ON f.MPA_ID = mpa.ID\n)" + + " LEFT OUTER JOIN\n" + + " (SELECT film_id, count(film_id) as count_film\n" + + " FROM LIKES GROUP BY film_id) AS popular\n" + + " ON f.id = popular.film_id\n" + + "ORDER BY popular.count_film DESC\n"; @Autowired private NamedParameterJdbcTemplate jdbc; @@ -138,125 +147,27 @@ public Film extractData(ResultSet rs) throws SQLException, DataAccessException { */ @Override public Collection findAllFilms() { - try { - // Загружаем из базы данных все фильмы - Map filmsMap = new HashMap<>(); - filmsMap = jdbc.query("SELECT f.*, mpa.name as mpa_name FROM films AS f INNER JOIN mpa ON f.mpa_id = mpa.id", - new ResultSetExtractor>() { - @Override - public Map extractData(ResultSet rs) - throws SQLException, DataAccessException { - Map fMap = new HashMap<>(); - while (rs.next()) { - Film film = new FilmRowMapper().mapRow(rs, 1); - Integer mpaId = rs.getInt("mpa_id"); - if (mpaId != 0) { - Mpa mpa = new Mpa(); - mpa.setId(mpaId); - mpa.setName(rs.getString("mpa_name")); - film.setMpa(mpa); - } - fMap.put(film.getId(), film); - } - return fMap; - } - }); - - // загружаем из базы данных все ссылки на жанры - List filmsGenres; - filmsGenres = jdbc.query( - "SELECT fg.*, g.name AS genre_name FROM films_genres AS fg INNER JOIN genres AS g ON fg.GENRE_ID = g.ID", - new FilmGenreRowMapper()); - - // пополням фильмы сведениями о жанрах - for (FilmGenre filmGenre : filmsGenres) { - Genre genre = new Genre(); - int filmId = filmGenre.getFilmId(); - genre.setId(filmGenre.getGenreId()); - genre.setName(filmGenre.getGenreName()); - filmsMap.get(filmId).addGenre(genre); - } - - return filmsMap.values(); - - } catch (EmptyResultDataAccessException ignored) { - return List.of(); - } - + return findFilmsByQuery(SQL_FIND_ALL_FILMS); } /** * Поиск популярных фильмов * - * @param count - * @return + * @param count - количество фильмов в итоговом списке + * @return - список самых популярных фильмов */ @Override public Collection findPopularFilms(int count) { - String sql = "SELECT f.*, mpa.name AS mpa_name, popular.count_film\n" + - "FROM (films AS f INNER JOIN mpa ON f.MPA_ID = mpa.ID\n)" + - " LEFT OUTER JOIN\n" + - " (SELECT film_id, count(film_id) as count_film\n" + - " FROM LIKES GROUP BY film_id) AS popular\n" + - " ON f.id = popular.film_id\n" + - "ORDER BY popular.count_film DESC\n" + - "LIMIT :count"; - - Map popularFilms; - try { - popularFilms = jdbc.query(sql, new MapSqlParameterSource() - .addValue("count", count), - new ResultSetExtractor>() { - @Override - public Map extractData(ResultSet rs) - throws SQLException, DataAccessException { - Map mapFilm = new LinkedHashMap<>(); - while (rs.next()) { - Film film = new FilmRowMapper().mapRow(rs, 1); - Integer mpaId = rs.getInt("mpa_id"); - if (mpaId != 0) { - Mpa mpa = new Mpa(); - mpa.setId(mpaId); - mpa.setName(rs.getString("mpa_name")); - film.setMpa(mpa); - } - mapFilm.put(film.getId(), film); - } - return mapFilm; - } - }); - - // загружаем из базы данных соответствующие ссылки на жанры - List ids = new ArrayList<>(popularFilms.keySet()); - List filmsGenres; - filmsGenres = jdbc.query( - "SELECT fg.*, " + - "g.name AS genre_name " + - "FROM films_genres AS fg INNER JOIN genres AS g ON fg.genre_id = g.id " + - "WHERE fg.film_id IN (:values)", - new MapSqlParameterSource() - .addValue("values", ids), - new FilmGenreRowMapper()); - - // пополням фильмы сведениями о жанрах - for (FilmGenre filmGenre : filmsGenres) { - Genre genre = new Genre(); - int filmId = filmGenre.getFilmId(); - genre.setId(filmGenre.getGenreId()); - genre.setName(filmGenre.getGenreName()); - popularFilms.get(filmId).addGenre(genre); - } - return popularFilms.values(); - - } catch (EmptyResultDataAccessException ignored) { - return List.of(); + if (count > 0 ) { + return findFilmsByQuery(SQL_FIND_POPULAR_FILMS + " LIMIT " + count); } + return findFilmsByQuery(SQL_FIND_POPULAR_FILMS); } /** * Обновление сведений о фильме * - * @param updFilm - обновленный объект + * @param updFilm - объект c информацией для обновления. id должен быть определен */ @Override public void updateFilm(Film updFilm) { @@ -282,7 +193,7 @@ public void updateFilm(Film updFilm) { .addValue("filmId", filmId)); // добавляем жанры Фильма если определены новые - if (updFilm.getGenres().size() > 0) { + if (!updFilm.getGenres().isEmpty()) { SqlParameterSource[] batch = updFilm.getGenres().stream() .map(genre -> new MapSqlParameterSource() .addValue("film_id", updFilm.getId()) @@ -345,6 +256,9 @@ public Integer getFilmRank(Integer filmId) { } } + /** + * Удаление всех фильмов + */ @Override public void removeAllFilms() { jdbc.update("DELETE FROM likes", new MapSqlParameterSource() @@ -354,4 +268,62 @@ public void removeAllFilms() { jdbc.update("DELETE FROM films", new MapSqlParameterSource() .addValue("table", "films")); } + + /** + * Метод поиска фильмов заполнения их соответствующими жанрами + * + * @param sqlQueryFilms - строка SQL запроса для выборки всех полей объекта Film + * @return - коллекция фильмов. + */ + private Collection findFilmsByQuery(String sqlQueryFilms) { + try { + // Загружаем из базы данных информацию о фильмах + Map filmsMap; + filmsMap = jdbc.query(sqlQueryFilms, + new ResultSetExtractor>() { + @Override + public Map extractData(ResultSet rs) + throws SQLException, DataAccessException { + Map fMap = new LinkedHashMap<>(); + while (rs.next()) { + Film film = new FilmRowMapper().mapRow(rs, 1); + Integer mpaId = rs.getInt("mpa_id"); + if (mpaId != 0) { + Mpa mpa = new Mpa(); + mpa.setId(mpaId); + mpa.setName(rs.getString("mpa_name")); + film.setMpa(mpa); + } + fMap.put(film.getId(), film); + } + return fMap; + } + }); + // Если ничего не нашли, то возвращаем пустой список + if (filmsMap.isEmpty()) { + return List.of(); + } + + // Загружаем из базы данных все ссылки на жанры + List filmsGenres; + filmsGenres = jdbc.query( + "SELECT fg.*, g.name AS genre_name FROM films_genres AS fg INNER JOIN genres AS g ON fg.GENRE_ID = g.ID", + new FilmGenreRowMapper()); + + // пополням фильмы сведениями о жанрах + for (FilmGenre filmGenre : filmsGenres) { + int filmId = filmGenre.getFilmId(); + if (filmsMap.keySet().contains(filmId)) { + Genre genre = new Genre(); + genre.setId(filmGenre.getGenreId()); + genre.setName(filmGenre.getGenreName()); + filmsMap.get(filmId).addGenre(genre); + } + } + return filmsMap.values(); + } catch (EmptyResultDataAccessException ignored) { + return List.of(); + } + } + } diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java deleted file mode 100644 index a3b0dce..0000000 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/InMemoryFilmStorage.java +++ /dev/null @@ -1,140 +0,0 @@ -package ru.yandex.practicum.filmorate.storage.film; - -import org.springframework.stereotype.Repository; -import ru.yandex.practicum.filmorate.model.Film; - -import java.util.*; - -@Repository("inMemoryFilmStorage") -public class InMemoryFilmStorage implements FilmStorage { - - private final Map films = new HashMap<>(); - private final Map> likes = new HashMap<>(); - private final List filmsRating = new ArrayList<>(); - Integer filmId = 0; - - @Override - public Film addNewFilm(Film film) { - filmId++; - film.setId(filmId); - // film.setRank(0); - films.put(filmId, film); - likes.put(filmId, new HashSet<>()); - filmsRating.add(film); - return film; - } - - @Override - public Optional getFilmById(Integer id) { - return Optional.ofNullable(films.get(id)); - } - - @Override - public Collection findAllFilms() { - return films.values(); - } - - @Override - public void updateFilm(Film updFilm) { - films.put(updFilm.getId(), updFilm); - } - - /** - * Добавление "лайка" к фильму. - * - * @param filmId - идентифмкатор фильма - * @param userId - идентификатор пользователя - * @return - число никальных лайков - */ - @Override - public Integer addNewLike(Integer filmId, Integer userId) { - likes.get(filmId).add(userId); - Film film = films.get(filmId); - // film.setRank(likes.get(filmId).size()); - setFilmsRating(film); - return likes.get(filmId).size(); - } - - /** - * Удаление "лайка" у фильма - * - * @param filmId - идентификатор фильма - * @param userId - идентификатор пользователя - * @return - число независимых "лайков" у фильма - */ - @Override - public Integer removeLike(Integer filmId, Integer userId) { - likes.get(filmId).remove(userId); - Film film = films.get(filmId); - // film.setRank(likes.get(filmId).size()); - setFilmsRating(film); - return likes.get(filmId).size(); - } - - @Override - public Integer getFilmRank(Integer filmId) { - int filmRank = 0; - if (likes.containsKey(filmId)) { - filmRank = likes.get(filmId).size(); - } - return filmRank; - } - - /** - * Определение позиции фильма в рейтинге. - * Так как рейтинг представляет собой уже упорядоченный список, - * то сортировать весь список нет смысла. - * Нужно уточнить место в рейтинге заданного объекта. - * - * @param film - */ - private void setFilmsRating(Film film) { - int ratingSize = filmsRating.size(); - - // Если фильмов меньше двух, то ничего не делаем - if (ratingSize < 2) { - return; - } - - int index = filmsRating.indexOf(film); - - Integer filmRank = getFilmRank(film.getId()); - - // Проверяем изменение рейтинга на возрастание - while ((index > 0) && - (filmRank > likes.get(filmsRating.get(index - 1).getId()).size())) { - filmsRating.set(index, filmsRating.get(index - 1)); - filmsRating.set(--index, film); - } - - // Проверяем изменение рейтинга на убывание - while (index < (ratingSize - 1) && - (filmRank < likes.get(filmsRating.get(index + 1).getId()).size())) { - filmsRating.set(index, filmsRating.get(index + 1)); - filmsRating.set(++index, film); - } - } - - /** - * Поиск самых популярных фильмов - * - * @param count - количество фильмов для поиска - * @return - список фильмов - */ - @Override - public Collection findPopularFilms(int count) { - if (count > filmsRating.size()) { - count = filmsRating.size(); - } - return filmsRating.subList(0, count); - - } - - @Override - public void removeAllFilms() { - likes.clear(); - filmsRating.clear(); - films.clear(); - filmId = 0; - } -} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java deleted file mode 100644 index 564b438..0000000 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/InMemoryUserStorage.java +++ /dev/null @@ -1,78 +0,0 @@ -package ru.yandex.practicum.filmorate.storage.user; - -import org.springframework.stereotype.Repository; -import ru.yandex.practicum.filmorate.model.User; - -import java.util.*; - -@Repository("inMemoryUserStorage") -public class InMemoryUserStorage implements UserStorage { - - private final Map users = new HashMap<>(); - private final Map> friends = new HashMap<>(); - private Integer userId = 0; - - @Override - public User addNewUser(User user) { - userId++; - user.setId(userId); - users.put(userId, user); - friends.put(userId, new HashSet<>()); - return user; - } - - @Override - public Optional getUserById(Integer id) { - return Optional.ofNullable(users.get(id)); - } - - @Override - public Collection findAllUsers() { - return users.values(); - } - - @Override - public void updateUser(User updUser) { - users.put(updUser.getId(), updUser); - } - - @Override - public void removeAllUsers() { - friends.clear(); - users.clear(); - userId = 0; - } - - @Override - public void addFriend(Integer userId, Integer friendId) { - friends.get(userId).add(friendId); - friends.get(friendId).add(userId); - } - - @Override - public void breakUpFriends(Integer id1, Integer id2) { - friends.get(id1).remove(id2); - friends.get(id2).remove(id1); - } - - @Override - public Collection getUserFriends(Integer userId) { - List dtoFriends = new ArrayList<>(); - for (Integer friendId : friends.get(userId)) { - dtoFriends.add(users.get(friendId)); - } - return dtoFriends; - } - - @Override - public Collection getCommonFriends(Integer id1, Integer id2) { - List friendsId = new ArrayList<>(friends.get(id1)); - friendsId.retainAll(new ArrayList<>(friends.get(id2))); - List dtoFriends = new ArrayList<>(); - for (Integer id : friendsId) { - dtoFriends.add(users.get(id)); - } - return dtoFriends; - } - -} diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java index 452ab29..6414f24 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorage.java @@ -16,7 +16,7 @@ import java.util.List; import java.util.Optional; -@Repository("userDbStorage") +@Repository public class UserDbStorage implements UserStorage { private static final String SQL_INSERT_USER = "INSERT INTO users (email, login, name, birthday) VALUES (:email, :login, :name, :birthday)"; diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 874def3..a6d0924 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -1,7 +1,7 @@ -- Создаем таблицу пользователей CREATE TABLE IF NOT EXISTS users ( id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - email VARCHAR(255) NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, login VARCHAR(40) NOT NULL, name VARCHAR(40) NOT NULL, birthday DATE NOT NULL diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorageTest.java index 0abca78..be69cda 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorageTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorageTest.java @@ -6,6 +6,7 @@ import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; import org.springframework.context.annotation.Import; +import org.springframework.dao.DuplicateKeyException; import ru.yandex.practicum.filmorate.model.User; import java.time.LocalDate; @@ -13,8 +14,7 @@ import java.util.Optional; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; /** * Тестирование Хранилища пользователей в базе данных @@ -68,11 +68,18 @@ void getUserById() { @Test void addNewUser() { User user = new User(); - user.setEmail("test@user.test"); user.setName("TesstUserName"); user.setLogin("TestUserLogin"); user.setBirthday(LocalDate.of(2001, 7, 22)); + // Проверяем попытку добавить пользвателя с неуникальным Email + user.setEmail(getTestUser().getEmail()); + assertThrows(DuplicateKeyException.class, + () -> { userDbStorage.addNewUser(user); }, + "Попытка записи неуникального значения Email должна приводить к исключению."); + + // меняем Email на уникальный + user.setEmail("test@user.test"); User userDb = userDbStorage.addNewUser(user); assertNotNull(userDb.getId(), "addNewUser() - При добавлении пользователя в базу должен быть присвоен не нулевой идентификатор"); diff --git a/src/test/resources/schema.sql b/src/test/resources/schema.sql index 3724252..714f97c 100644 --- a/src/test/resources/schema.sql +++ b/src/test/resources/schema.sql @@ -1,7 +1,7 @@ -- Создаем таблицу пользователей CREATE TABLE IF NOT EXISTS users ( id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - email VARCHAR(255) NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, login VARCHAR(40) NOT NULL, name VARCHAR(40) NOT NULL, birthday DATE NOT NULL From 82955eb4074d5a152e47cacd383d2bdc0063b0fa Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Wed, 26 Feb 2025 21:41:16 +0700 Subject: [PATCH 18/19] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D1=8F=D0=B5=D0=BC=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B8?= =?UTF-8?q?=20=D1=81=D1=82=D0=B8=D0=BB=D1=8F.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yandex/practicum/filmorate/service/UserServiceImpl.java | 1 - .../practicum/filmorate/storage/film/FilmDbStorage.java | 2 +- .../practicum/filmorate/storage/user/UserDbStorageTest.java | 4 +++- 3 files changed, 4 insertions(+), 3 deletions(-) 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 2d5b2bb..7161159 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/UserServiceImpl.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/UserServiceImpl.java @@ -1,7 +1,6 @@ package ru.yandex.practicum.filmorate.service; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.exception.ValidationException; diff --git a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java index ff52362..d9c100e 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/storage/film/FilmDbStorage.java @@ -158,7 +158,7 @@ public Collection findAllFilms() { */ @Override public Collection findPopularFilms(int count) { - if (count > 0 ) { + if (count > 0) { return findFilmsByQuery(SQL_FIND_POPULAR_FILMS + " LIMIT " + count); } return findFilmsByQuery(SQL_FIND_POPULAR_FILMS); diff --git a/src/test/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorageTest.java b/src/test/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorageTest.java index be69cda..40632ae 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorageTest.java +++ b/src/test/java/ru/yandex/practicum/filmorate/storage/user/UserDbStorageTest.java @@ -75,7 +75,9 @@ void addNewUser() { // Проверяем попытку добавить пользвателя с неуникальным Email user.setEmail(getTestUser().getEmail()); assertThrows(DuplicateKeyException.class, - () -> { userDbStorage.addNewUser(user); }, + () -> { + userDbStorage.addNewUser(user); + }, "Попытка записи неуникального значения Email должна приводить к исключению."); // меняем Email на уникальный From 2270e0219cbe5d7ab2efae64dfba162c569f0551 Mon Sep 17 00:00:00 2001 From: Andrej Stelmaschuk Date: Wed, 26 Feb 2025 22:00:52 +0700 Subject: [PATCH 19/19] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=B2=D0=B0=D0=BB=D0=B8=D0=B4=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=BF=D0=B0=D1=80=D0=B0=D0=BC=D0=B5=D1=82=D1=80?= =?UTF-8?q?=D0=B0=20'count'=20=D0=B2=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4?= =?UTF-8?q?=D0=B5=20FilmController.findPopularFilms?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yandex/practicum/filmorate/controller/FilmController.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 eb598a1..e94ed7c 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -1,5 +1,6 @@ package ru.yandex.practicum.filmorate.controller; +import jakarta.validation.constraints.Min; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; @@ -52,7 +53,7 @@ public Film findFilm(@PathVariable Integer id) { } @GetMapping("/popular") - public Collection findPopularFilms(@RequestParam(defaultValue = "10") int count) { + public Collection findPopularFilms(@RequestParam(defaultValue = "10") @Min(1) int count) { log.info("Ищем популярные {} фильмов.", count); return service.findPopularFilms(count); }