blaze-persistence icon indicating copy to clipboard operation
blaze-persistence copied to clipboard

Hibernate first level cache support not working with JpaRepository.findById() when invoked more than once in same @Transactional

Open Psycomentis06 opened this issue 2 months ago • 4 comments

@EnableBlazeRepositories integration with spring boot and spring data jpa does not support Hibernate first level cache.

Calling JpaRepository.findById n times in the same @Transactional block executes the SELECT query n times.

This is my repository setup

public interface SupplierReviewMetricRepository extends JpaRepository<SupplierReviewMetric, UUID>,
 JpaSpecificationExecutor<SupplierReviewMetric> {}

This is my blaze config

@Configuration
@EnableEntityViews(basePackages = {"com.gitlab.mess"})
@EnableBlazeRepositories(basePackages = {"com.gitlab.mess"})
public class BlazePersistenceConfig {

    @PersistenceUnit
    private EntityManagerFactory entityManagerFactory;

    @Bean
    @Lazy(false)
    public CriteriaBuilderFactory criteriaBuilderFactory() {
        var config = Criteria.getDefault();
        return config.createCriteriaBuilderFactory(entityManagerFactory);
    }

    @Bean
    public EntityViewManager entityViewManager(CriteriaBuilderFactory cbf, EntityViewConfiguration evc) {
        return evc.createEntityViewManager(cbf);
    }
}

Used libraries :

  • spring-boot:3.5.4
  • blaze-persistence:1.6.16

Psycomentis06 avatar Sep 30 '25 15:09 Psycomentis06

In my existing code base I used to use the findById always instead of getById which is deprecated in favor of getReferenceById. So I made a simple experiment with findById vs getReferenceById.

I made them on the fly. I used a controller and a service for quick test.

Test 1 (findById)

@Service
public class TestService {
    private final CategoryBlazeRepository categoryBlazeRepository;

    public TestService(CategoryBlazeRepository categoryBlazeRepository) {
        this.categoryBlazeRepository = categoryBlazeRepository;
    }

    @Transactional
    public Category see(UUID id) {
        return categoryBlazeRepository.findById(id).orElse(new Category().setName("Not Found 3"));
    }
}
@RestController
@RequestMapping("/test")
public class TestController {

    private final CategoryBlazeRepository categoryBlazeRepository;
    private final TestService testService;

    public TestController(CategoryBlazeRepository categoryBlazeRepository, TestService testService) {
        this.categoryBlazeRepository = categoryBlazeRepository;
        this.testService = testService;
    }

    @GetMapping
    @Transactional
    public Object test() {
        UUID categoryId = UUID.fromString("36bcec62-f2d0-416b-8618-bf1f24db5fdd");
        var c = categoryBlazeRepository.findById(categoryId).orElse(new Category().setName("Not Found 1"));
        c = categoryBlazeRepository.findById(categoryId).orElse(new Category().setName("Not Found 2"));
        c = testService.see(categoryId);
        return c.getName();
    }
}

Logs

2025-10-08T09:45:33.589+01:00 DEBUG 7148 --- [spring-demo] [omcat-handler-0] org.hibernate.SQL                        : select c1_0.id,c1_0.created_at,c1_0.created_by,c1_0.image,c1_0.name,c1_0.sort_order,c1_0.parent_id,c1_0.published_at,c1_0.revision,c1_0.slug,c1_0.updated_at,c1_0.updated_by from spring.category c1_0 where c1_0.id=?
2025-10-08T09:45:33.632+01:00 DEBUG 7148 --- [spring-demo] [omcat-handler-0] org.hibernate.SQL                        : select c1_0.id,c1_0.created_at,c1_0.created_by,c1_0.image,c1_0.name,c1_0.sort_order,c1_0.parent_id,c1_0.published_at,c1_0.revision,c1_0.slug,c1_0.updated_at,c1_0.updated_by from spring.category c1_0 where c1_0.id=?
2025-10-08T09:45:33.637+01:00 DEBUG 7148 --- [spring-demo] [omcat-handler-0] org.hibernate.SQL                        : select c1_0.id,c1_0.created_at,c1_0.created_by,c1_0.image,c1_0.name,c1_0.sort_order,c1_0.parent_id,c1_0.published_at,c1_0.revision,c1_0.slug,c1_0.updated_at,c1_0.updated_by from spring.category c1_0 where c1_0.id=?

Test 2 (getReferenceById)

@Service
public class TestService {
    private final CategoryBlazeRepository categoryBlazeRepository;

    public TestService(CategoryBlazeRepository categoryBlazeRepository) {
        this.categoryBlazeRepository = categoryBlazeRepository;
    }

    @Transactional
    public Category see(UUID id) {
        return categoryBlazeRepository.getReferenceById(id);
    }
}
@RestController
@RequestMapping("/test")
public class TestController {

    private final CategoryBlazeRepository categoryBlazeRepository;
    private final TestService testService;

    public TestController(CategoryBlazeRepository categoryBlazeRepository, TestService testService) {
        this.categoryBlazeRepository = categoryBlazeRepository;
        this.testService = testService;
    }

    @GetMapping
    @Transactional
    public Object test() {
        UUID categoryId = UUID.fromString("36bcec62-f2d0-416b-8618-bf1f24db5fdd");
        var c = categoryBlazeRepository.getReferenceById(categoryId);
        c = categoryBlazeRepository.getReferenceById(categoryId);
        c = testService.see(categoryId);
        return c.getName();
    }
}

Logs

2025-10-08T09:54:03.755+01:00 DEBUG 13028 --- [spring-demo] [omcat-handler-0] org.hibernate.SQL                        : select c1_0.id,c1_0.created_at,c1_0.created_by,c1_0.image,c1_0.name,c1_0.sort_order,c1_0.parent_id,c1_0.published_at,c1_0.revision,c1_0.slug,c1_0.updated_at,c1_0.updated_by from spring.category c1_0 where c1_0.id=?

Test 3 (Mix findById and getReferenceById)

@Service
public class TestService {
    private final CategoryBlazeRepository categoryBlazeRepository;

    public TestService(CategoryBlazeRepository categoryBlazeRepository) {
        this.categoryBlazeRepository = categoryBlazeRepository;
    }

    @Transactional
    public Category see(UUID id) {
        return categoryBlazeRepository.findById(id).orElse(new Category().setName("Not Found 2"));
    }
}
@RestController
@RequestMapping("/test")
public class TestController {

    private final CategoryBlazeRepository categoryBlazeRepository;
    private final TestService testService;

    public TestController(CategoryBlazeRepository categoryBlazeRepository, TestService testService) {
        this.categoryBlazeRepository = categoryBlazeRepository;
        this.testService = testService;
    }

    @GetMapping
    @Transactional
    public Object test() {
        UUID categoryId = UUID.fromString("36bcec62-f2d0-416b-8618-bf1f24db5fdd");
        var c = categoryBlazeRepository.getReferenceById(categoryId);
        c = categoryBlazeRepository.findById(categoryId).orElse(null);
        c = testService.see(categoryId);
        return c.getName();
    }
}

Logs

2025-10-08T09:59:06.742+01:00 DEBUG 18760 --- [spring-demo] [omcat-handler-0] org.hibernate.SQL                        : select c1_0.id,c1_0.created_at,c1_0.created_by,c1_0.image,c1_0.name,c1_0.sort_order,c1_0.parent_id,c1_0.published_at,c1_0.revision,c1_0.slug,c1_0.updated_at,c1_0.updated_by from spring.category c1_0 where c1_0.id=?
2025-10-08T09:59:06.791+01:00 DEBUG 18760 --- [spring-demo] [omcat-handler-0] org.hibernate.SQL                        : select c1_0.id,c1_0.created_at,c1_0.created_by,c1_0.image,c1_0.name,c1_0.sort_order,c1_0.parent_id,c1_0.published_at,c1_0.revision,c1_0.slug,c1_0.updated_at,c1_0.updated_by from spring.category c1_0 where c1_0.id=?

I'm not sure if this behavior is intentional or a bug. If it's intentional it should be stated because its a breaking change to the default SimpleJpaRepository behavior which mean in this case replacing @EnableJpaRepositories with @EnableBlazeRepositories is not a drop-in replacement

Psycomentis06 avatar Oct 08 '25 09:10 Psycomentis06

Is Category an entity or an entity view?

jwgmeligmeyling avatar Oct 08 '25 09:10 jwgmeligmeyling

Category is an entity

Psycomentis06 avatar Oct 09 '25 09:10 Psycomentis06

Thanks for the report. I think it should be somewhat easy to do this by special casing in com.blazebit.persistence.spring.data.impl.repository.EntityViewAwareRepositoryImpl.findById like:

Class<V> entityViewClass = metadata == null
            || metadata.getEntityViewClass() == null ? this.entityViewClass : (Class<V>) metadata.getEntityViewClass();
if (entityViewClass == null) {
    // todo: determine lockMode and properties based on metadata
    return getEntityManager().find(getDomainClass(), id, lockMode, properties);
}

beikov avatar Oct 20 '25 16:10 beikov