Skip to content

Commit

Permalink
11 mass update (#20)
Browse files Browse the repository at this point in the history
* feat(mass-update): allow mass update
  • Loading branch information
arnaud-thorel-of authored Feb 7, 2024
1 parent 76432fb commit 7bcfeb5
Show file tree
Hide file tree
Showing 26 changed files with 722 additions and 118 deletions.
52 changes: 46 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

<dependency>
<groupId>fr.ouestfrance.querydsl</groupId>
<artifactId>querydsl-postgrest</artifactId>
Expand Down Expand Up @@ -52,8 +53,10 @@ cookies, ...) you need to deploy.

#### WebClient configuration example

Add the dependency :
Add the dependency :

```xml

<dependency>
<groupId>fr.ouestfrance.querydsl</groupId>
<artifactId>querydsl-postgrest-webclient-adapter</artifactId>
Expand Down Expand Up @@ -87,8 +90,10 @@ public class PostgrestConfiguration {

#### RestTemplate configuration example

Add the dependency :
Add the dependency :

```xml

<dependency>
<groupId>fr.ouestfrance.querydsl</groupId>
<artifactId>querydsl-postgrest-resttemplate-adapter</artifactId>
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<phase>deploy</phase>
<goals>
<goal>sign</goal>
</goals>
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <T> type of parametrized list
* @return parametrized type
*/
public static <T> ParameterizedTypeReference<List<T>> listRef(Class<T> clazz) {
return ParameterizedTypeReference.forType(TypeUtils.parameterize(List.class, clazz));
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -56,36 +57,32 @@ public <T> RangeResponse<T> search(String resource, Map<String, List<String>> 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 <T> List<T> post(String resource, List<Object> value, Map<String, List<String>> headers, Class<T> clazz) {
HttpHeaders httpHeaders = toHeaders(headers);
return restTemplate.exchange(resource, HttpMethod.POST, new HttpEntity<>(value, httpHeaders), listRef(clazz)).getBody();
public <T> BulkResponse<T> post(String resource, List<Object> value, Map<String, List<String>> headers, Class<T> clazz) {
ResponseEntity<List<T>> response = restTemplate.exchange(resource, HttpMethod.POST, new HttpEntity<>(value, toHeaders(headers)), listRef(clazz));
return toBulkResponse(response);
}


@Override
public <T> List<T> patch(String resource, Map<String, List<String>> params, Object value, Map<String, List<String>> headers, Class<T> clazz) {
MultiValueMap<String, String> 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 <T> BulkResponse<T> patch(String resource, Map<String, List<String>> params, Object value, Map<String, List<String>> headers, Class<T> clazz) {
ResponseEntity<List<T>> 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 <T> List<T> delete(String resource, Map<String, List<String>> params, Map<String, List<String>> headers, Class<T> clazz) {
MultiValueMap<String, String> 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 <T> BulkResponse<T> delete(String resource, Map<String, List<String>> params, Map<String, List<String>> headers, Class<T> clazz) {
ResponseEntity<List<T>> 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
Expand All @@ -94,9 +91,6 @@ public List<CountItem> count(String resource, Map<String, List<String>> map) {
.queryParams(toMultiMap(map)).build().toString(), new HashMap<>()), HttpMethod.GET, new HttpEntity<>(null, new HttpHeaders()), listRef(CountItem.class)).getBody();
}

private static <T> ParameterizedTypeReference<List<T>> listRef(Class<T> clazz) {
return ParameterizedTypeReference.forType(TypeUtils.parameterize(List.class, clazz));
}

private static MultiValueMap<String, String> toMultiMap(Map<String, List<String>> params) {
return new LinkedMultiValueMap<>(params);
Expand All @@ -105,4 +99,5 @@ private static MultiValueMap<String, String> toMultiMap(Map<String, List<String>
private static HttpHeaders toHeaders(Map<String, List<String>> headers) {
return new HttpHeaders(toMultiMap(headers));
}

}
Original file line number Diff line number Diff line change
@@ -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 <T> Type of response
* @return BulkResponse
*/
public static <T> BulkResponse<T> toBulkResponse(ResponseEntity<List<T>> response) {
return Optional.ofNullable(response)
.map(x -> {
Optional<Range> 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<Range> getCount(HttpHeaders headers) {
return Optional.ofNullable(headers.get("Content-Range"))
.flatMap(x -> x.stream().findFirst())
.map(Range::of);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Post> result = repository.upsert(new ArrayList<>(List.of(new Post())));
assertNotNull(result);
assertEquals(300L, result.getAffectedRows());
assertTrue(result.isEmpty());
}


@Test
void shouldPatchPost(MockServerClient client) {
Expand All @@ -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<Post> 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"))
Expand All @@ -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<Post> 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));
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <T> type of parametrized list
* @return parametrized type
*/
public static <T> ParameterizedTypeReference<List<T>> listRef(Class<T> clazz) {
return ParameterizedTypeReference.forType(TypeUtils.parameterize(List.class, clazz));
}
}
Loading

0 comments on commit 7bcfeb5

Please sign in to comment.