Skip to content

Conversation

@DokPlay
Copy link
Owner

@DokPlay DokPlay commented Nov 4, 2025


Как проверить

  1. Запустить тесты:
    сначала кэш надо удалить
mvn -q clean test
mvn test
  1. Запустить приложение:
mvn spring-boot:run

При необходимости начать «с чистой» БД — останови приложение и удали файлы db/*.mv.dbdb/*.trace.db), они уже в .gitignore.


Примеры запросов (PowerShell, Invoke-RestMethod)

Предусловие: приложение запущено локально на http://localhost:8080.
Советы:

  • В Windows вместо curl используй irm (Invoke-RestMethod) — он корректно парсит JSON и UTF-8.
  • Запросы, помеченные [*], требуют уникальные значения (например, email/login/название фильма). Подставь свои или используй генерацию в примерах ниже.

0. Инициализация сессии

$base = "http://localhost:8080"
$ct   = "application/json; charset=utf-8"
$ProgressPreference = "SilentlyContinue"

1. Справочники

# Все MPA-рейтинги
irm "$base/mpa" | ConvertTo-Json -Depth 10

# Все жанры
irm "$base/genres" | ConvertTo-Json -Depth 10

2. Пользователи

# 2.1 Создать пользователя №1 [*] (email/login должны быть уникальны)
$u1Body = @{
  email    = "user1_{0}@example.com" -f (Get-Random)
  login    = "user1_{0}" -f (Get-Random)
  name     = "User One"
  birthday = "1990-01-01"
} | ConvertTo-Json
$u1 = irm "$base/users" -Method Post -ContentType $ct -Body $u1Body
$u1 | ConvertTo-Json -Depth 10

# 2.2 Создать пользователя №2 [*]
$u2Body = @{
  email    = "user2_{0}@example.com" -f (Get-Random)
  login    = "user2_{0}" -f (Get-Random)
  name     = "User Two"
  birthday = "1991-02-02"
} | ConvertTo-Json
$u2 = irm "$base/users" -Method Post -ContentType $ct -Body $u2Body
$u2 | ConvertTo-Json -Depth 10

# 2.3 Получить всех пользователей / одного по id
irm "$base/users"                | ConvertTo-Json -Depth 10
irm "$base/users/$($u1.id)"      | ConvertTo-Json -Depth 10

3. Дружба (add/remove + проверка)

# 3.1 Добавить в друзья u2 к u1
irm "$base/users/$($u1.id)/friends/$($u2.id)" -Method Put

# 3.2 Проверка друзей у u1
irm "$base/users/$($u1.id)/friends" | ConvertTo-Json -Depth 10

# 3.3 Удалить из друзей
irm "$base/users/$($u1.id)/friends/$($u2.id)" -Method Delete

# 3.4 Повторная проверка
irm "$base/users/$($u1.id)/friends" | ConvertTo-Json -Depth 10

4. Фильмы и лайки

# 4.1 Все фильмы
irm "$base/films" | ConvertTo-Json -Depth 10

# 4.2 Создать фильм [*] (название сделаем уникальным для наглядности)
$filmBody = @{
  name        = "Test Movie {0}" -f (Get-Random)
  description = "Some description"
  releaseDate = "2010-01-01"
  duration    = 110
  mpa         = @{ id = 1 }
  genres      = @(@{ id = 1 }, @{ id = 2 })
} | ConvertTo-Json
$film = irm "$base/films" -Method Post -ContentType $ct -Body $filmBody
$film | ConvertTo-Json -Depth 10

# 4.3 Поставить лайки от двух пользователей
irm "$base/films/$($film.id)/like/$($u1.id)" -Method Put
irm "$base/films/$($film.id)/like/$($u2.id)" -Method Put

# 4.4 Проверить фильм и топ популярных
irm "$base/films/$($film.id)"                 | ConvertTo-Json -Depth 10
irm "$base/films/popular?count=10"            | ConvertTo-Json -Depth 10

# 4.5 Снять лайк и перепроверить
irm "$base/films/$($film.id)/like/$($u1.id)" -Method Delete
irm "$base/films/$($film.id)"                 | ConvertTo-Json -Depth 10

5. Дополнительно (CRUD-примеры)

# Обновить фильм (PUT /films)
$filmUpdate = @{
  id          = $film.id
  name        = "$($film.name) — updated"
  description = $film.description
  releaseDate = $film.releaseDate
  duration    = 120
  mpa         = @{ id = 1 }
  genres      = @(@{ id = 1 })   # оставим один жанр
} | ConvertTo-Json
irm "$base/films" -Method Put -ContentType $ct -Body $filmUpdate | ConvertTo-Json -Depth 10

# Получить пользователя по id (ручная проверка)
irm "$base/users/$($u2.id)" | ConvertTo-Json -Depth 10

Что делать, если вернулся 4xx/5xx

  • Для запросов [*] нужно использовать уникальные email/login (для пользователей) и, при необходимости, уникальное имя фильма.
    Примеры выше уже генерируют уникальные значения через Get-Random.
  • Если всё равно получаешь ошибку из-за старых данных в H2 (файловый режим), останови приложение и удали файлы БД в каталоге db/ (*.mv.db, *.trace.db), затем запусти снова.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

return;
}
final Set<Integer> seen = new HashSet<>();
final LinkedHashSet<Genre> normalized = new LinkedHashSet<>();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

попробуй обозначить тип переменной Set

  • final Set normalized = new LinkedHashSet<>();
    не должно ничего поменятся, т.к. конкретный объект подставляется в правой стороне.
    Такой подход дает возможность переменной, методам не зависеть от конкретной реализации. И при каком либо измененении, рефакторингу подвергнется только одна строка в одном методе одного класса.

final String sql = "SELECT f.id, f.name, f.description, f.release_date, f.duration, f.mpa_id, "
+ "m.name AS mpa_name FROM films f JOIN mpa_ratings m ON f.mpa_id = m.id ORDER BY f.id";
final List<Film> films = jdbcTemplate.query(sql, FILM_MAPPER);
films.forEach(this::enrichFilm);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

тут у нас беда. К примеру фильмов 1000.
Мы ожидаем, что вытащим данные из базы в 1-3 запроса.

  • но в текущей реализации стреляет ошибка N+1, один общий запрос + на каждый фильм по два запроса жанры и лайки, итого 1 + 2*1000 = 2001 запрос в базу на ровном месте.
  • если 10ть пользователей кликнет получение фильмов, то получим 20010 запросов в течении короткого времени.

if (userId == null) {
continue;
}
jdbcTemplate.update(sql, film.getId(), userId);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

тоже запросы в цикле. попробуй батчАпдейт

+ "JOIN mpa_ratings m ON f.mpa_id = m.id "
+ "LEFT JOIN film_likes fl ON f.id = fl.film_id "
+ "GROUP BY f.id, f.name, f.description, f.release_date, f.duration, f.mpa_id, m.name "
+ "ORDER BY COUNT(fl.user_id) DESC, f.id ASC LIMIT ?";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • fl.user_id боюсь, что может быть не оптимальным запросом подсчет каждому фильму кол-ва лайков и затем применение лимита. Как будто всю таблицу перетрясти может. Можно конечно проверить анализом запроса к базе.
    Есть идея. к примеру добавить фильму поле Integer - рейтинг, когда лайк добавили , обновить поле , убавили - обновить. Но при выводе не потребуется считать .
  • еще вопрос, применили соединение с таблицей film_likes и получил все нужные данные, но двумя строками ниже в цикле вызываем loadLikes и снова N+1

.withTableName("users")
.usingGeneratedKeyColumns("id");
final LocalDate birthday = user.getBirthday();
final HashMap<String, Object> values = new HashMap<>();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

применяй более широкий тип переменной, тут к примеру будет оптимальным:
final Map<String, Object> values = new HashMap<>();

if (friendId == null || friendId.equals(user.getId())) {
continue;
}
jdbcTemplate.update(sql, user.getId(), friendId);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

попробуй batchUpdate

@DokPlay DokPlay force-pushed the add-database branch 4 times, most recently from b40b6cc to f7787da Compare November 6, 2025 00:49
@chehovnp
Copy link

chehovnp commented Nov 6, 2025

работу принимаю.
Есть замечание по кол-ву коммитов.

Рекомендую два инструмента:

  • amend commit и force push позволяют не делать новый минорный коммит, а перезаписывают предыдущий.
  • squash into - рекомендуется сделать перед ревью, когда коммитов десятки, то их можно с хлопнуть в один или несколько, разбив тем самым на важные блоки.

@DokPlay
Copy link
Owner Author

DokPlay commented Nov 7, 2025

Обновление норм жанров в FilmService, которое позволяет использовать заданный интерфейс вместо конкретной реализации, сохраняя гибкость метода для последующих изменений.

Переработан FilmDbStorage для загрузки жанров и лайков, стандартизации карт вставок и пакетного сохранения жанров/лайков, чтобы удалить шаблоны доступа N+1 и сократить количество запросов.

Изменен UserDbStorage для использования карт, типизированных интерфейсов и обновлений пакетного сохранения отношений между странами для более эффективной записи.

Оптимизировал загрузку дружбы для пользователей findAll, чтобы получилось N+1 запроса, и повторно использовал помощника для однопользовательского поиска.

Уточнил название количества нормализованных жанров, сохраняющих использование интерфейса Set, чтобы лучше отображать рекомендации по брендам и названиям.

Переписал SQL для выбора популярных фильмов, добавив подзапрос с предварительной агрегацией лайков и использованием COALESCE, что увеличивает нагрузку от повторных подсчетов по таблице лайков.

Дедуплицирует идентификаторы фильмов перед воспроизведением загрузкой жанров и стремится к минимальному количеству пакетных запросов для любого расширенного результирующего набора.

Дедулицировал идентификаторы пользователей перед извлечением данных о дружбе и согласовал обработку пустых результатов с Collections.emptyMap(), чтобы избежать избыточного расширения заполнения.

Переиспользовал общий помощник richUsers внутри getById, чтобы при запросе одного пользователя его друзья тоже сразу подгружались, и для этого не нужен отдельный «загрузчик» друзей.

Удал лишний метод, который загружал друзей для одного пользователя, потому что теперь всё происходит через общий путь обогащения данных (тот самый richUsers).

Переименовал нормализованную коллекцию жанров в FilmService, чтобы было ясно, что это именно Set (множество), а не просто список, и таким образом жанры в ней не дублируются — всё в соответствии с примечаниями из код-ревью.

Повторно использовался специальный конструктор-заполнитель в UserDbStorage, так что при включении в другом SQL используется тот же помощник при формировании параметров, который используется в других местах, что подтверждает согласованность и удобочитаемость.

Повторно использовал модель нормализации фильма при обогащении результатов запроса и переключил подготовку JDBC vararg на toArray(Object[]::new) для жанра загрузки и аналогичных файлов.

Применил то же типобезопасное преобразование массива при расширении данных пользователей о дружбе.

Теперь фильмы, в которых нет жанров, получают новый пустой LinkedHashSet,
вместо null и вместо какого-то общего набора для всех.

Добавил импорт JDBC ResultSet и SQLException и переработал загрузку так, чтобы использовать отдельный ResultSetExtractor.

За счёт этого:

все друзья подгружаются одним пакетным запросом к БД;

при этом повторно используется уже существующая логика, которая выполняет Map с результатом (то есть код не дублируется).

Обобщил (унифицировал) помощники обогащения фильмов:
снаружи они теперь возвращают Map и Set через интерфейсы (Map, Set),
а внутри по-прежнему сохраняется конкретный LinkedHashSet.
→ Логика хранения не требует жёстко к производству коллекций.

Обновил утилиты обогащения для пользователей, чтобы они вернули
Map<Long, Set> (карта: id пользователя → множество id друзей/лайков и т.п.).
→ Код, который это использует, основан на абстракции (Карта, Набор), а не в какой-либо реализации.

Сделал загрузку лайков детерминированной – то теперь есть лайки приходят всегда в одном и том же порядке. Мы добавили этот запрос сортировки в таблицу Film_likes перед темой, чтобы собирать результаты.

Сохранил упорядоченность коллекций лайков, перейдя на LinkedHashSet при извлечении данных из ResultSet.

Нормализация жанров переносится в сеттер сущности.

Теперь фильм сам по себе, чтобы жанры были в нужном виде.
Заранее задаёт размер списков для батчей (жанры, лайки, дружба).
Он примерно знает, сколько элементов будет в пакете, и создает список нужного размера.
Он собирает лишние перераспределения массива внутри ArrayList → чуть меньше мусора в куче и меньше аллокаций.

В обогащении фильмов
Теперь, если для фильма нет данных по жанрам или лайкам,
он по умолчанию подставляет пустые коллекции,
а не устанавливает null.
При этом исправлении проблемы N+1 (когда много мелких запросов) остается работоспособным.

В расширении пользователей
упростил код: дружбу всегда добавляется, начиная с пустого уровня,
поэтому не нужно вручную делать if (friends != null) и прочие нулевые проверки.

Упрощенное обогащение фильмов по умолчанию, основанное на Collections.emptySet(), что позволяет избежать происхождения основных определений, сохраняя при этом количество лайков и жанров без изменений.

Применил тот же вывод о пустом наборе для обогащения пользователей, чтобы друзья постоянно использовали общие вспомогательные действия.

Заменил пакетные запросы MERGE в хранилище фильмов на обычный INSERT,
причём:

теперь SQL более «переносимый» между разными БД (без особенностей конкретного диалекта);

при этом сохраняется защита от дублей при загрузке жанров и лайков (защита дедупликации).

Сделал ту же замену INSERT для пакетного обновления дружбы пользователей,
чтобы тоже не использовать базоспецифичный синтаксис.

Упростил вспомогательные методы для обогащения фильмов и пользователей в JDBC —
теперь, когда готовит набор идентификаторов, использует Stream.toList(),
из-за этого можно удалить лишние импорты Collectors.

То же самое сделал в пайплайне выбор популярных фильмов в памяти —
тоже перевёл на Stream.toList(), поэтому сборщикам импорта больше не нужно.

Заранее задал размер вспомогательных наборов в FilmService.normalizeGenres на основе размера входной коллекции жанров, чтобы уменьшить количество перехэширований при нормализации.

Для кэшей обогащения фильмов
теперь они используют LinkedHashMap, чтобы жанры и лайки, загруженные батчем,
обеспечивали приемлемый порядок при обходе (когда обратно заливаются в сущности Film).

То же самое для пользователей дружбы
Обогащение друзей тоже перевёл на LinkedHashMap,
чтобы порядок при восстановлении множества друзей был стабильным и одинаковым.

Вынес основную часть SQL-запроса для выбора фильмов в одну константу и
переиспользовал ее в трех координатах — в запросе списка фильмов, детальной карточки и популярного списка.

Смысл: теперь везде, где выбираются, используются один и тот же фильмы SELECT f.id, f.name, ...,
чтобы набор колонок (проекция) всегда был одинаковым и последовательным.

Заранее задал размер вспомогательных наборов (наборов), которые использовались при пакетной загрузке жанров и лайков фильма, чтобы уменьшить количество перехешированных во время сохранения в базе.

Уменьшение количества повторяющихся поисковиков по картам при просмотре фильмов за счет повторного использования выбранных жанровых и аналогичных пакетов перед применением их к каждому объекту.

Защищенный поиск друзей пользователя, поэтому идентификаторы друзей включаются только тогда, когда пакетный поиск предоставляет данные.

Переиспользовал локальные ссылки на коллекции жанров и лайков при включении пакетной загрузки в FilmDbStorage и заранее задает размер вспомогательных контейнеров, чтобы избежать лишних обращений к объекту фильма во время JDBC-обновлений.

Упростил обогащение фильмов, теперь все основано на Film#setGenres, а сама модель обрабатывает нулевые или пустые наборы жанров без исключений.

Теперь при обогащении фильмов, если в фильме нет жанров или лайков,
по умолчанию под названием пустые коллекции, а не null.
Из-за этой модели фильм всегда в консистентном состоянии, и при «гидрации» (когда объект наполняется данными из БД) не нужно делать уведомления if (genres != null) / if (likes != null).

Добавил проверку на отсутствие лайков перед пакетной вставкой в ​​фильм_лайки:
если лайков нет, то батч вообще не запускается, и база лишний раз не трогается.

Заменил существующую модификацию ограничений в FilmService неизменяемой локальной переменной перед делегированием в хранилище, сохранив обработку по умолчанию неизменной.

Нормализовал предел параметров внутри FilmDbStorage.findMostPopular с помощью выделенной локальной переменной, чтобы метод больше не изменял свои аргументы, по-прежнему постоянно используя централизованную логику по умолчанию.

Обновлены параметры карты вставки JDBC в FilmDbStorage, чтобы предварительно использовать LinkedHashMap определенного размера, исключить определенный порядок столбцов и удалить неиспользуемые импортируемые хэш-карты.

Применил тот же подход к детерминированному упорядочению параметров вставки в UserDbStorage и заменил избыточный импорт хэш-карты.

Повторно использовал константу DEFAULT_POPULAR_LIMIT через интерфейс FilmStorage, чтобы выровнять сервис и хранилище JDBC для резервного показа популярности.

Нормализована обработка пустых жанров с помощью Collections.emptySet() перед передачей разработчику фильма, что обеспечивает использование типизированного интерфейса во всем сервисе.

@DokPlay DokPlay merged commit dcb0504 into main Nov 17, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants