Skip to content

Commit

Permalink
Feature/small api improvements (#51)
Browse files Browse the repository at this point in the history
* support paths directly in `Filter`

* Glitch in generics of PageRequest

* support totalCount calculation for a request (on demand)

The total is passed via the request(s)/cursor back to the client, so it should be calculated only once and not for each request again.
  • Loading branch information
p3t authored Oct 10, 2024
1 parent b264755 commit 9b8da9d
Show file tree
Hide file tree
Showing 11 changed files with 154 additions and 20 deletions.
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ public void queryData() {
}
```

### There is no total count in the Page...
### There is no total count in the Page - by default

Executing a count operation can be a quite expensive operation! Therefore, the total count of records is not part of the page response. It is also usually not required to re-count all records with each page request!
So, if you need to know the total count of records, you can execute a count query on the repository:
Expand All @@ -249,6 +249,27 @@ public long queryCount( PageRequest<DataRecord> request ) {

The request (if available) should be passed as it might contain filters which reduce returned count.

It is also possible to instruct the repository to do the count for the page request. Still, the total-count will be stored within the returned self & next-page requests, and will not be recalculated for the subsequent requests!

```java
public void queryData() {
final PageRequest<DataRecord> request = PageRequest.create(
b -> b.pageSize( 5 ).withTotalCount( true ).asc( DataRecord_.id ) );

final var page = dataRecordRepository.loadPage( request );
log( "Total records: " + page.totalCount() );

// insert some data...

final var page2 = dataRecordRepository.loadPage( page.next().orElseThrow() );
log( "Total records: " + page2.totalCount() == page.totalCount() ); // will output true

// force recalculating the total count
final var page3 = dataRecordRepository.loadPage( secondPage.next().orElseThrow().withEnableTotalCount( true ) );
log( "Total records: " + page3.totalCount() ); // new total count
}
```

### Example: Extend filter with custom rules

In some cases you might want to filter the results by defining your own custom rules. One example where this could be useful is, when you need to implement sophisticated access rights (ACLs).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public class DtoPageRequest {
@Max( 100 )
private int pageSize = 100; // Must not be final!

private boolean withTotalCount;

public DtoPageRequest withPageSize( final int pageSize ) {
this.pageSize = pageSize;
return this;
Expand Down Expand Up @@ -54,7 +56,7 @@ public <T> PageRequest<T> toPageRequest( final Function<String, Attribute> attri
final Attribute attribute = attributeProvider.apply( name );
b.filter( Filter.create( fb -> fb.attribute( attribute ).values( values ) ) );
} );
b.pageSize( pageSize );
b.pageSize( pageSize ).enableTotalCount( withTotalCount );
} );
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ public static <T> FromDtoMapper<T> create( final Consumer<FromDtoMapperBuilder<

public PageRequest<E> map() {
return PageRequest.<E>builder().positions( positions() ).filters( filters() ).pageSize( request.getPageSize() )
.enableTotalCount( request.hasTotalCount() )
.totalCount( request.hasTotalCount() ? request.getTotalCount() : null )
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@ class ToDtoMapper<E> {
private final PageRequest<E> pageRequest;

public Cursor.PageRequest map() {
return Cursor.PageRequest.newBuilder().addAllPositions( positions() ).setPageSize( pageRequest.pageSize() )
.addAllFilters( filters() )
.build();
final var builder = Cursor.PageRequest.newBuilder()
.addAllPositions( positions() )
.setPageSize( pageRequest.pageSize() )
.addAllFilters( filters() );
pageRequest.totalCount().ifPresent( builder::setTotalCount );
return builder.build();
}


Expand Down
1 change: 1 addition & 0 deletions cursorpaging-jpa-api/src/main/proto/pagerequest.proto
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@ message PageRequest {
int32 page_size = 1;
repeated Position positions = 2;
repeated Filter filters = 3;
optional int64 total_count = 4;
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,18 @@ void shouldSerializeReversedPageRequests() {

}

@Test
void shouldSerializeTotalCountIfPresent() {
final var request = createPageRequest().copy( b -> b.enableTotalCount( true ).totalCount( 42L ) );
final EntitySerializer<TestEntity> serializer = EntitySerializer.create();
final var serializedRequest = serializer.toBase64( request );
final var deserializedRequest = serializer.toPageRequest( serializedRequest );
assertThat( deserializedRequest ).isEqualTo( request ).satisfies( r -> {
assertThat( r.totalCount() ).isPresent().get().isEqualTo( 42L );
assertThat( r.enableTotalCount() ).isTrue();
} );
}

@Test
void shouldLearnAttributesBySerializing() {
final var request = createPageRequest();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,18 @@ public FilterBuilder attribute( final SingularAttribute<?, ? extends Comparable<
return this;
}

/**
* Creates an attribute as path to an embedded entity's property.
*
* @param attributes the path to the property
* @return the builder
*/
@SafeVarargs
public final FilterBuilder path( final SingularAttribute<?, ? extends Comparable<?>>... attributes ) {
this.attribute = Attribute.path( attributes );
return this;
}

public FilterBuilder attribute( final Attribute attribute ) {
this.attribute = attribute;
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ public List<E> getContent() {
return Collections.unmodifiableList( content );
}

public Optional<Long> getTotalCount() {
return self.totalCount();
}

/**
* The request which can be used to fetching the next page.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import lombok.Builder;
import lombok.EqualsAndHashCode;
Expand All @@ -25,7 +26,7 @@
*
* @param <E> the entity type
*/
@Builder( toBuilder = true )
@Builder
@Getter
@Accessors( fluent = true )
@EqualsAndHashCode
Expand Down Expand Up @@ -64,6 +65,13 @@ public class PageRequest<E> {
@Builder.Default
private final int pageSize = DEFAULT_PAGE_SIZE;

/**
* Control if the total element count should be calculated if missing in the request
*/
private final boolean enableTotalCount;

private final Long totalCount;

/**
* Adding some short-cut builder methods to create a request
*
Expand All @@ -78,7 +86,7 @@ public static class PageRequestBuilder<E> {
* @return the builder
*/
public PageRequestBuilder<E> firstPage( final Order order,
final SingularAttribute<E, ? extends Comparable<?>> attribute ) {
final SingularAttribute<? super E, ? extends Comparable<?>> attribute ) {
return addPosition( Position.create( b -> b.attribute( Attribute.of( attribute ) ).order( order ) ) );
}

Expand All @@ -88,7 +96,7 @@ public PageRequestBuilder<E> firstPage( final Order order,
* @param attribute the attribute used to create a position (ascending ordered)
* @return the builder
*/
public PageRequestBuilder<E> asc( final SingularAttribute<E, ? extends Comparable<?>> attribute ) {
public PageRequestBuilder<E> asc( final SingularAttribute<? super E, ? extends Comparable<?>> attribute ) {
return firstPage( Order.ASC, attribute );
}

Expand All @@ -98,7 +106,7 @@ public PageRequestBuilder<E> asc( final SingularAttribute<E, ? extends Comparabl
* @param attribute the attribute used to create a position (descending ordered)
* @return the builder
*/
public PageRequestBuilder<E> desc( final SingularAttribute<E, ? extends Comparable<?>> attribute ) {
public PageRequestBuilder<E> desc( final SingularAttribute<? super E, ? extends Comparable<?>> attribute ) {
return firstPage( Order.DESC, attribute );
}

Expand Down Expand Up @@ -154,6 +162,7 @@ public PageRequestBuilder<E> filters( final Collection<Filter> filters ) {
return this;
}


private PageRequestBuilder<E> addPosition( final Position pos ) {
if ( this.positions == null ) {
this.positions = new ArrayList<>( 3 );
Expand All @@ -176,6 +185,49 @@ public static <E> PageRequest<E> create( final Consumer<PageRequestBuilder<E>> c
return builder.build();
}

/**
* Create a new page-request from the current one, but with the provided customizer applied
*
* @param c customizer for the copy
* @return A new page-request with existing and customized attributes
*/
public PageRequest<E> copy( Consumer<PageRequestBuilder<E>> c ) {
PageRequestBuilder<E> builder = PageRequest.<E>builder()
.totalCount( totalCount )
.enableTotalCount( enableTotalCount )
.pageSize( pageSize );
c.accept( builder );
if ( !builder.filters$set && !filters.isEmpty() ) {
builder.filters( filters );
}
if ( (builder.rules == null || builder.rules.isEmpty()) && !rules.isEmpty() ) {
builder.rules( rules );
}
if ( (builder.positions == null || builder.positions.isEmpty()) && !positions.isEmpty() ) {
builder.positions( positions );
}
return builder.build();
}

/**
* Enable the total count calculation for the request.<br> Setting {@code enable = true} forces also the
* re-calculation of the total count for a page-request where the total-count is already present.
*
* @return A copy of the page-request where the total-count is removed and the enable flag is set accordingly
*/
public PageRequest<E> withEnableTotalCount( boolean enable ) {
return copy( b -> b.enableTotalCount( enable ).totalCount( null ) );
}

/**
* Get the total count if present
*
* @return the total count if present
*/
public Optional<Long> totalCount() {
return Optional.ofNullable( totalCount );
}

/**
* Create a new {@linkplain PageRequest} pointing to the position defined through the attributes of the provided
* entity.
Expand All @@ -184,16 +236,14 @@ public static <E> PageRequest<E> create( final Consumer<PageRequestBuilder<E>> c
* @return A new PageRequest with the positions set to the values of the provided entity
*/
public PageRequest<E> positionOf( final E entity ) {
return create( b -> b.positions( positions.stream()
.map( p -> p.positionOf( entity ) )
.toList() ).pageSize( this.pageSize ) );
return create( b -> b.positions( positions.stream().map( p -> p.positionOf( entity ) ).toList() )
.pageSize( this.pageSize )
.totalCount( this.totalCount )
.enableTotalCount( this.enableTotalCount ) );
}

public PageRequest<E> toReversed() {
return PageRequest.<E>builder()
.rules( rules ).filters( filters ).positions( positions.stream().map( Position::toReversed ).toList() )
.pageSize( pageSize )
.build();
return copy( b -> b.positions( positions.stream().map( Position::toReversed ).toList() ) );
}

public boolean isFirstPage() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,12 @@ public Page<E> loadPage( final PageRequest<E> request ) {
.setMaxResults( getMaxResultSize( request ) )
.getResultList();

return Page.create( b -> b.content( toContent( results, request ) )
.self( request )
.next( toNextRequest( results, request ) ) );
final PageRequest<E> self = request.enableTotalCount() && request.totalCount().isEmpty() ? request.copy(
b -> b.totalCount( count( request ) ) ) : request;

return Page.create( b -> b.content( toContent( results, self ) ) //
.self( self ) //
.next( toNextRequest( results, self ) ) );
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,30 @@ void shouldCountWhereFilterWithLikeExpressions() {
assertThat( count ).isEqualTo( 2 );
}

@Test
void shouldAddCountInPageRequestWhenRequested() {
// Given
testDataGenerator.generateData( TestDataGenerator.NAMES.length );
final PageRequest<DataRecord> request = PageRequest.create(
b -> b.pageSize( 5 ).enableTotalCount( true ).asc( DataRecord_.id ) );

// When
final var page = dataRecordRepository.loadPage( request );

assertThat( page ).isNotNull();
assertThat( page.getTotalCount() ).isPresent().get().isEqualTo( (long) TestDataGenerator.NAMES.length );

testDataGenerator.generateDataRecords( 66 );
final var secondPage = dataRecordRepository.loadPage( page.next().orElseThrow() );

// THEN the totalCount should not be re-calculated
assertThat( secondPage.getTotalCount() ).isPresent().get().isEqualTo( (long) TestDataGenerator.NAMES.length );

final var thirdPage = dataRecordRepository.loadPage(
secondPage.next().orElseThrow().withEnableTotalCount( true ) );
assertThat( thirdPage.getTotalCount() ).isPresent().get().isEqualTo( TestDataGenerator.NAMES.length + 66L );
}

@Test
void shouldReturnTotalCountWhenNoFilterPresent() {
testDataGenerator.generateData( 42 );
Expand All @@ -290,7 +314,7 @@ void shouldReturnTotalCountWhenNoFilterPresent() {

final var count = dataRecordRepository.count( request );

assertThat( count ).isEqualTo( 42 );
assertThat( count ).isEqualTo( 42L );
}

@Test
Expand Down

0 comments on commit 9b8da9d

Please sign in to comment.