Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added ChatGPT Image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 14 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
# java-filmorate
The Filmorate project — это приложение для работы с базой данных фильмов.

![Static Badge](https://img.shields.io/badge/Java-21-green)
![Static Badge](https://img.shields.io/badge/Spring_Boot-3.5.9-green)
![Static Badge](https://img.shields.io/badge/JDBCTemplate-8A2BE2)
![Static Badge](https://img.shields.io/badge/Lombok-red)
![Static Badge](https://img.shields.io/badge/H2_database-blue)
![Static Badge](https://img.shields.io/badge/DockerFile-blue)
![Static Badge](https://img.shields.io/badge/JUnit-5-orange)
![Static Badge](https://img.shields.io/badge/Maven-orange)

**The Filmorate project** — это бэкенд приложения для работы с базой данных фильмов.
А также площадка для общения и взаимодействия пользователей.

![preview](ChatGPT%20Image.png)

### Основные возможности
- Хранение данных о фильмах
- Хранение данных о пользователях
Expand Down Expand Up @@ -45,15 +57,7 @@ The Filmorate project — это приложение для работы с б
- и подобные
### Примеры пользовательских запросов (Request Examples)
![ExamplesOfRequests](ExamplesOfRequests.png)
### Архитектура и структура проекта (Architecture and Project Structure)
Проект использует Spring Boot версии 3.5.9 и включает в себя следующие основные зависимости:
- spring-boot-starter-web
- spring-boot-starter-test
- spring-boot-starter-validation
- spring-boot-starter-jdbc
- h2database - lombok
- logback-classic
- logbook-spring-boot-starter

### Схема базы данных (Database map)
![DatabaseMap](FILMO_RATE_DATABASE_MAP.png "Database map:")
### Примеры запросов к базе данных:
Expand Down
2 changes: 0 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@
<java.version>21</java.version>
<h2database.version>2.2.220</h2database.version>
<zolando-logbook.version>3.9.0</zolando-logbook.version>

<!-- PLUGINS -->
<maven-checkstyle-plugin.version>3.3.1</maven-checkstyle-plugin.version>
<maven-compiler-plugin.version>3.11.0</maven-compiler-plugin.version>
<maven-resources-plugin.version>3.3.1</maven-resources-plugin.version>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import ru.yandex.practicum.filmorate.dto.FilmDto;
import ru.yandex.practicum.filmorate.dto.request.create.FilmCreateRequest;
import ru.yandex.practicum.filmorate.dto.request.update.FilmUpdateRequest;
import ru.yandex.practicum.filmorate.model.Assessment;
import ru.yandex.practicum.filmorate.service.FilmService;
import ru.yandex.practicum.filmorate.service.util.LikeAction;

Expand Down Expand Up @@ -37,8 +38,8 @@ public Collection<FilmDto> getTop(@RequestParam(defaultValue = "10", required =
}

@GetMapping("/common")
public Collection<FilmDto> getCommonLikedFilms(@RequestParam(required = true) Long userId,
@RequestParam(required = true) Long friendId) {
public Collection<FilmDto> getCommonLikedFilms(@RequestParam Long userId,
@RequestParam Long friendId) {
return filmService.getCommonLikedFilms(userId, friendId);
}

Expand Down Expand Up @@ -66,14 +67,18 @@ public FilmDto update(@RequestBody FilmUpdateRequest request) {
return filmService.update(request);
}

@PutMapping("/{filmId}/like/{userId}")
public void setLike(@PathVariable long filmId, @PathVariable long userId) {
filmService.changeLike(LikeAction.SET, filmId, userId);
@PutMapping({"/{filmId}/like/{userId}/{assessment}", "/{filmId}/like/{userId}"})
public void setLike(
@PathVariable long filmId,
@PathVariable long userId,
@PathVariable(required = false) Integer assessment
) {
filmService.changeLike(LikeAction.SET, filmId, userId, Assessment.of(assessment));
}

@DeleteMapping("/{filmId}/like/{userId}")
public void deleteLike(@PathVariable long filmId, @PathVariable long userId) {
filmService.changeLike(LikeAction.REMOVE, filmId, userId);
filmService.changeLike(LikeAction.REMOVE, filmId, userId, null);
}

@DeleteMapping("/{filmId}")
Expand Down
12 changes: 7 additions & 5 deletions src/main/java/ru/yandex/practicum/filmorate/dal/FilmStorage.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package ru.yandex.practicum.filmorate.dal;

import ru.yandex.practicum.filmorate.model.Assessment;
import ru.yandex.practicum.filmorate.model.Film;
import ru.yandex.practicum.filmorate.model.Like;

import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

Expand All @@ -30,17 +31,17 @@ public interface FilmStorage {
/// Получить топ фильмов по фильтрам
Collection<Film> getTopByFilters(Integer count, Integer genreId, String year);

/// Поставить лайк
void setLike(long filmId, long userId);
/// Поставить лайк с оценкой (по умолчанию оценка 10)
void addLike(long filmId, long userId, Assessment assessment);

/// Убрать лайк
void removeLike(long filmId, long userId);

/// Получить список фильмов по id режиссера
Collection<Film> getALLFilmsOfDirector(Integer directorId);

/// Метод для получения матрицы лайков всех пользователей
Map<Long, Set<Long>> getAllLikes();
/// Получить все лайки
Set<Like> getAllLikes();

/// Получить список филиьов, которые понравились обоим пользователям
Collection<Film> getCommonLikedFilms(long userId, long friendId);
Expand All @@ -51,5 +52,6 @@ public interface FilmStorage {
/// Удаление фильма по id
void removeFilm(long filmId);

/// Получить коллекцию фильмов по id фильмов
Collection<Film> getFilmsByIds(Collection<Long> ids);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import ru.yandex.practicum.filmorate.dal.FilmStorage;
import ru.yandex.practicum.filmorate.dal.rowmappers.LikeRowMapper;
import ru.yandex.practicum.filmorate.exception.InternalServerException;
import ru.yandex.practicum.filmorate.model.Assessment;
import ru.yandex.practicum.filmorate.model.Film;
import ru.yandex.practicum.filmorate.model.Like;

import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;

import static ru.yandex.practicum.filmorate.dal.database.sql.FilmQueryes.*;

Expand All @@ -34,12 +39,13 @@ public Film createFilm(Film film) {
film.getDescription(),
film.getReleaseDate(),
film.getDuration().toMinutes(),
film.getMpaId()
film.getMpaId(),
0.0f
);
film.setId(id);
insertGenreIds(film);
insertDirectorIds(film);
return findById(id).get();
return film;
}

@Override
Expand All @@ -56,15 +62,19 @@ public Film updateFilm(Film film) {
);
insertGenreIds(film);
insertDirectorIds(film);
return findById(film.getId()).get();
return film;
}

@Override
@Transactional
public Optional<Film> findById(long filmId) {
Optional<Film> filmOptional = findOneByIdInTable(filmId, "films");
filmOptional.ifPresent(film -> film.setGenreIds(getGenreIdsByFilmId(filmId)));
filmOptional.ifPresent(film -> film.setDirectorIds(getDirectorIdsByFilmId(filmId)));
filmOptional.ifPresent(film -> {
film.setGenreIds(getGenreIdsByFilmId(filmId));
film.setDirectorIds(getDirectorIdsByFilmId(filmId));
film.setRate(calculateRating(getLikesByFilmId(filmId)));
});

return filmOptional;
}

Expand All @@ -73,9 +83,18 @@ public Collection<Film> findAll() {
return findManyFilms(SQL_FILMS_FIND_ALL);
}

@Transactional
@Override
public void setLike(long filmId, long userId) {
updateWithControl(SQL_FILMS_SET_LIKE, filmId, userId);
public void addLike(long filmId, long userId, Assessment assessment) {
jdbc.update(
SQL_FILMS_SET_LIKE,
filmId,
userId,
assessment.getValue()
);

float rate = calculateRating(getLikesByFilmId(filmId));
jdbc.update("UPDATE films SET rate = ? WHERE id = ?", rate, filmId);
}

@Override
Expand All @@ -95,16 +114,15 @@ public Collection<Film> getTop(int top) {

@Override
public Collection<Film> getTopByFilters(Integer top, Integer genreId, String year) {
Collection<Film> films;
if (genreId != null && year != null && !year.isEmpty()) {
Collection<Film> films = List.of();
if (genreId != null && year != null && !year.isBlank()) {
films = findManyByQuery(SQL_FILMS_FIND_TOP_BY_GENRE_AND_YEAR, Integer.parseInt(year), genreId, top);
} else if (year != null && !year.isEmpty()) {
} else if (year != null && !year.isBlank()) {
films = findManyByQuery(SQL_FILMS_FIND_TOP_BY_YEAR, Integer.parseInt(year), top);
} else if (genreId != null) {
films = findManyByQuery(SQL_FILMS_FIND_TOP_BY_GENRES, genreId, top);
} else {
films = findManyByQuery(SQL_FILMS_FIND_TOP, top);
}

return films.stream().peek(film -> {
Set<Integer> genres = getGenreIdsByFilmId(film.getId());
film.setGenreIds(new HashSet<>(genres));
Expand All @@ -118,18 +136,6 @@ public boolean checkFilmIsNotPresent(Long filmId) {
return checkIdIsNotPresentInTable(filmId, "films");
}

@Override
public Map<Long, Set<Long>> getAllLikes() {
String sql = "SELECT user_id, film_id FROM likes";
Map<Long, Set<Long>> allLikes = new HashMap<>();
jdbc.query(sql, (rs) -> {
long userId = rs.getLong("user_id");
long filmId = rs.getLong("film_id");
allLikes.computeIfAbsent(userId, k -> new HashSet<>()).add(filmId);
});
return allLikes;
}

@Override
public Collection<Film> getCommonLikedFilms(long userId, long friendId) {
return findManyFilms(SQL_FILMS_FIND_COMMON_LIKED, userId, friendId);
Expand All @@ -156,6 +162,41 @@ public void removeFilm(long filmId) {
updateWithControl("DELETE FROM films WHERE id = ?", filmId);
}

@Override
public Set<Like> getAllLikes() {
String sql = "SELECT user_id, film_id, assessment FROM likes";
Set<Like> allLikes = new HashSet<>();
jdbc.query(sql, (rs) -> {
long filmId = rs.getLong("film_id");
long userId = rs.getLong("user_id");
int assessment = rs.getInt("assessment");
Like like = Like.builder()
.filmId(filmId)
.userId(userId)
.assessment(Assessment.of(assessment))
.build();
allLikes.add(like);
});
return allLikes;
}

@Override
public Collection<Film> getFilmsByIds(Collection<Long> ids) {
if (ids == null || ids.isEmpty()) {
return Collections.emptyList();
}

String inSql = String.join(",", Collections.nCopies(ids.size(), "?"));
String sql = String.format(SQL_FILMS_GET_BY_IDS, inSql);

return findManyFilms(sql, ids.toArray());
}

private Set<Like> getLikesByFilmId(long filmId) {
return jdbc.query(SQL_FILMS_FIND_LIKES_BY_FILM_ID, new LikeRowMapper(), filmId).stream()
.collect(Collectors.toUnmodifiableSet());
}

private void insertGenreIds(Film film) {
jdbc.update("DELETE FROM genres_of_films WHERE film_id = ?", film.getId());
Set<Integer> genreIds = film.getGenreIds();
Expand Down Expand Up @@ -187,8 +228,6 @@ private Collection<Film> findManyFilms(String query, Object... params) {
throw new InternalServerException("Не удалось получить все фильмы из базы.\n" + e.getMessage());
}
});
long userId = rs.getLong("user_id");
if (!rs.wasNull() && film != null) film.addLike(userId);
int genreId = rs.getInt("genre_id");
if (!rs.wasNull() && film != null) film.addGenreId(genreId);
int directorId = rs.getInt("director_id");
Expand All @@ -206,24 +245,23 @@ private Set<Integer> getDirectorIdsByFilmId(long filmId) {
return findColumnByQuery(SQL_FILMS_FIND_DIRECTORIDS_BY_FILM_ID, Integer.class, filmId);
}

@Override
public Collection<Film> getFilmsByIds(Collection<Long> ids) {
if (ids == null || ids.isEmpty()) {
return Collections.emptyList();
private float calculateRating(Set<Like> likes) {
if (likes.isEmpty()) {
return 0f;
} else if (likes.size() == 1) {
return likes.iterator().next().getAssessment().getValue();
}
AtomicLong count = new AtomicLong();
AtomicLong sum = new AtomicLong();
likes.stream()
.map(Like::getAssessment)
.filter(assessment -> !assessment.equals(Assessment.UNDEFINED))
.map(Assessment::getValue).forEach(value -> {
sum.addAndGet(value);
count.getAndIncrement();
});

String inSql = String.join(",", Collections.nCopies(ids.size(), "?"));

String sql = String.format("""
SELECT f.id, f.film_name, f.description, f.release_date, f.duration, f.mpa_id,
l.user_id, gof.genre_id, dof.director_id
FROM films f
LEFT JOIN likes l ON f.id = l.film_id
LEFT JOIN genres_of_films gof ON f.id = gof.film_id
LEFT JOIN directors_of_films dof ON f.id = dof.film_id
WHERE f.id IN (%s)
""", inSql);

return findManyFilms(sql, ids.toArray());
float average = (float) sum.get() / count.get();
return Math.round(average * 100) / 100.0f;
}
}
Loading
Loading