From 7bcfeb5b0298d4e020a6042c4c418a2656f6690f Mon Sep 17 00:00:00 2001 From: Arnaud THOREL <146101071+arnaud-thorel-of@users.noreply.github.com> Date: Wed, 7 Feb 2024 11:07:40 +0100 Subject: [PATCH] 11 mass update (#20) * feat(mass-update): allow mass update --- README.md | 52 ++++++++++-- pom.xml | 2 +- .../postgrest/ParametrizedTypeUtils.java | 26 ++++++ .../postgrest/PostgrestRestTemplate.java | 45 +++++------ .../querydsl/postgrest/ResponseUtils.java | 46 +++++++++++ .../PostgrestRestTemplateRepositoryTest.java | 36 +++++++++ .../postgrest/ParametrizedTypeUtils.java | 26 ++++++ .../postgrest/PostgrestWebClient.java | 56 ++++++------- .../querydsl/postgrest/ResponseUtils.java | 46 +++++++++++ ... => PostgrestWebClientRepositoryTest.java} | 81 ++++++++++++++----- .../querydsl/postgrest/PostgrestClient.java | 7 +- .../postgrest/PostgrestRepository.java | 53 ++++++++---- .../querydsl/postgrest/Repository.java | 45 ++++++++++- .../querydsl/postgrest/model/BulkOptions.java | 24 ++++++ .../querydsl/postgrest/model/BulkRequest.java | 22 +++++ .../postgrest/model/BulkResponse.java | 55 +++++++++++++ .../querydsl/postgrest/model/Range.java | 9 +++ .../postgrest/model/impl/CountFilter.java | 6 ++ .../services/BulkExecutorService.java | 53 ++++++++++++ .../PostgrestRepositoryCountMockTest.java | 4 +- .../PostgrestRepositoryDeleteMockTest.java | 3 +- .../PostgrestRepositoryGetMockTest.java | 1 + .../PostgrestRepositoryPatchMockTest.java | 35 +++++++- .../PostgrestRepositoryUpsertMockTest.java | 39 +++++++-- .../querydsl/postgrest/model/RangeTest.java | 3 + .../services/BulkExecutorServiceTest.java | 65 +++++++++++++++ 26 files changed, 722 insertions(+), 118 deletions(-) create mode 100644 querydsl-postgrest-resttemplate-adapter/src/main/java/fr/ouestfrance/querydsl/postgrest/ParametrizedTypeUtils.java create mode 100644 querydsl-postgrest-resttemplate-adapter/src/main/java/fr/ouestfrance/querydsl/postgrest/ResponseUtils.java create mode 100644 querydsl-postgrest-webclient-adapter/src/main/java/fr/ouestfrance/querydsl/postgrest/ParametrizedTypeUtils.java create mode 100644 querydsl-postgrest-webclient-adapter/src/main/java/fr/ouestfrance/querydsl/postgrest/ResponseUtils.java rename querydsl-postgrest-webclient-adapter/src/test/java/fr/ouestfrance/querydsl/postgrest/{PostrgrestWebClientRepositoryTest.java => PostgrestWebClientRepositoryTest.java} (65%) create mode 100644 querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/model/BulkOptions.java create mode 100644 querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/model/BulkRequest.java create mode 100644 querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/model/BulkResponse.java create mode 100644 querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/services/BulkExecutorService.java create mode 100644 querydsl-postgrest/src/test/java/fr/ouestfrance/querydsl/postgrest/services/BulkExecutorServiceTest.java diff --git a/README.md b/README.md index 3cad942..4d3a83e 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ and provides class and annotation to improve your developer experience using Pos Add the following dependency to your Maven project: ```xml + fr.ouestfrance.querydsl querydsl-postgrest @@ -52,8 +53,10 @@ cookies, ...) you need to deploy. #### WebClient configuration example -Add the dependency : +Add the dependency : + ```xml + fr.ouestfrance.querydsl querydsl-postgrest-webclient-adapter @@ -87,8 +90,10 @@ public class PostgrestConfiguration { #### RestTemplate configuration example -Add the dependency : +Add the dependency : + ```xml + fr.ouestfrance.querydsl querydsl-postgrest-resttemplate-adapter @@ -198,10 +203,6 @@ You can then create your functions : - findUsersByName : Will return list of users which name contains part of search content ```java -import fr.ouestfrance.querydsl.FilterField; -import fr.ouestfrance.querydsl.FilterOperation; -import lombok.AllArgsConstructor; -import lombok.Getter; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -349,6 +350,45 @@ extends FilterOperation with | CS | Contains for JSON/Range datatype | | CD | Contained for JSON/Range datatype | +#### Bulk Operations + +PostgREST allow to execute operations over a wide range items. +QueryDSL-Postgrest allow to handle pagination fixed by user or fixed by the postgREST max page + +```java +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class UserService { + + private final UserRepository userRepository; + + + public void invalidatePassword() { + UserSearch criteria = new UserSearch(); + // Will invalidate all passwords with chunk of 1000 users + userRepository.patch(criteria, new UserPatchPassword(false), BulkOptions.builder() + .countsOnly(true) + .pageSize(1000) + .build()); + // Generate n calls of + // PATCH /users {"password_validation": false } -H Range 0-999 + // PATCH /users {"password_validation": false } -H Range 1000-1999 + // PATCH /users {"password_validation": false } -H Range 2000-2999 + // etc since the users are all updated + } +} +``` + +| Option | Default Value | Description | +|------------|---------------|---------------------------------------------------------------------------| +| countsOnly | false | Place return=headers-only if true, otherwise keep default return | +| pageSize | -1 | Specify the size of the chunk, otherwise let postgrest activate its limit | + +> Bulk Operations are allowed on `Patch`, `Delete` and `Upsert` + ## Need Help ? If you need help with the library please start a new thread QA / Issue on github diff --git a/pom.xml b/pom.xml index db31d0b..e1d72cb 100644 --- a/pom.xml +++ b/pom.xml @@ -149,7 +149,7 @@ sign-artifacts - verify + deploy sign diff --git a/querydsl-postgrest-resttemplate-adapter/src/main/java/fr/ouestfrance/querydsl/postgrest/ParametrizedTypeUtils.java b/querydsl-postgrest-resttemplate-adapter/src/main/java/fr/ouestfrance/querydsl/postgrest/ParametrizedTypeUtils.java new file mode 100644 index 0000000..5eb5d4b --- /dev/null +++ b/querydsl-postgrest-resttemplate-adapter/src/main/java/fr/ouestfrance/querydsl/postgrest/ParametrizedTypeUtils.java @@ -0,0 +1,26 @@ +package fr.ouestfrance.querydsl.postgrest; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.reflect.TypeUtils; +import org.springframework.core.ParameterizedTypeReference; + +import java.util.List; + +/** + * Type Utilities + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class ParametrizedTypeUtils { + + /** + * Create parametrized type of List of T + * + * @param clazz class of T + * @param type of parametrized list + * @return parametrized type + */ + public static ParameterizedTypeReference> listRef(Class clazz) { + return ParameterizedTypeReference.forType(TypeUtils.parameterize(List.class, clazz)); + } +} diff --git a/querydsl-postgrest-resttemplate-adapter/src/main/java/fr/ouestfrance/querydsl/postgrest/PostgrestRestTemplate.java b/querydsl-postgrest-resttemplate-adapter/src/main/java/fr/ouestfrance/querydsl/postgrest/PostgrestRestTemplate.java index 58e58c0..23273b0 100644 --- a/querydsl-postgrest-resttemplate-adapter/src/main/java/fr/ouestfrance/querydsl/postgrest/PostgrestRestTemplate.java +++ b/querydsl-postgrest-resttemplate-adapter/src/main/java/fr/ouestfrance/querydsl/postgrest/PostgrestRestTemplate.java @@ -1,8 +1,7 @@ package fr.ouestfrance.querydsl.postgrest; +import fr.ouestfrance.querydsl.postgrest.model.BulkResponse; import fr.ouestfrance.querydsl.postgrest.model.CountItem; -import fr.ouestfrance.querydsl.postgrest.model.Page; -import fr.ouestfrance.querydsl.postgrest.model.PageImpl; import fr.ouestfrance.querydsl.postgrest.model.Range; import fr.ouestfrance.querydsl.postgrest.model.RangeResponse; import lombok.AccessLevel; @@ -22,7 +21,9 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.stream.Stream; + +import static fr.ouestfrance.querydsl.postgrest.ParametrizedTypeUtils.listRef; +import static fr.ouestfrance.querydsl.postgrest.ResponseUtils.toBulkResponse; /** * Rest interface for querying postgrest @@ -56,36 +57,32 @@ public RangeResponse search(String resource, Map> pa return Optional.of(response) .map(HttpEntity::getBody) .map(x -> { - Range range = Optional.ofNullable(response.getHeaders().get("Content-Range")) - .map(List::stream) - .map(Stream::findFirst) - .filter(Optional::isPresent) - .map(Optional::get) - .map(Range::of).orElse(null); + Range range = ResponseUtils.getCount(response.getHeaders()) + .orElse(null); return new RangeResponse<>(x, range); }).orElse(new RangeResponse<>(List.of(), null)); } @Override - public List post(String resource, List value, Map> headers, Class clazz) { - HttpHeaders httpHeaders = toHeaders(headers); - return restTemplate.exchange(resource, HttpMethod.POST, new HttpEntity<>(value, httpHeaders), listRef(clazz)).getBody(); + public BulkResponse post(String resource, List value, Map> headers, Class clazz) { + ResponseEntity> response = restTemplate.exchange(resource, HttpMethod.POST, new HttpEntity<>(value, toHeaders(headers)), listRef(clazz)); + return toBulkResponse(response); } + @Override - public List patch(String resource, Map> params, Object value, Map> headers, Class clazz) { - MultiValueMap queryParams = toMultiMap(params); - return restTemplate.exchange(restTemplate.getUriTemplateHandler() - .expand(UriComponentsBuilder.fromPath(resource).queryParams(queryParams).build().toString(), new HashMap<>()), - HttpMethod.PATCH, new HttpEntity<>(value, toHeaders(headers)), listRef(clazz)) - .getBody(); + public BulkResponse patch(String resource, Map> params, Object value, Map> headers, Class clazz) { + ResponseEntity> response = restTemplate.exchange(restTemplate.getUriTemplateHandler() + .expand(UriComponentsBuilder.fromPath(resource).queryParams(toMultiMap(params)).build().toString(), new HashMap<>()), + HttpMethod.PATCH, new HttpEntity<>(value, toHeaders(headers)), listRef(clazz)); + return toBulkResponse(response); } @Override - public List delete(String resource, Map> params, Map> headers, Class clazz) { - MultiValueMap queryParams = toMultiMap(params); - return restTemplate.exchange(restTemplate.getUriTemplateHandler().expand(UriComponentsBuilder.fromPath(resource) - .queryParams(queryParams).build().toString(), new HashMap<>()), HttpMethod.DELETE, new HttpEntity<>(null, toHeaders(headers)), listRef(clazz)).getBody(); + public BulkResponse delete(String resource, Map> params, Map> headers, Class clazz) { + ResponseEntity> response = restTemplate.exchange(restTemplate.getUriTemplateHandler().expand(UriComponentsBuilder.fromPath(resource) + .queryParams(toMultiMap(params)).build().toString(), new HashMap<>()), HttpMethod.DELETE, new HttpEntity<>(null, toHeaders(headers)), listRef(clazz)); + return toBulkResponse(response); } @Override @@ -94,9 +91,6 @@ public List count(String resource, Map> map) { .queryParams(toMultiMap(map)).build().toString(), new HashMap<>()), HttpMethod.GET, new HttpEntity<>(null, new HttpHeaders()), listRef(CountItem.class)).getBody(); } - private static ParameterizedTypeReference> listRef(Class clazz) { - return ParameterizedTypeReference.forType(TypeUtils.parameterize(List.class, clazz)); - } private static MultiValueMap toMultiMap(Map> params) { return new LinkedMultiValueMap<>(params); @@ -105,4 +99,5 @@ private static MultiValueMap toMultiMap(Map private static HttpHeaders toHeaders(Map> headers) { return new HttpHeaders(toMultiMap(headers)); } + } diff --git a/querydsl-postgrest-resttemplate-adapter/src/main/java/fr/ouestfrance/querydsl/postgrest/ResponseUtils.java b/querydsl-postgrest-resttemplate-adapter/src/main/java/fr/ouestfrance/querydsl/postgrest/ResponseUtils.java new file mode 100644 index 0000000..e941af1 --- /dev/null +++ b/querydsl-postgrest-resttemplate-adapter/src/main/java/fr/ouestfrance/querydsl/postgrest/ResponseUtils.java @@ -0,0 +1,46 @@ +package fr.ouestfrance.querydsl.postgrest; + +import fr.ouestfrance.querydsl.postgrest.model.BulkResponse; +import fr.ouestfrance.querydsl.postgrest.model.Range; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; + +import java.util.List; +import java.util.Optional; + +/** + * Utility class that helps to transform response + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class ResponseUtils { + + /** + * Transform a ResponseEntity of list to bulkResponse + * + * @param response response entity + * @param Type of response + * @return BulkResponse + */ + public static BulkResponse toBulkResponse(ResponseEntity> response) { + return Optional.ofNullable(response) + .map(x -> { + Optional count = getCount(x.getHeaders()); + return new BulkResponse<>(x.getBody(), count.map(Range::getCount).orElse(0L), count.map(Range::getTotalElements).orElse(0L)); + }) + .orElse(new BulkResponse<>(List.of(), 0L, 0L)); + } + + /** + * Extract Range headers + * + * @param headers headers where Content-Range is + * @return range object + */ + public static Optional getCount(HttpHeaders headers) { + return Optional.ofNullable(headers.get("Content-Range")) + .flatMap(x -> x.stream().findFirst()) + .map(Range::of); + } +} diff --git a/querydsl-postgrest-resttemplate-adapter/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestRestTemplateRepositoryTest.java b/querydsl-postgrest-resttemplate-adapter/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestRestTemplateRepositoryTest.java index 021e049..05265d9 100644 --- a/querydsl-postgrest-resttemplate-adapter/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestRestTemplateRepositoryTest.java +++ b/querydsl-postgrest-resttemplate-adapter/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestRestTemplateRepositoryTest.java @@ -4,6 +4,7 @@ import fr.ouestfrance.querydsl.postgrest.app.PostRepository; import fr.ouestfrance.querydsl.postgrest.app.PostRequest; import fr.ouestfrance.querydsl.postgrest.criterias.Criteria; +import fr.ouestfrance.querydsl.postgrest.model.BulkResponse; import fr.ouestfrance.querydsl.postgrest.model.Page; import fr.ouestfrance.querydsl.postgrest.model.Pageable; import lombok.SneakyThrows; @@ -111,6 +112,16 @@ void shouldUpsertPost(MockServerClient client) { } + @Test + void shouldUpsertBulkPost(MockServerClient client) { + client.when(HttpRequest.request().withPath("/posts")) + .respond(HttpResponse.response().withHeader("Content-Range", "0-299/300")); + BulkResponse result = repository.upsert(new ArrayList<>(List.of(new Post()))); + assertNotNull(result); + assertEquals(300L, result.getAffectedRows()); + assertTrue(result.isEmpty()); + } + @Test void shouldPatchPost(MockServerClient client) { @@ -123,6 +134,19 @@ void shouldPatchPost(MockServerClient client) { result.stream().map(Object::getClass).forEach(x -> assertEquals(Post.class, x)); } + @Test + void shouldPatchBulkPost(MockServerClient client) { + client.when(HttpRequest.request().withPath("/posts").withQueryStringParameter("userId", "eq.25")) + .respond(HttpResponse.response().withHeader("Content-Range", "0-299/300")); + PostRequest criteria = new PostRequest(); + criteria.setUserId(25); + BulkResponse result = repository.patch(criteria, new Post()); + assertNotNull(result); + assertEquals(300L, result.getAffectedRows()); + assertTrue(result.isEmpty()); + } + + @Test void shouldDeletePost(MockServerClient client) { client.when(HttpRequest.request().withPath("/posts").withQueryStringParameter("userId", "eq.25")) @@ -134,6 +158,18 @@ void shouldDeletePost(MockServerClient client) { result.stream().map(Object::getClass).forEach(x -> assertEquals(Post.class, x)); } + @Test + void shouldDeleteBulkPost(MockServerClient client) { + client.when(HttpRequest.request().withPath("/posts").withQueryStringParameter("userId", "eq.25")) + .respond(HttpResponse.response().withHeader("Content-Range", "0-299/300")); + PostRequest criteria = new PostRequest(); + criteria.setUserId(25); + BulkResponse result = repository.delete(criteria); + assertNotNull(result); + assertEquals(300L, result.getAffectedRows()); + assertTrue(result.isEmpty()); + } + private HttpResponse jsonFileResponse(String resourceFileName) { return HttpResponse.response().withContentType(MediaType.APPLICATION_JSON) .withBody(jsonOf(resourceFileName)); diff --git a/querydsl-postgrest-webclient-adapter/src/main/java/fr/ouestfrance/querydsl/postgrest/ParametrizedTypeUtils.java b/querydsl-postgrest-webclient-adapter/src/main/java/fr/ouestfrance/querydsl/postgrest/ParametrizedTypeUtils.java new file mode 100644 index 0000000..5eb5d4b --- /dev/null +++ b/querydsl-postgrest-webclient-adapter/src/main/java/fr/ouestfrance/querydsl/postgrest/ParametrizedTypeUtils.java @@ -0,0 +1,26 @@ +package fr.ouestfrance.querydsl.postgrest; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.reflect.TypeUtils; +import org.springframework.core.ParameterizedTypeReference; + +import java.util.List; + +/** + * Type Utilities + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class ParametrizedTypeUtils { + + /** + * Create parametrized type of List of T + * + * @param clazz class of T + * @param type of parametrized list + * @return parametrized type + */ + public static ParameterizedTypeReference> listRef(Class clazz) { + return ParameterizedTypeReference.forType(TypeUtils.parameterize(List.class, clazz)); + } +} diff --git a/querydsl-postgrest-webclient-adapter/src/main/java/fr/ouestfrance/querydsl/postgrest/PostgrestWebClient.java b/querydsl-postgrest-webclient-adapter/src/main/java/fr/ouestfrance/querydsl/postgrest/PostgrestWebClient.java index 01623fc..ea3daa6 100644 --- a/querydsl-postgrest-webclient-adapter/src/main/java/fr/ouestfrance/querydsl/postgrest/PostgrestWebClient.java +++ b/querydsl-postgrest-webclient-adapter/src/main/java/fr/ouestfrance/querydsl/postgrest/PostgrestWebClient.java @@ -1,14 +1,11 @@ package fr.ouestfrance.querydsl.postgrest; +import fr.ouestfrance.querydsl.postgrest.model.BulkResponse; import fr.ouestfrance.querydsl.postgrest.model.CountItem; -import fr.ouestfrance.querydsl.postgrest.model.Page; -import fr.ouestfrance.querydsl.postgrest.model.PageImpl; import fr.ouestfrance.querydsl.postgrest.model.Range; import fr.ouestfrance.querydsl.postgrest.model.RangeResponse; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.reflect.TypeUtils; -import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -20,7 +17,9 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.stream.Stream; + +import static fr.ouestfrance.querydsl.postgrest.ParametrizedTypeUtils.listRef; +import static fr.ouestfrance.querydsl.postgrest.ResponseUtils.toBulkResponse; /** * Rest interface for querying postgrest @@ -45,8 +44,7 @@ public class PostgrestWebClient implements PostgrestClient { * @return PostgrestWebClient implementation */ public static PostgrestWebClient of(WebClient webClient) { - return - new PostgrestWebClient(webClient); + return new PostgrestWebClient(webClient); } @Override @@ -66,12 +64,8 @@ public RangeResponse search(String resource, Map> pa return Optional.ofNullable(response) .map(HttpEntity::getBody) .map(x -> { - Range range = Optional.ofNullable(response.getHeaders().get("Content-Range")) - .map(List::stream) - .map(Stream::findFirst) - .filter(Optional::isPresent) - .map(Optional::get) - .map(Range::of).orElse(null); + Range range = ResponseUtils.getCount(response.getHeaders()) + .orElse(null); return new RangeResponse<>(x, range); }).orElse(new RangeResponse<>(List.of(), null)); } @@ -89,29 +83,24 @@ public List count(String resource, Map> params) return Optional.ofNullable(response).map(HttpEntity::getBody).orElse(List.of()); } - private static void safeAdd(Map> headers, HttpHeaders httpHeaders) { - Optional.ofNullable(headers) - .map(PostgrestWebClient::toMultiMap).ifPresent(httpHeaders::addAll); - // Add contentType with default on call if webclient default is not set - httpHeaders.put(CONTENT_TYPE, List.of(MediaType.APPLICATION_JSON_VALUE)); - } @Override - public List post(String resource, List value, Map> headers, Class clazz) { - return webClient.post().uri(uriBuilder -> { + public BulkResponse post(String resource, List value, Map> headers, Class clazz) { + ResponseEntity> response = webClient.post().uri(uriBuilder -> { uriBuilder.path(resource); return uriBuilder.build(); }).headers(httpHeaders -> safeAdd(headers, httpHeaders)) .bodyValue(value) .retrieve() - .bodyToMono(listRef(clazz)) + .toEntity(listRef(clazz)) .block(); + return toBulkResponse(response); } @Override - public List patch(String resource, Map> params, Object value, Map> headers, Class clazz) { - return webClient.patch().uri(uriBuilder -> { + public BulkResponse patch(String resource, Map> params, Object value, Map> headers, Class clazz) { + ResponseEntity> response = webClient.patch().uri(uriBuilder -> { uriBuilder.path(resource); uriBuilder.queryParams(toMultiMap(params)); return uriBuilder.build(); @@ -119,26 +108,31 @@ public List patch(String resource, Map> params, Obje .bodyValue(value) .headers(httpHeaders -> safeAdd(headers, httpHeaders)) .retrieve() - .bodyToMono(listRef(clazz)).block(); + .toEntity(listRef(clazz)).block(); + return toBulkResponse(response); } @Override - public List delete(String resource, Map> params, Map> headers, Class clazz) { - return webClient.delete().uri(uriBuilder -> { + public BulkResponse delete(String resource, Map> params, Map> headers, Class clazz) { + ResponseEntity> response = webClient.delete().uri(uriBuilder -> { uriBuilder.path(resource); uriBuilder.queryParams(toMultiMap(params)); return uriBuilder.build(); }).headers(httpHeaders -> safeAdd(headers, httpHeaders)) .retrieve() - .bodyToMono(listRef(clazz)).block(); + .toEntity(listRef(clazz)).block(); + return toBulkResponse(response); } - private static ParameterizedTypeReference> listRef(Class clazz) { - return ParameterizedTypeReference.forType(TypeUtils.parameterize(List.class, clazz)); - } private static MultiValueMap toMultiMap(Map> params) { return new LinkedMultiValueMap<>(params); } + private static void safeAdd(Map> headers, HttpHeaders httpHeaders) { + Optional.ofNullable(headers) + .map(PostgrestWebClient::toMultiMap).ifPresent(httpHeaders::addAll); + // Add contentType with default on call if webclient default is not set + httpHeaders.put(CONTENT_TYPE, List.of(MediaType.APPLICATION_JSON_VALUE)); + } } diff --git a/querydsl-postgrest-webclient-adapter/src/main/java/fr/ouestfrance/querydsl/postgrest/ResponseUtils.java b/querydsl-postgrest-webclient-adapter/src/main/java/fr/ouestfrance/querydsl/postgrest/ResponseUtils.java new file mode 100644 index 0000000..e941af1 --- /dev/null +++ b/querydsl-postgrest-webclient-adapter/src/main/java/fr/ouestfrance/querydsl/postgrest/ResponseUtils.java @@ -0,0 +1,46 @@ +package fr.ouestfrance.querydsl.postgrest; + +import fr.ouestfrance.querydsl.postgrest.model.BulkResponse; +import fr.ouestfrance.querydsl.postgrest.model.Range; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; + +import java.util.List; +import java.util.Optional; + +/** + * Utility class that helps to transform response + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class ResponseUtils { + + /** + * Transform a ResponseEntity of list to bulkResponse + * + * @param response response entity + * @param Type of response + * @return BulkResponse + */ + public static BulkResponse toBulkResponse(ResponseEntity> response) { + return Optional.ofNullable(response) + .map(x -> { + Optional count = getCount(x.getHeaders()); + return new BulkResponse<>(x.getBody(), count.map(Range::getCount).orElse(0L), count.map(Range::getTotalElements).orElse(0L)); + }) + .orElse(new BulkResponse<>(List.of(), 0L, 0L)); + } + + /** + * Extract Range headers + * + * @param headers headers where Content-Range is + * @return range object + */ + public static Optional getCount(HttpHeaders headers) { + return Optional.ofNullable(headers.get("Content-Range")) + .flatMap(x -> x.stream().findFirst()) + .map(Range::of); + } +} diff --git a/querydsl-postgrest-webclient-adapter/src/test/java/fr/ouestfrance/querydsl/postgrest/PostrgrestWebClientRepositoryTest.java b/querydsl-postgrest-webclient-adapter/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestWebClientRepositoryTest.java similarity index 65% rename from querydsl-postgrest-webclient-adapter/src/test/java/fr/ouestfrance/querydsl/postgrest/PostrgrestWebClientRepositoryTest.java rename to querydsl-postgrest-webclient-adapter/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestWebClientRepositoryTest.java index 5c7960b..648f8e8 100644 --- a/querydsl-postgrest-webclient-adapter/src/test/java/fr/ouestfrance/querydsl/postgrest/PostrgrestWebClientRepositoryTest.java +++ b/querydsl-postgrest-webclient-adapter/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestWebClientRepositoryTest.java @@ -4,9 +4,11 @@ import fr.ouestfrance.querydsl.postgrest.app.PostRepository; import fr.ouestfrance.querydsl.postgrest.app.PostRequest; import fr.ouestfrance.querydsl.postgrest.criterias.Criteria; +import fr.ouestfrance.querydsl.postgrest.model.BulkResponse; import fr.ouestfrance.querydsl.postgrest.model.Page; import fr.ouestfrance.querydsl.postgrest.model.Pageable; import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import org.mockserver.integration.ClientAndServer; import org.mockserver.junit.jupiter.MockServerSettings; @@ -21,18 +23,30 @@ import java.util.List; import static org.junit.jupiter.api.Assertions.*; +import static org.mockserver.model.HttpRequest.request; +import static org.mockserver.model.HttpResponse.response; @MockServerSettings(ports = 8007) -class PostrgrestWebClientRepositoryTest { +@Slf4j +class PostgrestWebClientRepositoryTest { private final PostgrestRepository repository = new PostRepository(PostgrestWebClient.of(WebClient.builder() .baseUrl("http://localhost:8007/") .build())); + @Test + void shouldCountPosts(ClientAndServer client) { + client.reset(); + client.when(request().withPath("/posts").withQueryStringParameter("select", "count()")) + .respond(jsonFileResponse("count_response.json")); + long count = repository.count(); + assertEquals(300, count); + } + @Test void shouldSearchPosts(ClientAndServer client) { - client.when(HttpRequest.request().withPath("/posts").withQueryStringParameter("select", "*,authors(*)")) + client.when(request().withPath("/posts").withQueryStringParameter("select", "*,authors(*)")) .respond(jsonFileResponse("posts.json").withHeader("Content-Range", "0-6/300")); Page search = repository.search(new PostRequest(), Pageable.ofSize(6)); System.out.println(search.getTotalElements()); @@ -44,17 +58,9 @@ void shouldSearchPosts(ClientAndServer client) { search.getData().stream().map(Object::getClass).forEach(x -> assertEquals(Post.class, x)); } - @Test - void shouldCountPosts(ClientAndServer client) { - client.when(HttpRequest.request().withPath("/posts").withQueryStringParameter("select", "count()")) - .respond(jsonFileResponse("count_response.json")); - long count = repository.count(new PostRequest()); - assertEquals(300, count); - } - @Test void shouldFindById(ClientAndServer client) { - client.when(HttpRequest.request().withPath("/posts") + client.when(request().withPath("/posts") .withQueryStringParameter("id", "eq.1") .withQueryStringParameter("select", "*,authors(*)")) .respond(jsonFileResponse("posts.json").withHeader("Content-Range", "0-6/300")); @@ -70,7 +76,7 @@ void shouldFindById(ClientAndServer client) { @Test void shouldSearchGetByIds(ClientAndServer client) { - client.when(HttpRequest.request().withPath("/posts") + client.when(request().withPath("/posts") .withQueryStringParameter("id", "in.(1,2,3)") .withQueryStringParameter("select", "*,authors(*)")) .respond(jsonFileResponse("posts.json").withHeader("Content-Range", "0-6/300")); @@ -86,7 +92,17 @@ void shouldSearchGetByIds(ClientAndServer client) { @Test void shouldUpsertPost(ClientAndServer client) { - client.when(HttpRequest.request().withPath("/posts")) + client.when(request().withPath("/posts")) + .respond(jsonFileResponse("new_posts.json")); + List result = repository.upsert(new ArrayList<>(List.of(new Post()))); + assertNotNull(result); + result.stream().map(Object::getClass).forEach(x -> assertEquals(Post.class, x)); + + } + + @Test + void shouldUpsertBulkPost(ClientAndServer client) { + client.when(request().withPath("/posts")) .respond(jsonFileResponse("new_posts.json")); List result = repository.upsert(new ArrayList<>(List.of(new Post()))); assertNotNull(result); @@ -97,18 +113,32 @@ void shouldUpsertPost(ClientAndServer client) { @Test void shouldPatchPost(ClientAndServer client) { - client.when(HttpRequest.request().withPath("/posts").withQueryStringParameter("userId", "eq.25")) - .respond(jsonFileResponse("posts.json")); + client.when(request().withPath("/posts").withQueryStringParameter("userId", "eq.25")) + .respond(response().withHeader("Content-Range", "0-299/300")); PostRequest criteria = new PostRequest(); criteria.setUserId(25); - List result = repository.patch(criteria, new Post()); + BulkResponse result = repository.patch(criteria, new Post()); assertNotNull(result); - result.stream().map(Object::getClass).forEach(x -> assertEquals(Post.class, x)); + assertEquals(300, result.getAffectedRows()); + assertTrue(result.isEmpty()); + } + + @Test + void shouldPatchBulkPost(ClientAndServer client) { + client.when(request().withPath("/posts").withQueryStringParameter("userId", "eq.25")) + .respond(response().withHeader("Content-Range", "0-299/300")); + PostRequest criteria = new PostRequest(); + criteria.setUserId(25); + BulkResponse result = repository.patch(criteria, new Post()); + assertNotNull(result); + assertEquals(300, result.getAffectedRows()); + assertTrue(result.isEmpty()); } + @Test void shouldDeletePost(ClientAndServer client) { - client.when(HttpRequest.request().withPath("/posts").withQueryStringParameter("userId", "eq.25")) + client.when(request().withPath("/posts").withQueryStringParameter("userId", "eq.25")) .respond(jsonFileResponse("posts.json")); PostRequest criteria = new PostRequest(); criteria.setUserId(25); @@ -117,8 +147,21 @@ void shouldDeletePost(ClientAndServer client) { result.stream().map(Object::getClass).forEach(x -> assertEquals(Post.class, x)); } + + @Test + void shouldDeleteBulkPost(ClientAndServer client) { + client.when(request().withPath("/posts").withQueryStringParameter("userId", "eq.25")) + .respond(response().withHeader("Content-Range", "0-299/300")); + PostRequest criteria = new PostRequest(); + criteria.setUserId(25); + BulkResponse result = repository.delete(criteria); + assertNotNull(result); + assertEquals(300, result.getAffectedRows()); + assertTrue(result.isEmpty()); + } + private HttpResponse jsonFileResponse(String resourceFileName) { - return HttpResponse.response().withContentType(MediaType.APPLICATION_JSON) + return response().withContentType(MediaType.APPLICATION_JSON) .withBody(jsonOf(resourceFileName)); } diff --git a/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/PostgrestClient.java b/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/PostgrestClient.java index 59ed5e7..805ebc8 100644 --- a/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/PostgrestClient.java +++ b/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/PostgrestClient.java @@ -1,5 +1,6 @@ package fr.ouestfrance.querydsl.postgrest; +import fr.ouestfrance.querydsl.postgrest.model.BulkResponse; import fr.ouestfrance.querydsl.postgrest.model.CountItem; import fr.ouestfrance.querydsl.postgrest.model.RangeResponse; @@ -34,7 +35,7 @@ RangeResponse search(String resource, Map> params, * @param clazz type of return * @return list of inserted objects */ - List post(String resource, List value, Map> headers, Class clazz); + BulkResponse post(String resource, List value, Map> headers, Class clazz); /** * Patch data @@ -47,7 +48,7 @@ RangeResponse search(String resource, Map> params, * @param clazz type of return * @return list of patched objects */ - List patch(String resource, Map> params, Object value, Map> headers, Class clazz); + BulkResponse patch(String resource, Map> params, Object value, Map> headers, Class clazz); /** * Delete data @@ -59,7 +60,7 @@ RangeResponse search(String resource, Map> params, * @param clazz type of return * @return list of deleted objects */ - List delete(String resource, Map> params, Map> headers, Class clazz); + BulkResponse delete(String resource, Map> params, Map> headers, Class clazz); /** * Count data diff --git a/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/PostgrestRepository.java b/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/PostgrestRepository.java index abcd685..033f828 100644 --- a/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/PostgrestRepository.java +++ b/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/PostgrestRepository.java @@ -9,12 +9,15 @@ import fr.ouestfrance.querydsl.postgrest.model.impl.CountFilter; import fr.ouestfrance.querydsl.postgrest.model.impl.OrderFilter; import fr.ouestfrance.querydsl.postgrest.model.impl.SelectFilter; +import fr.ouestfrance.querydsl.postgrest.services.BulkExecutorService; import fr.ouestfrance.querydsl.postgrest.services.ext.PostgrestQueryProcessorService; import fr.ouestfrance.querydsl.service.ext.QueryDslProcessorService; import java.lang.reflect.ParameterizedType; import java.util.*; +import static fr.ouestfrance.querydsl.postgrest.annotations.Header.Method.*; + /** * Postgrest repository implementation * @@ -23,6 +26,7 @@ public class PostgrestRepository implements Repository { private final QueryDslProcessorService processorService = new PostgrestQueryProcessorService(); + private final BulkExecutorService bulkService = new BulkExecutorService(); private final PostgrestConfiguration annotation; private final Map>> headersMap = new EnumMap<>(Header.Method.class); private final PostgrestClient client; @@ -65,9 +69,9 @@ public Page search(Object criteria, Pageable pageable) { if (pageable.getPageSize() > 0) { headers.put("Range-Unit", List.of("items")); headers.put("Range", List.of(pageable.toRange())); - headers.computeIfAbsent("Prefers", x -> new ArrayList<>()) - .add("count=" + annotation.countStrategy().name().toLowerCase()); } + headers.computeIfAbsent(Prefer.HEADER, x -> new ArrayList<>()) + .add("count=" + annotation.countStrategy().name().toLowerCase()); // Add sort if present if (pageable.getSort() != null) { queryParams.add(OrderFilter.of(pageable.getSort())); @@ -96,26 +100,37 @@ public long count(Object criteria) { @Override - public List upsert(List values) { - return client.post(annotation.resource(), values, headerMap(Header.Method.UPSERT), clazz); + public BulkResponse upsert(List values) { + return client.post(annotation.resource(), values, headerMap(UPSERT), clazz); } @Override - public List patch(Object criteria, Object body) { - List queryParams = processorService.process(criteria); - // Add select criteria - getSelects(criteria).ifPresent(queryParams::add); - return client.patch(annotation.resource(), toMap(queryParams), body, headerMap(Header.Method.UPSERT), clazz); + public BulkResponse upsert(List values, BulkOptions options) { + // Add return representation headers only + return bulkService.execute(x -> client.post(annotation.resource(), values, x.getHeaders(), clazz), + BulkRequest.builder().headers(headerMap(UPSERT)).build(), + options); } @Override - public List delete(Object criteria) { - List queryParams = processorService.process(criteria); - // Add select criteria - getSelects(criteria).ifPresent(queryParams::add); - return client.delete(annotation.resource(), toMap(queryParams), headerMap(Header.Method.DELETE), clazz); + public BulkResponse patch(Object criteria, Object body, BulkOptions options) { + List filters = processorService.process(criteria); + getSelects(criteria).ifPresent(filters::add); + return bulkService.execute(x -> client.patch(annotation.resource(), toMap(filters), body, x.getHeaders(), clazz), + BulkRequest.builder().headers(headerMap(PATCH)).build(), + options); + } + + + @Override + public BulkResponse delete(Object criteria, BulkOptions options) { + List filters = processorService.process(criteria); + getSelects(criteria).ifPresent(filters::add); + return bulkService.execute(x -> client.delete(annotation.resource(), toMap(filters), x.getHeaders(), clazz), + BulkRequest.builder().headers(headerMap(DELETE)).build(), + options); } /** @@ -162,12 +177,18 @@ private Optional getSelects(Object criteria) { * @throws PostgrestRequestException when search criteria result gave more than one item */ public Optional findOne(Object criteria) { - Page search = search(criteria); + List queryParams = processorService.process(criteria); + Map> headers = headerMap(Header.Method.GET); + + // Add select criteria + getSelects(criteria).ifPresent(queryParams::add); + RangeResponse search = client.search(annotation.resource(), toMap(queryParams), headers, clazz); + if (search.getTotalElements() > 1) { throw new PostgrestRequestException(annotation.resource(), "Search with params " + criteria + " must found only one result, but found " + search.getTotalElements() + " results"); } - return search.stream().findFirst(); + return search.data().stream().findFirst(); } /** diff --git a/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/Repository.java b/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/Repository.java index 6a5e2e5..bddeaba 100644 --- a/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/Repository.java +++ b/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/Repository.java @@ -1,5 +1,7 @@ package fr.ouestfrance.querydsl.postgrest; +import fr.ouestfrance.querydsl.postgrest.model.BulkOptions; +import fr.ouestfrance.querydsl.postgrest.model.BulkResponse; import fr.ouestfrance.querydsl.postgrest.model.Page; import fr.ouestfrance.querydsl.postgrest.model.Pageable; import fr.ouestfrance.querydsl.postgrest.model.exceptions.PostgrestRequestException; @@ -76,7 +78,7 @@ default long count() { * @return upsert value */ default T upsert(Object value) { - List upsert = upsert(List.of(value)); + BulkResponse upsert = upsert(List.of(value)); return upsert.stream().findFirst().orElse(null); } @@ -86,7 +88,19 @@ default T upsert(Object value) { * @param value values to upsert * @return values inserted or updated */ - List upsert(List value); + default BulkResponse upsert(List value) { + return upsert(value, new BulkOptions()); + } + + /** + * Upsert multiple values with bulkMode + * + * @param value values to upserts + * @param options bulk options + * @return bulk response + */ + BulkResponse upsert(List value, BulkOptions options); + /** * Update multiple body @@ -95,15 +109,38 @@ default T upsert(Object value) { * @param body to update * @return list of patched object */ - List patch(Object criteria, Object body); + default BulkResponse patch(Object criteria, Object body) { + return patch(criteria, body, new BulkOptions()); + } + /** + * Update multiple body + * + * @param criteria criteria data + * @param body to update + * @param options bulk options + * @return list of patched object + */ + BulkResponse patch(Object criteria, Object body, BulkOptions options); + + + /** + * Delete items using criteria + * + * @param criteria criteria to create deletion query + * @return list of deleted items + */ + default BulkResponse delete(Object criteria) { + return delete(criteria, new BulkOptions()); + } /** * Delete items using criteria * * @param criteria criteria to create deletion query + * @param options bulk options * @return list of deleted items */ - List delete(Object criteria); + BulkResponse delete(Object criteria, BulkOptions options); } diff --git a/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/model/BulkOptions.java b/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/model/BulkOptions.java new file mode 100644 index 0000000..4f427d5 --- /dev/null +++ b/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/model/BulkOptions.java @@ -0,0 +1,24 @@ +package fr.ouestfrance.querydsl.postgrest.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * BulkOptions + */ +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Builder +public class BulkOptions { + /** + * Count only result + */ + private boolean countOnly = false; + /** + * Allow to make multiple calls + */ + private int pageSize = -1; +} diff --git a/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/model/BulkRequest.java b/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/model/BulkRequest.java new file mode 100644 index 0000000..c6f7e89 --- /dev/null +++ b/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/model/BulkRequest.java @@ -0,0 +1,22 @@ +package fr.ouestfrance.querydsl.postgrest.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * BulkRequest + */ +@Builder +@AllArgsConstructor +@Getter +public class BulkRequest { + /** + * Default headers + */ + Map> headers; +} diff --git a/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/model/BulkResponse.java b/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/model/BulkResponse.java new file mode 100644 index 0000000..ec375dc --- /dev/null +++ b/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/model/BulkResponse.java @@ -0,0 +1,55 @@ +package fr.ouestfrance.querydsl.postgrest.model; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +import java.util.ArrayList; +import java.util.List; + +/** + * Bulk response allow to retrieve the number of affected rows and the data + * + * @param type of data + */ +@Getter +public class BulkResponse extends ArrayList { + + public BulkResponse(List data, long affectedRows, long totalElements) { + super(data != null ? data : new ArrayList<>()); + this.affectedRows = affectedRows; + this.totalElements = totalElements; + } + + /** + * Number of affected rows + */ + private long affectedRows; + + /** + * Total elements in the criteria + */ + private long totalElements; + + /** + * Create a bulk response from a list of items + * + * @param items items to add + * @param type of data + * @return bulk response + */ + public static BulkResponse of(T... items) { + return new BulkResponse<>(new ArrayList<>(List.of(items)), items.length, items.length); + } + + /** + * Allow bulkResponse to be merged with another + * + * @param response response + */ + public void merge(BulkResponse response) { + affectedRows += response.getAffectedRows(); + totalElements = response.getTotalElements(); + addAll(response); + } +} diff --git a/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/model/Range.java b/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/model/Range.java index b58946a..5233406 100644 --- a/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/model/Range.java +++ b/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/model/Range.java @@ -49,4 +49,13 @@ public static Range of(String rangeString) { } return range; } + + /** + * Get count + * + * @return count + */ + public long getCount() { + return totalElements == 0 ? 0 : limit - offset + 1; + } } diff --git a/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/model/impl/CountFilter.java b/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/model/impl/CountFilter.java index bec0bb9..cbf89ae 100644 --- a/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/model/impl/CountFilter.java +++ b/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/model/impl/CountFilter.java @@ -38,6 +38,12 @@ public String getKey() { return KEY_PARAMETER; } + /** + * Get method + * + * @return methodgit status + * + */ public String getMethod() { return "count()"; } diff --git a/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/services/BulkExecutorService.java b/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/services/BulkExecutorService.java new file mode 100644 index 0000000..2335dd7 --- /dev/null +++ b/querydsl-postgrest/src/main/java/fr/ouestfrance/querydsl/postgrest/services/BulkExecutorService.java @@ -0,0 +1,53 @@ +package fr.ouestfrance.querydsl.postgrest.services; + +import fr.ouestfrance.querydsl.postgrest.annotations.PostgrestConfiguration; +import fr.ouestfrance.querydsl.postgrest.model.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +/** + * Bulk Executor service allow to execute on a wide range of data + * Options allow you to specify bulk parameters : + * - countOnly : Do not get object representation + * - pageSize: Specify chunk size, allow to iterate on postgrest and avoid timeout exceptions + */ +public class BulkExecutorService { + + public BulkResponse execute(Function> function, BulkRequest request, BulkOptions options) { + List prefers = request.getHeaders().computeIfAbsent(Prefer.HEADER, x -> new ArrayList<>()); + if (options.isCountOnly()) { + prefers.stream().filter(x -> x.startsWith("return")).findFirst().ifPresent(prefers::remove); + prefers.add(Prefer.Return.HEADERS_ONLY); + } + // Add default count + prefers.add("count=" + PostgrestConfiguration.CountType.EXACT.name().toLowerCase()); + // If we want to bulk update (multiple page) + Pageable pageable = null; + if (options.getPageSize() > 0) { + pageable = Pageable.ofSize(options.getPageSize()); + request.getHeaders().put("Range-Unit", List.of("items")); + } + // Do first call + BulkResponse response = new BulkResponse<>(null, 0, 0); + response.merge(function.apply(request)); + // If pageable is null -> Retrieve default pageable from the first response + if (pageable == null) { + pageable = Pageable.ofSize((int) response.getAffectedRows()); + } + + // If everything is not found then do next calls + while (hasNextPage(response)) { + // Start on page 1 + pageable = pageable.next(); + request.getHeaders().put("Range", List.of(pageable.toRange())); + response.merge(function.apply(request)); + } + return response; + } + + private static boolean hasNextPage(BulkResponse response) { + return response.getTotalElements() > response.getAffectedRows(); + } +} diff --git a/querydsl-postgrest/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestRepositoryCountMockTest.java b/querydsl-postgrest/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestRepositoryCountMockTest.java index 40bcbd4..f9146e0 100644 --- a/querydsl-postgrest/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestRepositoryCountMockTest.java +++ b/querydsl-postgrest/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestRepositoryCountMockTest.java @@ -34,14 +34,14 @@ void beforeEach() { @Test - void shouldCountWhithoutCriteriaOrNull() { + void shouldCountWithoutCriteriaOrNull() { when(postgrestClient.count(anyString(), any())).thenReturn(List.of(CountItem.of(1))); assertEquals(1, repository.count(null)); assertEquals(1, repository.count()); } @Test - void shouldCountWhithCriteria() { + void shouldCountWithCriteria() { PostRequest request = new PostRequest(); request.setUserId(1); request.setId(1); diff --git a/querydsl-postgrest/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestRepositoryDeleteMockTest.java b/querydsl-postgrest/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestRepositoryDeleteMockTest.java index a7f6683..edabeaf 100644 --- a/querydsl-postgrest/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestRepositoryDeleteMockTest.java +++ b/querydsl-postgrest/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestRepositoryDeleteMockTest.java @@ -3,6 +3,7 @@ import fr.ouestfrance.querydsl.postgrest.app.Post; import fr.ouestfrance.querydsl.postgrest.app.PostDeleteRequest; import fr.ouestfrance.querydsl.postgrest.app.PostRepository; +import fr.ouestfrance.querydsl.postgrest.model.BulkResponse; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -37,7 +38,7 @@ void shouldDelete() { ArgumentCaptor>> queriesCaptor = multiMapCaptor(); ArgumentCaptor>> headerCaptor = multiMapCaptor(); Post deletedPost = new Post(); - when(postgrestClient.delete(anyString(), queriesCaptor.capture(), headerCaptor.capture(), eq(Post.class))).thenReturn(List.of(deletedPost)); + when(postgrestClient.delete(anyString(), queriesCaptor.capture(), headerCaptor.capture(), eq(Post.class))).thenReturn(BulkResponse.of(deletedPost)); List delete = repository.delete(new PostDeleteRequest(List.of("1", "2"))); assertNotNull(delete); diff --git a/querydsl-postgrest/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestRepositoryGetMockTest.java b/querydsl-postgrest/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestRepositoryGetMockTest.java index 4f8b152..ed0f337 100644 --- a/querydsl-postgrest/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestRepositoryGetMockTest.java +++ b/querydsl-postgrest/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestRepositoryGetMockTest.java @@ -243,6 +243,7 @@ void shouldSearchWithJoin() { // Assert query captors Map> queries = queryArgs.getValue(); String queryString = QueryStringUtils.toQueryString(queries); + log.info("queryString {}", queryString); log.info("queries {}", queries); System.out.println(queries); assertEquals(1, queries.get("or").size()); diff --git a/querydsl-postgrest/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestRepositoryPatchMockTest.java b/querydsl-postgrest/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestRepositoryPatchMockTest.java index e05d142..f1211ca 100644 --- a/querydsl-postgrest/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestRepositoryPatchMockTest.java +++ b/querydsl-postgrest/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestRepositoryPatchMockTest.java @@ -3,6 +3,8 @@ import fr.ouestfrance.querydsl.postgrest.app.Post; import fr.ouestfrance.querydsl.postgrest.app.PostDeleteRequest; import fr.ouestfrance.querydsl.postgrest.app.PostRepository; +import fr.ouestfrance.querydsl.postgrest.model.BulkOptions; +import fr.ouestfrance.querydsl.postgrest.model.BulkResponse; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -11,32 +13,34 @@ import java.util.List; import java.util.Map; +import java.util.Objects; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @Slf4j class PostgrestRepositoryPatchMockTest extends AbstractRepositoryMockTest { @Mock - private PostgrestClient webClient; + private PostgrestClient webclient; private PostgrestRepository repository; @BeforeEach void beforeEach() { - repository = new PostRepository(webClient); + repository = new PostRepository(webclient); } @Test - void shouldDelete() { + void shouldPatch() { ArgumentCaptor>> queriesCaptor = multiMapCaptor(); ArgumentCaptor>> headerCaptor = multiMapCaptor(); Post post = new Post(); post.setUserId(26); - when(webClient.patch(anyString(), queriesCaptor.capture(), eq(post), headerCaptor.capture(), eq(Post.class))).thenReturn(List.of(post)); + when(webclient.patch(anyString(), queriesCaptor.capture(), eq(post), headerCaptor.capture(), eq(Post.class))).thenReturn(BulkResponse.of(post)); List patched = repository.patch(new PostDeleteRequest(List.of("1", "2")), post); assertNotNull(patched); assertEquals(1, patched.size()); @@ -45,4 +49,27 @@ void shouldDelete() { Map> headers = headerCaptor.getValue(); assertTrue(headers.get("Prefer").contains("return=representation")); } + + + @Test + void shouldBulkPatchInBulkMode() { + ArgumentCaptor>> queriesCaptor = multiMapCaptor(); + ArgumentCaptor>> headerCaptor = multiMapCaptor(); + Post post = new Post(); + post.setUserId(26); + when(webclient.patch(anyString(), queriesCaptor.capture(), eq(post), headerCaptor.capture(), eq(Post.class))).thenReturn(new BulkResponse<>(null, 50, 450)); + + // Should patch all data by chunk of 100 + BulkResponse patched = repository.patch(new PostDeleteRequest(List.of("1", "2")), post, BulkOptions.builder() + .countOnly(true) + .pageSize(50) + .build()); + assertNotNull(patched); + assertEquals(0, patched.size()); + assertEquals(9, headerCaptor.getAllValues().size()); + assertEquals(450, patched.getTotalElements()); + assertEquals(450, patched.getAffectedRows()); + // Check that Bulk make 9 calls + headerCaptor.getAllValues().stream().map(x->x.get("Range").stream().findFirst().orElse(null)).filter(Objects::nonNull).forEach(System.out::println); + } } diff --git a/querydsl-postgrest/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestRepositoryUpsertMockTest.java b/querydsl-postgrest/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestRepositoryUpsertMockTest.java index 603a36a..68a415a 100644 --- a/querydsl-postgrest/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestRepositoryUpsertMockTest.java +++ b/querydsl-postgrest/src/test/java/fr/ouestfrance/querydsl/postgrest/PostgrestRepositoryUpsertMockTest.java @@ -1,35 +1,39 @@ package fr.ouestfrance.querydsl.postgrest; import fr.ouestfrance.querydsl.postgrest.app.Post; +import fr.ouestfrance.querydsl.postgrest.app.PostDeleteRequest; import fr.ouestfrance.querydsl.postgrest.app.PostRepository; +import fr.ouestfrance.querydsl.postgrest.model.BulkOptions; +import fr.ouestfrance.querydsl.postgrest.model.BulkResponse; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mock; +import org.mockito.Mockito; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.when; @Slf4j class PostgrestRepositoryUpsertMockTest extends AbstractRepositoryMockTest { @Mock - private PostgrestClient webClient; + private PostgrestClient client; private PostgrestRepository repository; @BeforeEach void beforeEach() { - repository = new PostRepository(webClient); + repository = new PostRepository(client); } @Test @@ -43,12 +47,12 @@ void shouldUpsert() { save.setTitle("title"); save.setBody("test"); - when(webClient.post(anyString(), postCaptor.capture(), headerCaptor.capture(), eq(Post.class))).thenAnswer(x -> { + when(client.post(anyString(), postCaptor.capture(), headerCaptor.capture(), eq(Post.class))).thenAnswer(x -> { Post post = new Post(); post.setId(generateId); post.setTitle(save.getTitle()); post.setBody(save.getBody()); - return List.of(post); + return BulkResponse.of(post); }); Post saved = repository.upsert(save); assertNotNull(saved); @@ -61,4 +65,27 @@ void shouldUpsert() { assertEquals(2, headers.get("Prefer").size()); assertEquals("return=representation", headers.get("Prefer").stream().findFirst().orElseThrow()); } + + @Test + void shouldUpsertInBulkMode() { + ArgumentCaptor>> headerCaptor = multiMapCaptor(); + Post save = new Post(); + save.setTitle("title"); + save.setBody("test"); + + when(client.post(anyString(), any(), headerCaptor.capture(), eq(Post.class))).thenReturn(new BulkResponse<>(null, 50, 450)); + + // Should patch all data by chunk of 100 + BulkResponse upsert = repository.upsert(List.of(save), BulkOptions.builder() + .countOnly(true) + .pageSize(50) + .build()); + assertNotNull(upsert); + assertEquals(0, upsert.size()); + assertEquals(450, upsert.getAffectedRows()); + assertEquals(450, upsert.getTotalElements()); + // Check that Bulk make 9 calls + assertEquals(9, headerCaptor.getAllValues().size()); + headerCaptor.getAllValues().stream().map(x->x.get("Range").stream().findFirst().orElse(null)).filter(Objects::nonNull).forEach(System.out::println); + } } diff --git a/querydsl-postgrest/src/test/java/fr/ouestfrance/querydsl/postgrest/model/RangeTest.java b/querydsl-postgrest/src/test/java/fr/ouestfrance/querydsl/postgrest/model/RangeTest.java index 75fb5fd..4bf3663 100644 --- a/querydsl-postgrest/src/test/java/fr/ouestfrance/querydsl/postgrest/model/RangeTest.java +++ b/querydsl-postgrest/src/test/java/fr/ouestfrance/querydsl/postgrest/model/RangeTest.java @@ -16,6 +16,9 @@ void shouldCreateRange() { assertEquals(0, range.getOffset()); assertEquals(24, range.getLimit()); assertEquals(156, range.getTotalElements()); + + long count = range.getCount(); + assertEquals(25, count); } } diff --git a/querydsl-postgrest/src/test/java/fr/ouestfrance/querydsl/postgrest/services/BulkExecutorServiceTest.java b/querydsl-postgrest/src/test/java/fr/ouestfrance/querydsl/postgrest/services/BulkExecutorServiceTest.java new file mode 100644 index 0000000..ed56bc4 --- /dev/null +++ b/querydsl-postgrest/src/test/java/fr/ouestfrance/querydsl/postgrest/services/BulkExecutorServiceTest.java @@ -0,0 +1,65 @@ +package fr.ouestfrance.querydsl.postgrest.services; + +import fr.ouestfrance.querydsl.postgrest.model.BulkOptions; +import fr.ouestfrance.querydsl.postgrest.model.BulkRequest; +import fr.ouestfrance.querydsl.postgrest.model.BulkResponse; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class BulkExecutorServiceTest { + + private final BulkExecutorService executorService = new BulkExecutorService(); + + @Test + void shouldBulkExecute() { + AtomicInteger calls = new AtomicInteger(0); + int pageSize = 50; + int totalElements = 450; + BulkResponse result = executorService.execute(x -> { + calls.incrementAndGet(); + return new BulkResponse<>(List.of(), pageSize, totalElements); + + }, BulkRequest.builder().headers(new HashMap<>()).build(), BulkOptions.builder().build()); + assertEquals(totalElements, result.getTotalElements()); + assertEquals(totalElements, result.getAffectedRows()); + assertEquals(totalElements / pageSize, calls.get()); + } + + @Test + void shouldBulkExecuteWithPageSize() { + AtomicInteger calls = new AtomicInteger(0); + int pageSize = 10; + int totalElements = 450; + BulkResponse result = executorService.execute(x -> { + calls.incrementAndGet(); + return new BulkResponse<>(List.of(), pageSize, totalElements); + + }, BulkRequest.builder().headers(new HashMap<>()).build(), BulkOptions.builder() + .countOnly(true) + .pageSize(pageSize) + .build()); + assertEquals(totalElements, result.getTotalElements()); + assertEquals(totalElements, result.getAffectedRows()); + assertEquals(totalElements / pageSize, calls.get()); + } + + @Test + void shouldBulkExecuteOnePage() { + AtomicInteger calls = new AtomicInteger(0); + BulkResponse result = executorService.execute(x -> { + calls.incrementAndGet(); + return new BulkResponse<>(List.of(), 100, 100); + }, + BulkRequest.builder().headers(new HashMap<>()).build(), + BulkOptions.builder().countOnly(true).build()); + assertEquals(100, result.getTotalElements()); + assertEquals(100, result.getAffectedRows()); + assertEquals(1, calls.get()); + } + +}