Handling a REST API request with many optional query params in Spring - java

For the sake of simplicity lets assume I have a Document object with seven fields (but imagine that it can have many more). This object looks something like this:
#Getter
#Setter
public class Document {
private String fileName;
private String fileType;
private String createdBy;
private Date createdAt;
private Date lastModifiedAt;
private List<String> modifiers;
private Long timesModified;
}
I want to create an endpoint which can receive any number of #RequestParam and returns a List<Document> of all the documents which match the given query. For example: return all documents with fileType == doc, which were created between createdAt == 01/01/2021 && createdAt 31/01/2021, modified timesModified == 5 times and modifiers.contains("Alex"). The reason for this is that I want to allow the user to query for documents depending on combination of fields the user wants. Originally to handle this we created the endpoint like so:
#GetMapping(value = {RestApi.LIST})
public ResponseEntity<List<Document>> getDocuments (#RequestParam Map<String, Object> optionalFilters) {
List<Document> documents = documentService.getListOfDocuments(optionalFilters);
if (documents != null) {
return new ResponseEntity<>(documents, HttpStatus.OK);
}
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
The problem with this is that because we use optionalFilters as Map<String, Object> this requires us to perform a lot of casting in our code and overall makes our code very tedious and cumbersome because we have to iterate through the whole map and create a custom query depending the fields that were passed. In order to try and improve this I created an OptionalFilters object:
#Getter
#Setter
#NoArgsConstructor
public class OptionalFilters {
private String fileName;
private String fileType;
private String createdBy;
private Date createdAt;
private Date lastModifiedAt;
private List<String> modifiers;
private Long timesModified;
}
And modified the endpoint to this:
#GetMapping(value = {RestApi.LIST})
public ResponseEntity<List<Document>> getDocuments (#Valid OptionalFilters optionalFilters) {
List<Document> documents = documentService.getListOfDocuments(optionalFilters);
if (documents != null) {
return new ResponseEntity<>(documents, HttpStatus.OK);
}
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
However, although that this simplifies the way we receive the parameters and extract the values from them, we still need to iterate through all the parameters and create a custom query. Is there some way to elevate and take advantage of Spring-Data (or any other solution) so that I don't have to create a custom query depending on each query param that is passed through? I am using Solr as the repository if this may be any help.

Using Query by Example is one the most simple option but it has its limitations. Excerpt from the above link:
Limitations
Like all things, the Query by Example API has some limitations. For instance:
Nesting and grouping statements are not supported, for example:
(firstName = ?0 and lastName = ?1) or seatNumber = ?2
String matching only includes exact, case-insensitive, starts, ends, contains, and regex
All types other than String are exact-match only
Query by Example is suitable choice if your filtering is never too complicated. But when restirictions like above hit the fan of your CPU cooler the choice is to use Specifications to construct queries.
One big difference is also that Using Query by Example you need to explicitly populate the example by its getters and setters. With specification you can make it in a generic way (with Java generics) using just use field names
In your case you could just pass the map to generic method and create filtering by just looping and adding by and (note that the link's example has static stuff mostly but it has not to be, you just need field name/criterion -pair to loop it in a generic way)
With specifications you can do anything that can be done with Query by Example and almost anything else also. The overhead to get familiar with specifications might be bigger but the advantage using specifications will be rewarding.
In a nutshell:
Spring interface Specification is based on JPA CriteriaQuery and for each you need only to implement one method:
Predicate toPredicate (Root<User> root, CriteriaQuery<?> query, CriteriaBuilder builder);
Repository interfaces needs just to extend JpaSpecificationExecutor<YourClass> When you have a set of predicates, you can - for example -
repository.findAll(Specification.where(spec1).and(spec2));
It might seem complicated or difficult at start but it is not that at all. The greatest advantage with Specification is that you can do almost anything programmatically instead of manipulating JPQL queries or so.

Related

How to build dynamic queries with Spring Data Redis Repositories?

I'm testing Redis with spring-data-redis using repositories like this:
public interface CreditCardRepository extends CrudRepository<CreditCard, String>{
List<CreditCard> findByIssuer(String issuer);
List<CreditCard> findByCreditNetwork(String creditNetwork);
List<CreditCard> findByCreditNetworkAndIssuer(String creditNetwork, String issuer);
}
Above methods will query over Redis structures like:
creditcard:creditNetwork:mastercard
creditcard:creditNetwork:visa
creditcard:issuer:company1
creditcard:issuer:company2
Right now my CreditCard object contains two attributes (issuer, network and the id), so it's easy to search objects like this:
private List<CreditCard> searchCardFromCache(CreditCardGetReq req) {
if (req.getIssuer() != null && req.getNetwork() != null) {
return ccRepository.findByIssuerAndCreditNetwork(req.getIssuer(), req.getNetwork().name());
}
if (req.getIssuer() != null) {
return ccRepository.findByIssuer(req.getIssuer());
}
if (req.getNetwork() != null) {
return ccRepository.findByCreditNetwork(req.getNetwork().name());
}
return null;
}
However, I don't like this code since I will have to create a combination of all the properties and will be very messy. In the future, I plan to have 15 properties so the 'if' chain is not possible.
I would like to ask you how can I create dynamic queries using spring-data-redis, so Redis can return the intersection based on the object properties in a better way than checking each property?
Have tried using MethodHandle by hardcoding (I previously deleted from the repository findByIssuerAndCreditNetwork) a method name that would be dynamic generated like this:
MethodType methodType = MethodType.methodType(cardList.getClass(), String.class, String.class);
// Dynamic create 'findByIssuerAndCreditNetwork'
MethodHandle methodHandle = MethodHandles.lookup().findVirtual(CreditCardRepository.class, "findByIssuerAndCreditNetwork", methodType);
But seems MethodHandle does not work since I got below error:
java.lang.NoSuchMethodException: no such method: com.creditcard.dao.CreditCardRepository.findByIssuerAndCreditNetwork(String,String)ArrayList/invokeInterface
Right now, there's no support to create dynamic queries. It sounds a bit as if Query by Example could be the thing you're looking for. Spring Data MongoDB and Spring Data JPA already implement Query by Example.
A query is created by the data store module to match an example domain object:
Person person = new Person();
person.setFirstname("Dave");
Example<Person> example = Example.of(person);
MongoRepository repo = …
List<Person> result = repo.findAll(example); // returns all objects that with Dave in firstname
Query by Example is not supported by Spring Data Redis right now but it should be possible to provide basic support.
I created a ticket DATAREDIS-605 to track the progress of this feature.

How to implement lazy fetching a list combined with a conditious get for one element of that list?

We're setting up a DB with an object model for safes that have bill readers inside to put the bills in. This safe is stored in the Unit class and that unit has a list of placeholders for, let's say, 3 components. Components can change over time, but placeholders will stay connected to the same safe. So this unit has 3 placeholders with identifiers like 'left reader', 'right reader' and 'printer'. The actual component inside such a placeholder has the product type (for instance Mei reader ... or JCM reader ...) and a serial number.
Now, as the unit itself will be requested from the server side quite often as it is shown in one or more (pages of) portals (to see it's own serial number, location, how much money is inside, which users are allowed to log on to this safe, etc.) and components need not be known most of the time we want to lazily load them. The unit itself needs to know it's actual components (configuration) with serial numbers for components to be able to know / tell the back end when a component is exchanged during a servicing by a mechanic.
There is an intermediate table (placeholdercomponents) with placeholder FK, component FK and a datetime placed and a datetime removed.
Right now there is a Unit class that contains, besides a lot else, this:
#OneToMany(mappedBy = "unit", fetch = FetchType.EAGER)
private List<Placeholder> placeholders = new ArrayList<>();
#XmlElementWrapper(name = "Placeholders")
#XmlElement(name = "Placeholder")
public final List<Placeholder> getPlaceholders() {
return placeholders;
}
public final void setPlaceholders(List<Placeholder> pPlaceholders) {
placeholders = pPlaceholders;
}
The placeholder class has this:
#OneToMany(mappedBy = "placeholder", fetch = FetchType.LAZY)
private List<PlaceholderComponent> placeholderComponents;
#XmlTransient
public List<PlaceholderComponent> getPlaceholderComponents() {
if (placeholderComponents == null)
placeholderComponents = new ArrayList<PlaceholderComponent>();
return placeholderComponents;
}
public void setOrganizationUnits(List<PlaceholderComponent> pPlaceholderComponents) {
placeholderComponents = pPlaceholderComponents;
}
#XmlElement(name = "Component")
public Component getCurrentComponent() {
if (placeholderComponents == null) {
return null;
} else {
PlaceholderComponent placeComp = placeholderComponents.stream()
.filter(pc -> pc.getDateTimeRemoved() == null)
.findFirst()
.orElse(null);
if (placeComp == null) {
return null;
} else {
return placeComp.getComponent();
}
}
}
And in the service there should be two possible calls getUnit & getUnitWithComponents. Now, if I remove the #XmlElement(name = "Component") annotation above getCurrentComponent() and do nothing with the placeholderComponents inside the getUnit call it works as expected, but getUnitWithComponents doesn't work (it works, but doesn't return the components inside the placeholders). When I leave the annotation getUnitWithComponents works as expected, but getUnit gives an error about not being able to lazily load the placeholderComponents as, apparently, JAXB wants to build the Component element even though the whole list isn't called in the service and therefore not loaded.
I can think of 3 possible 'solutions' working around this:
Remove the getCurrentComponent and make a separate call for retrieving the current component in a placeholder which would send a single component XML back
Make two different objects for Placeholder while both are for the same DB-table, using the one for getUnit (where the Component annotation isn't present) and the other for getUnitWithComponents (where the Component annotation is present).
Another option would be splitting into DAO's and DTO's as there might be more of those situations coming, but that splitting would take quite some time by now.
So the main question is: is there a way to get the components inside the unit XML for some calls and leave them out for most of the calls without a workaround? And if not, what solution would you chose? 1, 2, 3 or another one?

Filter collection by multiple attributes of its elements - QueryDSL

I am working on a dynamic filter component based on QueryDSL with the use of SpringData for query execution. Thus I create Predicate instances from the received data ad pass it to QueryDslPredicateExecutor. For dynamic access to entity attributes I use generic PathBuilder typed to the entity class.
Consider the following (simplified) code:
class Offer {
List<LanguageToName> names;
}
class LanguageToName {
String name;
String language;
}
When I try to query Offer entites, that have in their collection name element with attribute 'abc', I simply create the predicate as follows:
pathBuilder.getCollection("names", LanguageToName.class).any().getString("name")
.like("%" + fieldData.getFieldValue() + "%");
However, I was unable to come up with a solution to filter the collection by multiple attributes of the containing objects with the use of PathBuilder. When I append the code above with .and() and access the collection again via the pathBuilder variable, I naturally get the result equivalent to appending sql query with AND EXISTS..., which is not the desired result. I also tried to use getCollection().contains(), but I was unable to create the Expression<LanguageToName> that would describe such case.
Is there a way to create a Predicate that would filter entities by multiple attributes of the elements from a collection, that is a field of the queried entity?
I had similar issue and finally solved this with subquery (however, it seems to me that it works only for 1 level of nestedness).
My initial predicate was (it was making 2 independent sub-queries):
Predicate predicate = codeTable.customer.id.eq(customerId)
.and(codeTable.qualifierResults.any().customerQualifier.type.eq("TARGET_TYPE"))
.and(codeTable.qualifierResults.any().customerQualifier.referenceType.code.eq("TARGET_CODE"));
But the correct predicate that I ended up with was:
BooleanExpression customerQualifierCondition = JPAExpressions
.selectFrom(codeTableQualifierResult)
.where(codeTableQualifierResult.in(codeTable.qualifierResults),
codeTableQualifierResult.customerQualifier.type.eq("TARGET_TYPE"),
codeTableQualifierResult.customerQualifier.referenceType.code.eq("TARGET_CODE"))
.exists();
Predicate predicate = codeTable.customer.id.eq(customerId).and(customerQualifierCondition);
The idea is to write 1 separate sub-query where you apply all necessary conditions at once (instead of applying them for your collection independently).
I ran across the same problem in my project.
My workaround is to build the exists subquery manually.
Assuming that your both classes are mapped as Entities:
#Entity
#Table(name = "Offer")
public class Offer {
#Id
String id;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "offer")
List<LanguageToName> names;
}
#Entity
#Table(schema = "dcsdba", name = "Language_To_Name")
public class LanguageToName {
#Id
String id;
#ManyToOne(fetch= FetchType.LAZY)
#JoinColumn(name="Offer_id")
private Offer offer;
String name;
String language;
}
A simple query with any():
BooleanExpression namesFilter = QOffer.offer.names.any().name.eq("Esperanto");
maps to
select
offer0_.id as id1_7_
from
offer offer0_
where
exists (
select
1
from
dcsdba.language_to_name names1_
where
offer0_.id=names1_.offer_id
and names1_.name=?
)
A subquery:
BooleanExpression namesFilter = JPAExpressions.selectOne()
.from(languageToName)
.where(languageToName.offer.eq(QOffer.offer)
.and(languageToName.name.eq("Esperanto")))
.exists();
Maps to:
select
offer0_.id as id1_7_
from
offer offer0_
where
exists (
select
1
from
dcsdba.language_to_name languageto1_
where
languageto1_.offer_id=offer0_.id
and languageto1_.name=?
)
which matches perfectly previous SQL.
You can add additional conditions like:
BooleanExpression namesFilter = JPAExpressions.selectOne()
.from(languageToName)
.where(languageToName.offer.eq(QOffer.offer)
.and(languageToName.name.eq("Esperanto"))
.and(languageToName.language.like("E%")))
.exists();

How to save List in SQL database?

My entity class contains List of primefaces.model.map.Marker:
#Entity
#Table(name = "appuser")
public class UserEntity extends BaseEntity {
private List<Marker> places = new ArrayList<Marker>();
public List<Marker> getPlaces() {
return places;
}
}
Also, I have a sql file that describes my table. I know how to save String, double and other primitive types. But I don't know how to save arrays, collections of primitive types or collections with objects of my own type.
Could you help me with this, saving a list to Oracle DB?
I know that there exist different approaches to do this: save the list as a single string, as an object...
P.S.: It would be great if you could give me a reference to a site or book that describes the interaction between Java and SQL.
The usual DB representation of Lists is a separate table. Read about JPA's #OneToMany Annotation.
A good source for information on this topic is the JPA2 Specification (full of examples, but I've heard some people say it's hard to read / boring)
As Duckstep allready said, the usual
The usual DB representation of Lists is a separate table.
If you do not want to create a OtM connection in your database, you could create a helper class which puts the list in a String and splits the string into a list like the following:
public class Helper {
public String stringify(List<Marker> l) {
String rs = "";
for (Marker marker : l) {
rs = rs + ',' + marker.toString();
}
rs.substring(1);
return rs;
}
public List<Marker> makeList(String rs){
List<Marker> rl = new LinkedList<Marker>();
String[] a = rs.split(",");
for (String string : a) {
Marker rm = new Marker();
// I don't know what class of marker you use,
//but here you should create the marker from the string
rl.add(rm);
}
return rl;
}
}
But seriously! Its better to use a OtM table, as you are totally filling your Database with useless repetitions.

What is the best way to read nested object from a join query

Assume I have a model like following
class Chest {
public Id id;
public List<Drawer> drawers;
public Price price;
}
class Drawer {
public Id id;
public Price price;
}
And a JOOQ query to fetch a Chest object with its Drawers:
dsl.selectFrom(CHEST.join(DRAWERS).onKey()).where(CHEST.ID.eq(1)).fetch()
What is the best way to construct the Chest object from the result of the query above?
Thanks.
In general, using JOIN to materialise object graphs won't really work well, as you're denormalising your database entities into a table (with duplicates) before you try to normalise the data again in a mapping algorithm. JPA hides these things from you by offering an alternative query language that doesn't expose so many SQL features.
In your particular case, however, you can get this to run via the jOOQ API by using the Result.intoGroups() methods. Thus:
Map<Record, Result<Record>> result =
dsl.selectFrom(...).fetch().intoGroups(CHEST.fields());
List<Chest> list = new ArrayList<>();
for (Entry<Record, Result<Record>> entry : result.entrySet()) {
Record chest = entry.getKey();
Result<Record> drawers = entry.getValue();
list.add(new Chest(
chest.into(Id.class), // These into(Class<?>) methods assume that you
drawers.into(Drawer.class) // want to use jOOQ's DefaultRecordMapper
));
}
The above algorithm is probably incomplete, or not exactly what you need. But it'll give you a general idea of what's possible out-of-the-box via jOOQ API.

Categories