QueryDSL-PostgRest is a PostgRest implementation of QueryDSL library and provides class and annotation to improve your developer experience using PostgRest.
PostgREST is an open source project that provides a fully RESTful API from any existing PostgreSQL database
Add the following dependency to your Maven project:
<dependency>
<groupId>fr.ouestfrance.querydsl</groupId>
<artifactId>querydsl-postgrest</artifactId>
<version>${querydsl-postgrest.version}</version>
</dependency>
Add the following dependency to your gradle project:
implementation 'fr.ouestfrance.querydsl:querydsl-postgrest:${querydsl-postgrest.version}'
QueryDsl postgrest provides class to simplify querying postgrest api using PostgrestClient,
It actually provides by default WebClient adapter PostgrestWebClient
adapter.
It's really easy to create your own HttpClientAdapter (RestTemplate, OkHttpClient, HttpConnexion, ...) by
implementing PostgrestClient
interface.
You can also specify authenticators, interceptors (retry, transform) and every configuration (timeout, default headers, cookies, ...) you need to deploy.
Add the dependency :
<dependency>
<groupId>fr.ouestfrance.querydsl</groupId>
<artifactId>querydsl-postgrest-webclient-adapter</artifactId>
<version>${querydsl-postgrest.version}</version>
</dependency>
import com.fasterxml.jackson.databind.ObjectMapper;
import fr.ouestfrance.querydsl.postgrest.PostgrestClient;
import fr.ouestfrance.querydsl.postgrest.PostgrestWebClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
@Configuration
public class PostgrestConfiguration {
@Bean
public PostgrestClient podstgrestClient() {
String serviceUrl = "http://localhost:9000";
WebClient webclient = WebClient.builder()
.baseUrl(serviceUrl)
// Here you can add any filters or default configuration you want
.build();
return PostgrestWebClient.of(webclient);
}
}
Add the dependency :
<dependency>
<groupId>fr.ouestfrance.querydsl</groupId>
<artifactId>querydsl-postgrest-resttemplate-adapter</artifactId>
<version>${querydsl-postgrest.version}</version>
</dependency>
import com.fasterxml.jackson.databind.ObjectMapper;
import fr.ouestfrance.querydsl.postgrest.PostgrestClient;
import fr.ouestfrance.querydsl.postgrest.PostgrestRestTemplate;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.DefaultUriBuilderFactory;
@Configuration
public class PostgrestConfiguration {
@Bean
public PostgrestClient podstgrestClient() {
String serviceUrl = "http://localhost:9000";
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(serviceUrl));
restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory(HttpClients.createDefault()));
return PostgrestRestTemplate.of(webclient);
}
}
You can start by writing the first search object using @FilterField
annotations. This example allow you to search user
with :
- equals filter on id key
- like filter on name key
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class UserSearch {
@FilterField
String id;
@FilterField(operation = FilterOperation.LIKE.class)
String name;
}
@Since 1.1.0 - Record Support
public record UserSearch(
@FilterField String id,
@FilterField(operation = FilterOperation.LIKE.class) String name
) {
}
To access data, you have to create Repository for your type and put @PostgrestConfiguration
to specify extra data
Property | Required | Format | Description | Example |
---|---|---|---|---|
resource | O | String | Resource name in the postgrest api | "users" |
countStrategy | X | String | Count strategy (exact, planned, estimated) default is exact | CountType.EXACT |
import fr.ouestfrance.querydsl.postgrest.PostgrestClient;
@Repository
@PostgrestConfiguration(resource = "users")
public class UserRepository extends PostgrestRepository<User> {
public UserRepository(PostgrestClient client) {
super(client);
}
}
Method | Return | Parameters | Description |
---|---|---|---|
search | Page<T> |
criteria : Object pageRequest : Pageable (optional) |
Search request based on criteria and pagination |
findOne | Optional<T> |
criteria : Object |
find one entity based on criteria Raise PostgrestRequestException if criteria return more than one item |
getOne | T |
criteria : Object |
get one entity based on criteria 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 value: Object |
Update entities found by criterias |
delete | List<T> |
criteria : Object |
Delete entities found by the criteria |
You can then create your functions :
- getUserById : Will return user with a specific id or raise a NotFoundException
- findUsersByName : Will return list of users which name contains part of search content
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
/**
* Return a specific
* @param id id of the user
* @return user found
*/
public User getById(String id) {
UserSearch criteria = new UserSearch();
criteria.setId(id);
return userRepository.getOne(criteria);
}
/**
* Return all users matching part of the name
* @param name name to search
* @return list of users
*/
public List<User> findByName(String name) {
UserSearch search = new UserSearch();
search.setName(name);
return userRepository.search(search);
}
}
When certain columns are wide (such as those holding binary data), it is more efficient for the server to withhold them
in a response. The client can specify which columns are required using the select parameter.
This can be defined by annotation @Select
Select annotation can be added on the Repository but also to the criteria object that allow you to add specific selection for filtering
Property | Required | Format | Description | Example |
---|---|---|---|---|
value | O | String | select value tu add | "firstname" |
alias | X | String | renaming column or alias | "fullName" |
You can add extra selection by adding @Select
annotation.
In this example there is an inner join on Posts.author
and selecting only firstName
and lastName
@PostgrestConfiguration(resource = "posts")
@Select(alias = "author", value = "author!inner(firstName, lastName)")
public class PostRepository extends PostgrestRepository<Post> {
}
Will return json like this :
[
{
"id": 1,
"title": "Post 1",
"author": {
"firstName": "John",
"lastName": "Doe"
}
},
{
"id": 2,
"title": "Post 2",
"author": {
"firstName": "Jane",
"lastName": "Doe"
}
}
]
This library allow strategy based on Prefer
header see
official PostgREST Documentation by adding @Header
annotation over your Repository
// Return representation object for all functions
@Header(key = Prefer.HEADER, value = Prefer.Return.REPRESENTATION)
// Make Upsert using POST with Merge_Duplicated value
@Header(key = Prefer.HEADER, value = Prefer.Resolution.MERGE_DUPLICATES, methods = UPSERT)
public class PostRepository extends PostgrestRepository<Post> {
}
// 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> {
}
Any chance you want to have a more complex condition, it's possible to make mixin or / and condition by
using groupName
@Getter
@Setter
@Select(alias = "filterFormats", value = "formats!inner(minSize, maxSize)")
public class PostRequestWithSize {
// size = $size OR (filterFormats.minSize < size AND filterFormats.maxSize > size)
@FilterField(key = "size", groupName = "sizeOrGroup")
@FilterFields(groupName = "sizeOrGroup", value = {
@FilterField(key = "filterFormats.minSize", operation = FilterOperation.GTE.class),
@FilterField(key = "filterFormats.maxSize", operation = FilterOperation.LTE.class, orNull = true)
})
private String size;
}
or on multiple fields
public class PostRequestWithAuthorOrSubject {
// subject = $subject OR name= $name
@FilterField(groupName = "subjectOrAuthorName")
private String subject;
@FilterField(groupName = "subjectOrAuthorName")
private String name;
}
extends FilterOperation with
Operator | Description |
---|---|
ILIKE | Case-insensitive LIKE |
CS | Contains for JSON/Range datatype |
CD | Contained for JSON/Range datatype |
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
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
Post
,Patch
,Delete
andUpsert
Supports of rpc function calls
Configuration Its use a PostgrestRpcClient which use the PostgrestClient
import com.fasterxml.jackson.databind.ObjectMapper;
import fr.ouestfrance.querydsl.postgrest.PostgrestClient;
import fr.ouestfrance.querydsl.postgrest.PostgrestWebClient;
import fr.ouestfrance.querydsl.postgrest.PostgrestRpcClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
@Configuration
public class PostgrestConfiguration {
@Bean
public PostgrestRpcClient rpcClient() {
String serviceUrl = "http://localhost:9000";
WebClient webclient = WebClient.builder()
.baseUrl(serviceUrl)
// Here you can add any filters or default configuration you want
.build();
return new PostgrestRpcClient(PostgrestWebClient.of(webclient));
}
}
then you can call your rpc method using this call
public class Example{
private PostgrestRpcClient rpcClient;
public List<Coordinate> getCoordinates(){
// call getCoordinates_v1 and expect to return a list of Coordinates
return rpcClient.exectureRpc("getCoordinates_v1", TypeUtils.parameterize(List.class, Coordinate.class));
// CALL => ${base_url}/rpc/getCoordinates_v1
}
public Coordinate getCoordinate(Point point){
// call findClosestCoordinate_v1 with body {x:?, y:?}
// expect to return a single coordinate
return rpcClient.executeRpc("findClosestCoordinate_v1", point, Coordinate.class);
// CALL => ${base_url}/rpc/findClosestCoordinate_v1
// => with body {x: point.x, y: point.y}
}
public SimpleCoordinate getCoordinateX(Point point){
// call findClosestCoordinate_v1 with body {x:?, y:?}
// add Criteria that add select=x,y and z=gte.0.0
// and return the result as a SimpleCoordinate class
return rpcClient.executeRpc("findClosestCoordinate_v1", new CoordinateCriteria(0.0), point, SimpleCoordinate.class);
// CALL => ${base_url}/rpc/findClosestCoordinate_v1?z=gte.0.0&select=x,y
// => with body {x: point.x, y: point.y}
}
@Select({"x", "y"})
record CoordinateCriteria(
@FilterField(key = "z", operation = FilterOperation.GTE.class)
private Float z
){}
record SimpleCoordinate(Float x, Float y){}
}
If you need help with the library please start a new thread QA / Issue on github
If you want to request a feature or report a bug, please create a GitHub Issue
If you want to make a contribution to the project, please create a PR
The QueryDSL is licensed under MIT License