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

Spring Data JPA OR composed Specification keyset pagination not paging correctly

Open adityamparikh opened this issue 2 years ago • 12 comments

BlazePaginationRepository:

 public PagedList<T> findTopN(Specification<T> specification,
                                 Sort sortBy,
                                 int pageSize,
                                 Class<T> focalEntityClass) {
        return sortedCriteriaBuilder(specification, sortBy, focalEntityClass)
                .page(0, pageSize)
                .withKeysetExtraction(true)
                .withExtractAllKeysets(true)
                .getResultList();
    }
public PagedList<T> findNextN(Specification<T> specification,
                                  Sort sortBy,
                                  PagedList<T> previousPage,
                                  Class<T> focalEntityClass) {
        return sortedCriteriaBuilder(specification, sortBy, focalEntityClass)
                .page(
                        previousPage.getKeysetPage(),
                        previousPage.getPage() * previousPage.getMaxResults(),
                        previousPage.getMaxResults()
                )
                .withKeysetExtraction(true)
                .withExtractAllKeysets(true)
                .getResultList();
    }
private CriteriaBuilder<T> sortedCriteriaBuilder(Specification<T> specification, Sort sortBy, Class<T> focalEntityClass) {
        BlazeCriteriaBuilder cb = BlazeCriteria.get(cbf);
        BlazeCriteriaQuery<T> query = cb.createQuery(focalEntityClass);

        Root<T> root = query.from(focalEntityClass);

        if(null != specification){
            query.where(specification.toPredicate(root, query, cb));
        }

        CriteriaBuilder<T> builder = query.createCriteriaBuilder(entityManager);

        sortBy.forEach(order -> {
            builder.orderBy(
                    order.getProperty(),
                    order.isAscending()
            );
        });
        return builder;
    }
@Repository
public interface EntityRepository extends JpaRepository<FocalEntity, FocalEntityID>,
        JpaSpecificationExecutor<FocalEntity>,
        BlazePaginationRepository<FocalEntity> {

Expected behavior

expected: <2>

Actual behavior

but was: <3>

Steps to reproduce

    @BeforeEach
    void insert(){
        entityRepository.save(getFocalEntity("AAPL"));
        entityRepository.save(getFocalEntity("SNAP"));
        entityRepository.save(getFocalEntity("TWTR"));

        entityRepository.save(getFocalEntity("QQQ"));
        entityRepository.save(getFocalEntity("TSLA"));
        entityRepository.save(getFocalEntity("AMZN"));

        entityRepository.save(getFocalEntity("GOOGL"));
        entityRepository.save(getFocalEntity("ADBE"));
        entityRepository.save(getFocalEntity("META"));

        entityRepository.save(getFocalEntity("TMUS"));
        entityRepository.save(getFocalEntity("SPY"));
        entityRepository.save(getFocalEntity("AMD"));

        entityRepository.save(getFocalEntity("AMEX"));
    }
    
    @Test
    void testSpecificationWithBlazeOr(){
        Sort sort = Sort.by(FocalEntity_.HEADER_START_DATETIME).ascending()
                .and(Sort.by(FocalEntity_.BUSINESS_KEY).ascending()
                        .and(Sort.by(FocalEntity_.PRIME_RECORD_IDENTIFIER)).ascending()
                        .and(Sort.by(FocalEntity_.VERSION).ascending()));

        Specification<FocalEntity> nameBeginsWithAOrT = nameBeginsWith("A").or(nameBeginsWith("T"));

        PagedList<FocalEntity> firstPage = entityRepository.findTopN(nameBeginsWithAOrT, sort, 3, FocalEntity.class);
        Assertions.assertEquals(3, firstPage.size());

        PagedList<FocalEntity> secondPage = entityRepository.findNextN(nameBeginsWithAOrT, sort, firstPage,FocalEntity.class);
        Assertions.assertEquals(3, secondPage.size());
        Assertions.assertEquals(3, secondPage.getTotalPages());
        Assertions.assertEquals(8, secondPage.getTotalSize());

        //blaze does not iterate over OR correctly
        PagedList<FocalEntity> third = entityRepository.findNextN(nameBeginsWithAOrT, sort, secondPage, FocalEntity.class);
        Assertions.assertEquals(2, third.size()); <---- fails

    }

Environment

Version: Using Spring Boot 2.7.x, Following https://vladmihalcea.com/keyset-pagination-spring/ JPA-Provider: Hibernate DBMS: H2 Application Server: web(tomcat)

adityamparikh avatar Feb 13 '23 19:02 adityamparikh

Hi there, can you please post the queries that are generated?

beikov avatar Feb 14 '23 08:02 beikov

Find TopN:

Hibernate: 
    select
        focalentit0_.prime_record_identifier as col_0_0_,
        focalentit0_.version as col_0_1_,
        focalentit0_.header_start_datetime as col_1_0_,
        focalentit0_.business_key as col_2_0_,
        focalentit0_.prime_record_identifier as col_3_0_,
        focalentit0_.version as col_4_0_,
        (select
            count(*) 
        from
            focal_entity focalentit1_ 
        where
            focalentit1_.business_key like replace(?,'\\','\\\\') 
            or focalentit1_.business_key like replace(?,'\\','\\\\')) as col_5_0_,
        focalentit0_.prime_record_identifier as prime_re1_0_,
        focalentit0_.version as version2_0_,
        focalentit0_.ask_price_account_id as ask_pric3_0_,
        focalentit0_.ask_price_account_name as ask_pric4_0_,
        focalentit0_.ask_price_as_of_date as ask_pric5_0_,
        focalentit0_.ask_price_cents as ask_pric6_0_,
        focalentit0_.ask_price_currency as ask_pric7_0_,
        focalentit0_.ask_price_value as ask_pric8_0_,
        focalentit0_.bid_price_account_id as bid_pric9_0_,
        focalentit0_.bid_price_account_name as bid_pri10_0_,
        focalentit0_.bid_price_as_of_date as bid_pri11_0_,
        focalentit0_.bid_price_cents as bid_pri12_0_,
        focalentit0_.bid_price_currency as bid_pri13_0_,
        focalentit0_.bid_price_value as bid_pri14_0_,
        focalentit0_.business_key as busines15_0_,
        focalentit0_.cash_instrument_id as cash_in16_0_,
        focalentit0_.cash_principal_qty as cash_pr17_0_,
        focalentit0_.event_impact_business_id as event_i18_0_,
        focalentit0_.event_impact_business_process_type as event_i19_0_,
        focalentit0_.event_impact_business_type as event_i20_0_,
        focalentit0_.event_impact_sys_event_type as event_i21_0_,
        focalentit0_.header_end_datetime as header_22_0_,
        focalentit0_.header_start_datetime as header_23_0_,
        focalentit0_.processing_is_ticket_released as process24_0_,
        focalentit0_.processing_is_trade_blotter_reporting_skipped as process25_0_,
        focalentit0_.processing_is_trade_feed_received as process26_0_,
        focalentit0_.schema_major_version as schema_27_0_,
        focalentit0_.schema_minor_version as schema_28_0_,
        focalentit0_.schema_patch_version as schema_29_0_,
        focalentit0_.ticket_datetime as ticket_30_0_,
        focalentit0_.ticket_num_id as ticket_31_0_,
        focalentit0_.ticket_type as ticket_32_0_ 
    from
        focal_entity focalentit0_ 
    where
        focalentit0_.business_key like replace(?,'\\','\\\\') 
        or focalentit0_.business_key like replace(?,'\\','\\\\') 
    order by
        focalentit0_.header_start_datetime ASC,
        focalentit0_.business_key ASC,
        focalentit0_.prime_record_identifier ASC,
        focalentit0_.version ASC limit ?

Find Next N:

    select
        focalentit0_.prime_record_identifier as col_0_0_,
        focalentit0_.version as col_0_1_,
        focalentit0_.header_start_datetime as col_1_0_,
        focalentit0_.business_key as col_2_0_,
        focalentit0_.prime_record_identifier as col_3_0_,
        focalentit0_.version as col_4_0_,
        (select
            count(*) 
        from
            focal_entity focalentit1_ 
        where
            focalentit1_.business_key like replace(?,'\\','\\\\') 
            or focalentit1_.business_key like replace(?,'\\','\\\\')) as col_5_0_,
        focalentit0_.prime_record_identifier as prime_re1_0_,
        focalentit0_.version as version2_0_,
        focalentit0_.ask_price_account_id as ask_pric3_0_,
        focalentit0_.ask_price_account_name as ask_pric4_0_,
        focalentit0_.ask_price_as_of_date as ask_pric5_0_,
        focalentit0_.ask_price_cents as ask_pric6_0_,
        focalentit0_.ask_price_currency as ask_pric7_0_,
        focalentit0_.ask_price_value as ask_pric8_0_,
        focalentit0_.bid_price_account_id as bid_pric9_0_,
        focalentit0_.bid_price_account_name as bid_pri10_0_,
        focalentit0_.bid_price_as_of_date as bid_pri11_0_,
        focalentit0_.bid_price_cents as bid_pri12_0_,
        focalentit0_.bid_price_currency as bid_pri13_0_,
        focalentit0_.bid_price_value as bid_pri14_0_,
        focalentit0_.business_key as busines15_0_,
        focalentit0_.cash_instrument_id as cash_in16_0_,
        focalentit0_.cash_principal_qty as cash_pr17_0_,
        focalentit0_.event_impact_business_id as event_i18_0_,
        focalentit0_.event_impact_business_process_type as event_i19_0_,
        focalentit0_.event_impact_business_type as event_i20_0_,
        focalentit0_.event_impact_sys_event_type as event_i21_0_,
        focalentit0_.header_end_datetime as header_22_0_,
        focalentit0_.header_start_datetime as header_23_0_,
        focalentit0_.processing_is_ticket_released as process24_0_,
        focalentit0_.processing_is_trade_blotter_reporting_skipped as process25_0_,
        focalentit0_.processing_is_trade_feed_received as process26_0_,
        focalentit0_.schema_major_version as schema_27_0_,
        focalentit0_.schema_minor_version as schema_28_0_,
        focalentit0_.schema_patch_version as schema_29_0_,
        focalentit0_.ticket_datetime as ticket_30_0_,
        focalentit0_.ticket_num_id as ticket_31_0_,
        focalentit0_.ticket_type as ticket_32_0_ 
    from
        focal_entity focalentit0_ 
    where
        (
            ?, ?, ?, ?
        ) < (
            focalentit0_.header_start_datetime, focalentit0_.business_key, focalentit0_.prime_record_identifier, focalentit0_.version
        ) 
        and 0=0 
        and (
            focalentit0_.business_key like replace(?,'\\','\\\\')
        ) 
        or focalentit0_.business_key like replace(?,'\\','\\\\') 
    order by
        focalentit0_.header_start_datetime ASC,
        focalentit0_.business_key ASC,
        focalentit0_.prime_record_identifier ASC,
        focalentit0_.version ASC limit ?

Causes this sort of sliding window where just one record from previous page is thrown away while other two are retained (page size was 3) even though total size and total pages values are correct.

adityamparikh avatar Feb 14 '23 14:02 adityamparikh

I think you might be running into something related to https://github.com/Blazebit/blaze-persistence/issues/1638 which was fixed via https://github.com/Blazebit/blaze-persistence/commit/087aa00323018b7940db05197c3a2cc2cd3634e7.

Can you try the 1.6.9-SNAPSHOT please?

beikov avatar Feb 14 '23 14:02 beikov

Are you able to publish the snapshot artifact? Thanks

adityamparikh avatar Feb 14 '23 15:02 adityamparikh

It's there in the snapshots repository: https://oss.sonatype.org/content/repositories/snapshots/

beikov avatar Feb 14 '23 15:02 beikov

Is it different than building from source? I tried that and got the same issue.

adityamparikh avatar Feb 14 '23 21:02 adityamparikh

@beikov We aren't able to view the contents of the URL (Directory listing forbidden) and can't easily get snapshot dependencies hence I went building from source route.

adityamparikh avatar Feb 17 '23 16:02 adityamparikh

It's the same as building from source. Including the snapshots repository is described here: https://maven.apache.org/guides/mini/guide-multiple-repositories.html

Do you think you can share a reproducer with me so I can take a deeper look? You can use the Spring Boot Archetype if that makes it easier for you:

mvn archetype:generate "-DarchetypeGroupId=com.blazebit" "-DarchetypeArtifactId=blaze-persistence-archetype-spring-boot-sample" "-DarchetypeVersion=1.6.8"

beikov avatar Feb 20 '23 08:02 beikov

@beikov Used the archetype and created the project. Updated Spring Boot and Java versions. https://github.com/adityaparikh91087/specification/blob/main/src/test/java/com/blazebit/springdatajpa/sample/SpecificationTest.java#L67 Here is the failing test line.

adityamparikh avatar Feb 20 '23 18:02 adityamparikh

Thanks for the test, I'll try to dig into this as soon as possible.

beikov avatar Feb 21 '23 07:02 beikov

@beikov Any insight? Awaiting release of 1.6.9

adityamparikh avatar Apr 26 '23 14:04 adityamparikh

Sorry, not yet. The Hibernate 6.2 integration for Quarkus 3 and Spring Boot 3 has priority right now.

beikov avatar Apr 26 '23 14:04 beikov