Skip to content

Commit

Permalink
feat(null): upsert and onConflict
Browse files Browse the repository at this point in the history
Issues :
- #3 Upsert does not really upsert
- #8 add on_conflict for upsert
  • Loading branch information
julienTho committed Jul 22, 2024
1 parent 778c8a5 commit b614013
Show file tree
Hide file tree
Showing 13 changed files with 256 additions and 51 deletions.
35 changes: 24 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,15 +185,17 @@ public class UserRepository extends PostgrestRepository<User> {

##### PostgrestRepository functions

| Method | Return | Parameters | Description |
|---------|---------------|-------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------|
| search | `Page<T>` | criteria : `Object`<br/>pageRequest : `Pageable` (optional) | Search request based on criteria and pagination |
| findOne | `Optional<T>` | criteria : `Object` | find one entity based on criteria<br/>Raise `PostgrestRequestException` if criteria return more than one item |
| getOne | `T` | criteria : `Object` | get one entity based on criteria<br/>Raise `PostgrestRequestException` if criteria returned no entity |
| upsert | `T` | value : `Object` | post data, you may define the strategy (Insert / Update) by adding header annotation `Prefer: resolution=merge-duplicates` |
| upsert | `List<T>` | value : `List<Object>` | post data, you may define the strategy (Insert / Update) by adding header annotation `Prefer: resolution=merge-duplicates` |
| update | `List<T>` | criteria : `Object`<br/>value: `Object` | Update entities found by criterias |
| delete | `List<T>` | criteria : `Object` | Delete entities found by the criteria |
| Method | Return | Parameters | Description |
|---------|---------------|-------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| search | `Page<T>` | criteria : `Object`<br/>pageRequest : `Pageable` (optional) | Search request based on criteria and pagination |
| findOne | `Optional<T>` | criteria : `Object` | find one entity based on criteria<br/>Raise `PostgrestRequestException` if criteria return more than one item |
| getOne | `T` | criteria : `Object` | get one entity based on criteria<br/>Raise `PostgrestRequestException` if criteria returned no entity |
| post | `T` | value : `Object` | post data |
| post | `List<T>` | value : `List<Object>` | post list of datas |
| upsert | `T` | value : `Object` | Insert or Update data. You can specify which properties form the unique constraint by adding @OnConflict annotation on your implementation of PostgrestRepository |
| upsert | `List<T>` | value : `List<Object>` | Insert or Update datas. You can specify which properties form the unique constraint by adding @OnConflict annotation on your implementation of PostgrestRepository |
| update | `List<T>` | criteria : `Object`<br/>value: `Object` | Update entities found by criterias |
| delete | `List<T>` | criteria : `Object` | Delete entities found by the criteria |

### Using the repository

Expand Down Expand Up @@ -291,7 +293,7 @@ Will return json like this :

This library allow strategy based on `Prefer` header see
official [PostgREST Documentation](https://postgrest.org/en/stable/references/api/preferences.html) by adding `@Header`
annotation over your Repositoru
annotation over your Repository

```java
// Return representation object for all functions
Expand All @@ -301,6 +303,17 @@ annotation over your Repositoru
public class PostRepository extends PostgrestRepository<Post> {
}
```
##### Upserts
```java
// If you want to specify which properties form your unique constraint, you don't need to write this header anymore :
@Header(key = Prefer.HEADER, value = Prefer.Resolution.MERGE_DUPLICATES, methods = UPSERT)
public class PostRepository extends PostgrestRepository<Post> {
}
//just annotate your implem of Repository with @OnConflict and specify wich fields form your unique constraint :
@OnConflict(columnNames = {"codeOrigine", "referencePersonne", "uuidAdresse"})
public class PostRepository extends PostgrestRepository<Post> {
}
```

#### Logical condition

Expand Down Expand Up @@ -387,7 +400,7 @@ public class UserService {
| 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`
> Bulk Operations are allowed on `Post` ,`Patch`, `Delete` and `Upsert`
## Need Help ?

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,13 @@ public <T> RangeResponse<T> search(String resource, Map<String, List<String>> pa
}

@Override
public <T> BulkResponse<T> post(String resource, List<Object> value, Map<String, List<String>> headers, Class<T> clazz) {
ResponseEntity<List<T>> response = restTemplate.exchange(getUri(resource), HttpMethod.POST, new HttpEntity<>(value, toHeaders(headers)), listRef(clazz));
public <T> BulkResponse<T> post(String resource, Map<String, List<String>> params, List<Object> value, Map<String, List<String>> headers, Class<T> clazz) {
ResponseEntity<List<T>> response = restTemplate.exchange(
getUri(resource, params),
HttpMethod.POST, new HttpEntity<>(value, toHeaders(headers)), listRef(clazz));
return toBulkResponse(response);
}


@Override
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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import org.mockserver.model.MediaType;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.DefaultUriBuilderFactory;
import shaded_package.org.apache.commons.io.IOUtils;

import java.nio.charset.Charset;
Expand Down Expand Up @@ -102,17 +101,35 @@ void shouldSearchGetByIds(MockServerClient client) {
}

@Test
void shouldUpsertPost(MockServerClient client) {
void shouldPost(MockServerClient client) {
client.when(HttpRequest.request().withPath("/posts"))
.respond(jsonFileResponse("new_posts.json"));
List<Post> result = repository.upsert(new ArrayList<>(List.of(new Post())));
List<Post> result = repository.post(new ArrayList<>(List.of(new Post())));
assertNotNull(result);
result.stream().map(Object::getClass).forEach(x -> assertEquals(Post.class, x));
}

@Test
void shouldBulkPost(MockServerClient client) {
client.when(HttpRequest.request().withPath("/posts"))
.respond(HttpResponse.response().withHeader("Content-Range", "0-299/300"));
BulkResponse<Post> result = repository.post(new ArrayList<>(List.of(new Post())));
assertNotNull(result);
assertEquals(300L, result.getAffectedRows());
assertTrue(result.isEmpty());
}

@Test
void shouldUpsertBulkPost(MockServerClient client) {
void shouldUpsert(MockServerClient client) {
client.when(HttpRequest.request().withPath("/posts"))
.respond(jsonFileResponse("new_posts.json"));
List<Post> result = repository.upsert(new ArrayList<>(List.of(new Post())));
assertNotNull(result);
result.stream().map(Object::getClass).forEach(x -> assertEquals(Post.class, x));
}

@Test
void shouldBulkUpsert(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())));
Expand All @@ -121,7 +138,6 @@ void shouldUpsertBulkPost(MockServerClient client) {
assertTrue(result.isEmpty());
}


@Test
void shouldPatchPost(MockServerClient client) {
client.when(HttpRequest.request().withPath("/posts").withQueryStringParameter("userId", "eq.25"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,15 @@

import fr.ouestfrance.querydsl.postgrest.PostgrestClient;
import fr.ouestfrance.querydsl.postgrest.PostgrestRepository;
import fr.ouestfrance.querydsl.postgrest.annotations.Header;
import fr.ouestfrance.querydsl.postgrest.annotations.PostgrestConfiguration;
import fr.ouestfrance.querydsl.postgrest.annotations.Select;
import fr.ouestfrance.querydsl.postgrest.annotations.*;
import fr.ouestfrance.querydsl.postgrest.model.Prefer;

import static fr.ouestfrance.querydsl.postgrest.annotations.Header.Method.UPSERT;

@PostgrestConfiguration(resource = "posts")
@Select("authors(*)")
@Header(key = Prefer.HEADER, value = Prefer.Return.REPRESENTATION)
@Header(key = Prefer.HEADER, value = Prefer.Resolution.MERGE_DUPLICATES, methods = UPSERT)
@OnConflict(columnNames = {"id", "title"})
public class PostRepository extends PostgrestRepository<Post> {

public PostRepository(PostgrestClient client) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,10 @@ public List<CountItem> count(String resource, Map<String, List<String>> params)


@Override
public <T> BulkResponse<T> post(String resource, List<Object> value, Map<String, List<String>> headers, Class<T> clazz) {
public <T> BulkResponse<T> post(String resource, Map<String, List<String>> params, List<Object> value, Map<String, List<String>> headers, Class<T> clazz) {
ResponseEntity<List<T>> response = webClient.post().uri(uriBuilder -> {
uriBuilder.path(resource);
uriBuilder.queryParams(toMultiMap(params));
return uriBuilder.build();
}).headers(httpHeaders -> safeAdd(headers, httpHeaders))
.bodyValue(value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,26 @@ void shouldSearchGetByIds(ClientAndServer client) {
}

@Test
void shouldUpsertPost(ClientAndServer client) {
void shouldPost(ClientAndServer client) {
client.when(request().withPath("/posts"))
.respond(jsonFileResponse("new_posts.json"));
List<Post> result = repository.upsert(new ArrayList<>(List.of(new Post())));
List<Post> result = repository.post(new ArrayList<>(List.of(new Post())));
assertNotNull(result);
result.stream().map(Object::getClass).forEach(x -> assertEquals(Post.class, x));

}

@Test
void shouldUpsertBulkPost(ClientAndServer client) {
void shouldPostBulk(ClientAndServer client) {
client.when(request().withPath("/posts"))
.respond(jsonFileResponse("new_posts.json"));
List<Post> result = repository.post(new ArrayList<>(List.of(new Post())));
assertNotNull(result);
result.stream().map(Object::getClass).forEach(x -> assertEquals(Post.class, x));
}

@Test
void shouldUpsert(ClientAndServer client) {
client.when(request().withPath("/posts"))
.respond(jsonFileResponse("new_posts.json"));
List<Post> result = repository.upsert(new ArrayList<>(List.of(new Post())));
Expand All @@ -109,6 +118,14 @@ void shouldUpsertBulkPost(ClientAndServer client) {

}

@Test
void shouldUpsertBulk(ClientAndServer client) {
client.when(request().withPath("/posts"))
.respond(jsonFileResponse("new_posts.json"));
List<Post> result = repository.upsert(new ArrayList<>(List.of(new Post())));
assertNotNull(result);
result.stream().map(Object::getClass).forEach(x -> assertEquals(Post.class, x));
}

@Test
void shouldPatchPost(ClientAndServer client) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ <T> RangeResponse<T> search(String resource, Map<String, List<String>> params,
* @param clazz type of return
* @return list of inserted objects
*/
<T> BulkResponse<T> post(String resource, List<Object> value, Map<String, List<String>> headers, Class<T> clazz);
<T> BulkResponse<T> post(String resource, Map<String, List<String>> queryParams, List<Object> value, Map<String, List<String>> headers, Class<T> clazz);

/**
* Patch data
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package fr.ouestfrance.querydsl.postgrest;

import fr.ouestfrance.querydsl.postgrest.annotations.Header;
import fr.ouestfrance.querydsl.postgrest.annotations.OnConflict;
import fr.ouestfrance.querydsl.postgrest.annotations.PostgrestConfiguration;
import fr.ouestfrance.querydsl.postgrest.annotations.Select;
import fr.ouestfrance.querydsl.postgrest.mappers.Operators;
import fr.ouestfrance.querydsl.postgrest.model.*;
import fr.ouestfrance.querydsl.postgrest.model.exceptions.MissingConfigurationException;
import fr.ouestfrance.querydsl.postgrest.model.exceptions.PostgrestRequestException;
Expand Down Expand Up @@ -102,19 +102,50 @@ public long count(Object criteria) {


@Override
public BulkResponse<T> upsert(List<Object> values) {
return client.post(annotation.resource(), values, headerMap(UPSERT), clazz);
public BulkResponse<T> post(List<Object> values) {
return client.post(annotation.resource(), new HashMap<>(), values, headerMap(UPSERT), clazz);
}

@Override
public BulkResponse<T> post(List<Object> values, BulkOptions options) {
// Add return representation headers only
return bulkService.execute(x -> client.post(annotation.resource(), new HashMap<>(), values, x.getHeaders(), clazz),
BulkRequest.builder().headers(headerMap(UPSERT)).build(),
options);
}

@Override
public BulkResponse<T> upsert(List<Object> values) {
return client.post(annotation.resource(), getQueryParams(), values, getHeaders(), clazz);
}

@Override
public BulkResponse<T> upsert(List<Object> 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(),
return bulkService.execute(x -> client.post(annotation.resource(), getQueryParams(), values, x.getHeaders(), clazz),
BulkRequest.builder().headers(getHeaders()).build(),
options);
}

private Map<String, List<String>> getQueryParams() {
OnConflict onConflict = this.getClass().getAnnotation(OnConflict.class);
Map<String, List<String>> queryParams = new HashMap<>();
if (Objects.nonNull(onConflict)) {
queryParams.put("on_conflict", Arrays.asList(onConflict.columnNames()));
}
return queryParams;
}

private Map<String, List<String>> getHeaders() {
Map<String, List<String>> headerMap = headerMap(UPSERT);
List<String> currentHeaders = headerMap.get(Prefer.HEADER);
if (currentHeaders == null) {
currentHeaders = new ArrayList<>();
}
currentHeaders.add(Prefer.Resolution.MERGE_DUPLICATES);
headerMap.put(Prefer.HEADER, currentHeaders);
return headerMap;
}

@Override
public BulkResponse<T> patch(Object criteria, Object body, BulkOptions options) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,41 @@ default long count() {
*/
T getOne(Object criteria);

/**
* Post a value
*
* @param value to post
* @return value inserted
*/
default T post(Object value) {
BulkResponse<T> post = post(List.of(value));
return post.stream().findFirst().orElse(null);
}

/**
* Post multiple values
*
* @param values values to post
* @return values inserted
*/
default BulkResponse<T> post(List<Object> values) {
return post(values, new BulkOptions());
}

/**
* Post multiple values with bulkMode
*
* @param value values to post
* @param options bulk options
* @return bulk response
*/
BulkResponse<T> post(List<Object> value, BulkOptions options);

/**
* Upsert a value
*
* @param value to upsert
* @return upsert value
* @return inserted or updated value
*/
default T upsert(Object value) {
BulkResponse<T> upsert = upsert(List.of(value));
Expand All @@ -85,22 +115,21 @@ default T upsert(Object value) {
/**
* Upsert multiple values
*
* @param value values to upsert
* @param values values to upsert
* @return values inserted or updated
*/
default BulkResponse<T> upsert(List<Object> value) {
return upsert(value, new BulkOptions());
default BulkResponse<T> upsert(List<Object> values) {
return upsert(values, new BulkOptions());
}

/**
* Upsert multiple values with bulkMode
*
* @param value values to upserts
* @param values values to upsert
* @param options bulk options
* @return bulk response
*/
BulkResponse<T> upsert(List<Object> value, BulkOptions options);

BulkResponse<T> upsert(List<Object> values, BulkOptions options);

/**
* Update multiple body
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package fr.ouestfrance.querydsl.postgrest.annotations;

import java.lang.annotation.*;

/**
* OnConflict annotation to specify which columns compose the unique constraint
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
public @interface OnConflict {

/**
* Columns names of the unique constraint
* @return Tab of the Columns names
*/
String[] columnNames() default "";
}
Loading

0 comments on commit b614013

Please sign in to comment.