Skip to content
This repository was archived by the owner on May 21, 2026. It is now read-only.
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
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import com.espacogeek.geek.services.GenreService;
import com.espacogeek.geek.services.MediaService;
import com.espacogeek.geek.services.SeasonService;
import com.espacogeek.geek.utils.MediaLazyLoader;

import jakarta.persistence.Id;
import jakarta.persistence.ManyToMany;
Expand All @@ -48,20 +49,23 @@ public class GenericMediaDataControllerImpl implements MediaDataController {
private final AlternativeTitlesService alternativeTitlesService;
private final ExternalReferenceService externalReferenceService;
private final SeasonService seasonService;
private final MediaLazyLoader mediaLazyLoader;

@Autowired
public GenericMediaDataControllerImpl(
@Lazy MediaService mediaService,
GenreService genreService,
AlternativeTitlesService alternativeTitlesService,
ExternalReferenceService externalReferenceService,
SeasonService seasonService
SeasonService seasonService,
MediaLazyLoader mediaLazyLoader
) {
this.mediaService = mediaService;
this.genreService = genreService;
this.alternativeTitlesService = alternativeTitlesService;
this.externalReferenceService = externalReferenceService;
this.seasonService = seasonService;
this.mediaLazyLoader = mediaLazyLoader;
}

public GenericMediaDataControllerImpl getInstance() {
Expand All @@ -73,6 +77,8 @@ public GenericMediaDataControllerImpl getInstance() {
*/
@Override
public MediaModel updateAllInformation(MediaModel media, MediaModel result, TypeReferenceModel typeReference, MediaApi mediaApi) {
mediaLazyLoader.initializeCollections(media);

if (result == null) {
Collection<ExternalReferenceModel> externalReferences = media.getExternalReference();
if (externalReferences == null || !Hibernate.isInitialized(externalReferences)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import com.espacogeek.geek.services.MediaCategoryService;
import com.espacogeek.geek.services.SeasonService;
import com.espacogeek.geek.services.TypeReferenceService;
import com.espacogeek.geek.utils.MediaLazyLoader;

import jakarta.annotation.PostConstruct;

Expand All @@ -51,9 +52,10 @@ public MovieControllerImpl(
GenreService genreService,
AlternativeTitlesService alternativeTitlesService,
ExternalReferenceService baseExternalReferenceService,
SeasonService seasonService
SeasonService seasonService,
MediaLazyLoader mediaLazyLoader
) {
super(mediaService, genreService, alternativeTitlesService, baseExternalReferenceService, seasonService);
super(mediaService, genreService, alternativeTitlesService, baseExternalReferenceService, seasonService, mediaLazyLoader);
this.movieAPI = movieAPI;
this.mediaCategoryService = mediaCategoryService;
this.externalReferenceService = externalReferenceService;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import java.util.List;

import com.espacogeek.geek.services.*;
import com.espacogeek.geek.utils.MediaLazyLoader;
import lombok.extern.slf4j.Slf4j;
import org.json.simple.JSONObject;
import org.springframework.beans.factory.annotation.Qualifier;
Expand All @@ -33,8 +34,8 @@ public class SerieControllerImpl extends GenericMediaDataControllerImpl {

private TypeReferenceModel typeReference;

public SerieControllerImpl(MediaService mediaService, GenreService genreService, AlternativeTitlesService alternativeTitlesService, ExternalReferenceService externalReferenceService, SeasonService seasonService, MediaApi tvSeriesApi, MediaCategoryService mediaCategoryService, ExternalReferenceService externalReferenceService1, TypeReferenceService typeReferenceService) {
super(mediaService, genreService, alternativeTitlesService, externalReferenceService, seasonService);
public SerieControllerImpl(MediaService mediaService, GenreService genreService, AlternativeTitlesService alternativeTitlesService, ExternalReferenceService externalReferenceService, SeasonService seasonService, MediaApi tvSeriesApi, MediaCategoryService mediaCategoryService, ExternalReferenceService externalReferenceService1, TypeReferenceService typeReferenceService, MediaLazyLoader mediaLazyLoader) {
super(mediaService, genreService, alternativeTitlesService, externalReferenceService, seasonService, mediaLazyLoader);
this.tvSeriesApi = tvSeriesApi;
this.mediaCategoryService = mediaCategoryService;
this.externalReferenceService = externalReferenceService1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
@Repository
public interface AlternativeTitlesRepository extends JpaRepository<AlternativeTitleModel, Integer> {

List<AlternativeTitleModel> findByMedia(MediaModel media);

List<AlternativeTitleModel> findByMediaIn(Collection<MediaModel> medias);

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@
import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import com.espacogeek.geek.models.GenreModel;

@Repository
public interface GenreRepository extends JpaRepository<GenreModel, Integer> {
public List<GenreModel> findAllByNameIn(List<String> nameGenres);

@Query("SELECT g FROM MediaModel m JOIN m.genre g WHERE m.id = :mediaId")
List<GenreModel> findByMediaId(@Param("mediaId") Integer mediaId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
@Repository
public interface SeasonRepository extends JpaRepository<SeasonModel, Integer> {

List<SeasonModel> findByMedia(MediaModel media);

List<SeasonModel> findByMediaIn(Collection<MediaModel> medias);

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
import org.springframework.dao.DataIntegrityViolationException;

import com.espacogeek.geek.models.AlternativeTitleModel;
import com.espacogeek.geek.models.MediaModel;

public interface AlternativeTitlesService {
public List<AlternativeTitleModel> saveAll(List<AlternativeTitleModel> alternativeTitles) throws DataIntegrityViolationException;

public List<AlternativeTitleModel> findAll(MediaModel media);
}
3 changes: 3 additions & 0 deletions src/main/java/com/espacogeek/geek/services/GenreService.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
import java.util.List;

import com.espacogeek.geek.models.GenreModel;
import com.espacogeek.geek.models.MediaModel;

public interface GenreService {
public List<GenreModel> findAllByNames(List<String> names);

public List<GenreModel> findAll(MediaModel media);

public List<GenreModel> saveAll(List<GenreModel> genres);
}
3 changes: 3 additions & 0 deletions src/main/java/com/espacogeek/geek/services/SeasonService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

import java.util.List;

import com.espacogeek.geek.models.MediaModel;
import com.espacogeek.geek.models.SeasonModel;

public interface SeasonService {
public List<SeasonModel> saveAll(List<SeasonModel> seasons);

public List<SeasonModel> findAll(MediaModel media);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.springframework.stereotype.Service;

import com.espacogeek.geek.models.AlternativeTitleModel;
import com.espacogeek.geek.models.MediaModel;
import com.espacogeek.geek.repositories.AlternativeTitlesRepository;
import com.espacogeek.geek.services.AlternativeTitlesService;

Expand All @@ -18,4 +19,9 @@ public class AlternativeTitlesServiceImpl implements AlternativeTitlesService {
public List<AlternativeTitleModel> saveAll(List<AlternativeTitleModel> alternativeTitles) throws DataIntegrityViolationException {
return alternativeTitlesRepository.saveAll(alternativeTitles);
}

@Override
public List<AlternativeTitleModel> findAll(MediaModel media) {
return alternativeTitlesRepository.findByMedia(media);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.springframework.stereotype.Service;

import com.espacogeek.geek.models.GenreModel;
import com.espacogeek.geek.models.MediaModel;
import com.espacogeek.geek.repositories.GenreRepository;
import com.espacogeek.geek.services.GenreService;

Expand All @@ -23,6 +24,14 @@ public List<GenreModel> findAllByNames(List<String> names) {
return genreRepository.findAllByNameIn(names);
}

/**
* @see GenreService#findAll(MediaModel)
*/
@Override
public List<GenreModel> findAll(MediaModel media) {
return genreRepository.findByMediaId(media.getId());
}

/**
* @see GenreService#saveAll(List<GenreModel>)
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
package com.espacogeek.geek.services.impl;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom;
Expand All @@ -31,13 +27,10 @@
import com.espacogeek.geek.types.MediaPage;
import com.espacogeek.geek.types.MediaSimplefied;
import com.espacogeek.geek.utils.MediaUtils;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.OneToMany;
import com.espacogeek.geek.utils.MediaLazyLoader;
import jakarta.transaction.Transactional;
import org.hibernate.Hibernate;

import static com.espacogeek.geek.utils.TextUtils.capitalize;

/**
* A Implementation class of MediaService @see MediaService
*/
Expand Down Expand Up @@ -69,6 +62,8 @@ public class MediaServiceImpl implements MediaService {
@Qualifier("tvSeriesApi")
private final MediaApi tvSeriesApi;

private final MediaLazyLoader mediaLazyLoader;

public MediaServiceImpl(
MediaRepository mediaRepository,
ExternalReferenceRepository externalsRepo,
Expand All @@ -78,7 +73,8 @@ public MediaServiceImpl(
TypeReferenceService typeReferenceService,
@Qualifier("gamesAndVNsAPI") MediaApi gamesAndVNsAPI,
@Qualifier("movieAPI") MediaApi movieAPI,
@Qualifier("tvSeriesApi") MediaApi tvSeriesApi
@Qualifier("tvSeriesApi") MediaApi tvSeriesApi,
MediaLazyLoader mediaLazyLoader
) {
this.mediaRepository = mediaRepository;
this.externalsRepo = externalsRepo;
Expand All @@ -89,6 +85,7 @@ public MediaServiceImpl(
this.gamesAndVNsAPI = gamesAndVNsAPI;
this.movieAPI = movieAPI;
this.tvSeriesApi = tvSeriesApi;
this.mediaLazyLoader = mediaLazyLoader;
}

/**
Expand Down Expand Up @@ -175,31 +172,12 @@ public Optional<MediaModel> findByReferenceAndTypeReference(ExternalReferenceMod
@Override
@Transactional
public Optional<MediaModel> findByIdEager(Integer id) {
var fieldList = new ArrayList<Field>();
MediaModel media = (MediaModel) mediaRepository.findById(id).orElse(null);

if (media == null)
return Optional.empty();

for (Field field : media.getClass().getDeclaredFields()) {
field.setAccessible(true);
if (field.isAnnotationPresent(OneToMany.class) || field.isAnnotationPresent(ManyToMany.class)) {
fieldList.add(field);
}
}

for (Field field : fieldList) {
try {
String getterName = "get" + capitalize(field.getName());
Method getter = media.getClass().getMethod(getterName);
var fieldValue = getter.invoke(media);
if (fieldValue instanceof Collection) {
((Collection<?>) fieldValue).size(); // This will initialize the collection
}
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
log.error("Failed to initialize field {} for media id={}: {}", field.getName(), media.getId(), e.getMessage());
}
}
mediaLazyLoader.initializeCollections(media);
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

findByIdEager() now relies on mediaLazyLoader.initializeCollections(media) instead of the previous reflective initialization of all @OneToMany/@ManyToMany fields. As implemented, MediaLazyLoader does not initialize company and people, so callers can still hit LazyInitializationException when accessing those associations outside the transaction. Please either extend MediaLazyLoader to include them or ensure findByIdEager still eagerly initializes them.

Suggested change
mediaLazyLoader.initializeCollections(media);
mediaLazyLoader.initializeCollections(media);
if (media.getCompany() != null) {
Hibernate.initialize(media.getCompany());
}
if (media.getPeople() != null) {
Hibernate.initialize(media.getPeople());
}

Copilot uses AI. Check for mistakes.

media = update(media);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.espacogeek.geek.models.MediaModel;
import com.espacogeek.geek.models.SeasonModel;
import com.espacogeek.geek.repositories.SeasonRepository;
import com.espacogeek.geek.services.SeasonService;
Expand All @@ -18,4 +19,9 @@ public class SeasonServiceImpl implements SeasonService {
public List<SeasonModel> saveAll(List<SeasonModel> seasons) {
return seasonRepository.saveAll(seasons);
}

@Override
public List<SeasonModel> findAll(MediaModel media) {
return seasonRepository.findByMedia(media);
}
}
75 changes: 75 additions & 0 deletions src/main/java/com/espacogeek/geek/utils/MediaLazyLoader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.espacogeek.geek.utils;

import java.util.LinkedHashSet;

import org.hibernate.Hibernate;
import org.springframework.stereotype.Component;

import com.espacogeek.geek.models.MediaModel;
import com.espacogeek.geek.services.AlternativeTitlesService;
import com.espacogeek.geek.services.ExternalReferenceService;
import com.espacogeek.geek.services.GenreService;
import com.espacogeek.geek.services.SeasonService;

/**
* Utility Spring bean that guarantees all lazy-loaded {@code Set<>} collections
* of a {@link MediaModel} are available for use outside an active Hibernate session.
*
* <p>With Java 21 virtual threads, thread-local Hibernate sessions can be closed
* before update methods finish processing a detached entity, causing
* {@code LazyInitializationException}. This class solves the problem once and for all
* by checking {@link Hibernate#isInitialized} for every lazy collection and falling
* back to a fresh repository query when the session is no longer active.
Comment on lines +18 to +22
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

The Javadoc says this checks Hibernate.isInitialized “for every lazy collection”, but the implementation/documentation only covers 4 collections. This is misleading given MediaModel also has lazy company and people sets. Either update the wording to match what’s covered, or extend the loader so the doc and behavior align.

Copilot uses AI. Check for mistakes.
*
* <p>Covered collections: {@code externalReference}, {@code alternativeTitles},
* {@code genre}, {@code season}.
*/
@Component
public class MediaLazyLoader {

private final ExternalReferenceService externalReferenceService;
private final AlternativeTitlesService alternativeTitlesService;
private final GenreService genreService;
private final SeasonService seasonService;

public MediaLazyLoader(
ExternalReferenceService externalReferenceService,
AlternativeTitlesService alternativeTitlesService,
GenreService genreService,
SeasonService seasonService) {
this.externalReferenceService = externalReferenceService;
this.alternativeTitlesService = alternativeTitlesService;
this.genreService = genreService;
this.seasonService = seasonService;
}

/**
* Ensures all lazy {@link java.util.Set} collections of the given {@link MediaModel}
* are initialized. For each collection that is {@code null} or not yet initialized
* by Hibernate (e.g., because the session that loaded the entity has been closed),
* the data is loaded directly from the corresponding service and set back on the entity.
*
* @param media the {@link MediaModel} to initialize; no-op when {@code null} or unsaved
*/
public void initializeCollections(MediaModel media) {
if (media == null || media.getId() == null) {
return;
}

if (media.getExternalReference() == null || !Hibernate.isInitialized(media.getExternalReference())) {
media.setExternalReference(new LinkedHashSet<>(externalReferenceService.findAll(media)));
}

if (media.getAlternativeTitles() == null || !Hibernate.isInitialized(media.getAlternativeTitles())) {
media.setAlternativeTitles(new LinkedHashSet<>(alternativeTitlesService.findAll(media)));
}

if (media.getGenre() == null || !Hibernate.isInitialized(media.getGenre())) {
media.setGenre(new LinkedHashSet<>(genreService.findAll(media)));
}

if (media.getSeason() == null || !Hibernate.isInitialized(media.getSeason())) {
media.setSeason(new LinkedHashSet<>(seasonService.findAll(media)));
Comment on lines +67 to +72
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

MediaLazyLoader initializes externalReference/alternativeTitles/genre/season, but MediaModel also has lazy company and people sets. Since findByIdEager() was changed to rely on this loader, those two associations can still remain uninitialized and cause LazyInitializationException when accessed outside a session. Consider adding company/people initialization (with appropriate service/repo fallbacks) or documenting that they’re intentionally excluded.

Copilot uses AI. Check for mistakes.
}
}
}
Loading
Loading