JPA #Query with Complex nullable object - java

I'm building a repository with a getAll query that uses a complex type parameter to filter, like:
#Query("""
SELECT c FROM City c
WHERE :filter IS NULL OR c.code LIKE %:#{#filter.code}%
""")
fun getAllCitiesFiltered(#Param("filter") filter: MyFilter?) : List<City>
The MyFilter class is a simple POJO:
class MyFilter {
var code: String? = null
var description: String? = null
}
At some point in my code I call getAllCitiesFiltered(filter) and this filter may be null or may have one of it's properties set to null. I have two questions:
How can I deal with the fact that filter is nullable? The way it is now (like above), whenever a null value is passed to it I get an exception EL1007E: Property or field 'code' cannot be found on null
Is there a less ugly way in HQL to deal with the fact the the properties code and description may be null and when they are null I don't want to filter by them? The only way I think of now is doing things like ... WHERE filter.code IS NULL or filter.code LIKE %c.code%
I'm new to JPA and I'm not sure if using #Query is the best approach here. I'm open to suggestions to change that as well.
Thanks!
EDIT after Alan Hay suggestion
I'm using QueryDSL and, coming from C#/LINQ background, finding it great.
The "problem" is that I'm doing things like this:
val city = QCity.city
var query = JPAQuery<City>(entityManager).from(city)
if (filter?.code != null)
query = query.where(city.code.eq("BH"))
if (filter?.description != null)
query = query.where(city.description.eq("Belo Horizonte"))
Is there a better way of writing this other than switching and if/else'ing?
Thanks!

The best way is to use interface default method :
interface CityJpaRepository : JpaRepository<City, Int> {
fun findByCodeLike(code: String) : List<City>
fun findByFilter(filter: MyFilter?) : List<City> {
return filter?.code?.let { findByCodeLike(it.code) } ?: findAll()
}
}

Related

How to restrict a hibernate criteria to a specific root entity

I'm writing a dynamic query using hibernates criteria language. I'm stunned as I'm unable to find any information regarding restricting the root entity dynamically (i.e. without specifying the id property).
I have an interface IEntity. There are several entities implementing IEntity. Some of them have an ID-property id while others have another ID-property (shadowId).
I want to write a single method covering both cases. Here is what I got so far:
#Override
public boolean querySomething(final IEntity ent) {
final Criteria criteria =
currentSession().createCriteria(HibernateUtils.deproxy(ent.getClass()));
criteria.createAlias("sharedProperty", "prop");
//This does not work:
criteria.add(Property.forName("this").eq(ent));
criteria.setProjection(Projections.count("prop.anotherProperty"));
final Number result = (Number) criteria.uniqueResult();
return result != null && result.longValue() > 0;
}
I would like to avoid to have a if statement like
if (ent instanceof TypeWithPropertyId){
criteria.add(Property.forName("id").eq(ent));
} else {
criteria.add(Property.forName("shadowId").eq(ent));
}
Edit
Note: HibernateUtils.deproxy(Class<?> clazz) removes any Proxy and returns the original class.
As of now, I didn't find anything in the criteria api which could help me. But I found IdentifierEqExpression by accident. Sadly IdentifierEqExpression still needs the ID Value and can not extract it itself. So for the moment I'm using a custom extension:
public class ThisEqualsExpression extends IdentifierEqExpression {
public ThisEqualsExpression(final Object value, final SessionFactory sf) {
super(sf.getClassMetadata(deproxy(value.getClass())).getIdentifier(value, null));
}
}
I would prefer not using this hack-ish approach, but I still like this better than an if-statement:
public boolean querySomething(final IEntity ent) {
final Criteria criteria =
currentSession().createCriteria(HibernateUtils.deproxy(ent.getClass()));
criteria.createAlias("sharedProperty", "prop");
//This does work:
criteria.add(new ThisEqualsExpression(ent, currentSession().getSessionFactory()));
criteria.setProjection(Projections.count("prop.anotherProperty"));
final Number result = (Number) criteria.uniqueResult();
return result != null && result.longValue() > 0;
}
Edit 2:
As requested some clarification:
In pure SQL, i have something like
SELECT COUNT(bar.property)
FROM Foo foo
INNER JOIN Bar bar ON foo.bar_id=bar.id
But I want something like
SELECT COUNT(bar.property)
FROM Foo foo
INNER JOIN Bar bar ON foo.bar_id=bar.id
WHERE foo.id=<some ID> --This is the important part
Back to Java:
Foos ID-Property depends on its type. It may be id (which I used in the SQL example above), but it could also be some other property. I was wondering if there was a way to restrict a criterias root entity without knowing the propertys name.

How to return only specific fields for a query in Spring Data MongoDB?

How can we select specific fields in Spring Data Mongo. I tried the following but I got cast exception from Foo to String.
Using #Query
#Query(value="{path : ?0}", fields="{path : 0}")
String findPathByPath(String path);
Non #Query
String findPathByPath(String path);
Here is the document model
#Document(collection = "foo")
public class Foo {
String name, path;
…
}
MongoDB only returns JSON documents for standard queries. What you'd like to see can be achieved by still returning a List<Foo>. The fields property in #Query will cause only the fields set to 1 being returned.
#Query(value="{ path : ?0}", fields="{ path : 0 }")
List<Foo> findByPath(String path);
We usually recommend introducing a dedicted DTO for that so that you prevent the partially filled Foo instance from being handed to save(…) in turn.
Another option is using the aggreation framework but that's more involved.
You can use
Query query = new Query();
query.fields().include("path");
You can use
public interface PersonRepository extends MongoRepository<Person, String>
#Query(value="{ 'firstname' : ?0 }",fields="{ 'firstname' : 1, 'lastname' : 1}")
List<Person> findByThePersonsFirstname(String firstname);
}
More information in spring data documentation
You can use below query to get specific fields.
#Query(fields="{path : 1}")
Foo findPathByPath(String path);
Records present in DB
{
"name" : "name2",
"path" : "path2"
},
{
"name" : "name3",
"path" : "path3"
}
Below query will return Foo object if path=Path3
{
"name": null,
"path": "path3"
}
we need to specify required fields with fieldName:1 and if don't require then specify it with 0.
I found this question while trying to get the value of a field from a specific object in my collection. From what my research shows, Mongo doesn't provide a way to natively return just a specific field's value from an object. (Disappointing since it seems pretty basic to be able to return just a specific value from a field like I would do in SQL or JSONPath).
To get around this, I wrote the following method using Spring MongoDB with Java 11:
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.MongoTemplate; //not used, just showing which type the template is
import java.util.Arrays;
import static java.util.Objects.requireNonNull;
/**
* Use this method to get a specific field from an object saved in Mongo. The objectId will be
* the id of the object to fetch, and the fieldValueToReturn will be the field to return.
*
* #return the value of the provided field-path converted to the class type provided
*/
public <T> T getFieldValueById(String objectId, String fieldValueToReturn, String collectionName, Class<T> classTypeToReturn) {
var query = new Query().addCriteria(Criteria.where("_id").is(objectId));
query.fields().include(fieldValueToReturn);
var result = mongoTemplate.findOne(query, org.bson.Document.class, collectionName);
requireNonNull(result, "Did not find any documents with id '" + objectId + "' in collection: " + collectionName);
return result.getEmbedded(Arrays.asList(fieldValueToReturn.split("\\.")), classTypeToReturn);
}
The getEmbedded call allows us to get the value of the nested field within the returned Bson document.
To use the method, just call it like this:
getFieldValueById("A1234", "field.nestedfield.nestedfield", "collectionName", String.class);
Hopefully this helps out someone else looking on how to do this.
As a side note, I'm not sure how to extend this to return a list of objects - if I get to that dilemma and solve it, I will try to update this answer. I'm also not sure if this is slower than running a Mongo aggregate query - I haven't tried running any performance comparisons between the two methods.
EDIT 2022-09-30: To return a list of a custom Pojo, it looks like you'll have to use an aggregate query via spring-data-mongodb. Also it seems basic queries are faster than aggregate queries, so use basic queries where possible.
You can directly pass your json query with #Query annotation, for example:
#Query("{ 'firstname' : 'john' }")
Here is the link to all json based queries in Spring Data MongoDb - https://docs.spring.io/spring-data/mongodb/docs/current/reference/html/#mongodb.repositories.queries.json-based
You can do the following.
In your repository, you have the method:
String findPathByPath(String path);
If the document looks like this (below), and you want to only return path
#Document(collection = "foo")
public class Foo {
String name;
String path;
String type;
…
}
Then create a Projection interface, e.g.
#Projection(name = "flattenedFoo", types = Foo.class)
public interface FlattenedInvoice {
String getPath(); // This returns the path value in Foo class
}
You can use the getter methods to get the fields from Foo that you are interested in.
Then in your get request, you would have to specify the projectionName.
e.g. with (#Resource path)
#RestResource(path = "findByPath", rel = "findByPath")
String findPathByPath(String path);
You could then say (In a get request):
..../findByPath?path=target_path&projection=flattenedFoo
this would then return a json with only the fields specifies in FlattenedFoo interface.

Spring data JPA and parameters that can be null

My understanding is, that with Spring data JPA I cannot have a query method to fetch all rows where a column equals a given non-null method parameter and use the same method to fetch all rows where this column is NULL when the method parameter is null.
Is that correct?
So I have to distinguish this in my JAVA code and I must use a separate query method explicitly asking for null values, like in the example below?
// Query methods
List<Something> findByParameter(Parameter parameter);
List<Something> findByParameterIsNull();
...
List<Something> result = new ArrayList<>();
if (parameter == null)
result = findByParameterIsNull();
else
result = findByParameter(parameter);
That's bad, if I have 4 parameters which could be null and would have to code 16 different query methods.
You are right.
A request has been made to support better handling of null parameters.
https://jira.spring.io/browse/DATAJPA-121
In your case, i would advise you to write your repository implementation and to use a custom CriteriaQuery to handle your case.
Also you can use the #Query annotation with the is null syntax :
#Query("[...] where :parameter is null"
public List<Something> getSomethingWithNullParameter();
EDIT
Since Spring data jpa 2.0, spring now supports #Nullable annotation. This can be helpful to handle null parameters passed.
From the documentation :
#Nullable – to be used on a parameter or return value that can be null.
i found something...if u put the parameter in the jpa method like this
#Param("value") String value,
then it can be null and in the query you will have this condition:
(table.value = :value OR :value IS NULL)
if the value is null it will automatically return true and if is not null, it will search that value in the table.
It seems Query by Example might be what you need.
Query by Example is a new feature in Spring Data (since version Hopper, out April 2016), which allows one to create simple dynamic queries with a code like this
Person person = new Person();
person.setFirstname("Dave");
ExampleMatcher matcher = ExampleMatcher.matching()
.withIncludeNullValues();
Example<Person> example = Example.of(person, matcher);
personRepository.count(example);
personRepository.findOne(example);
personRepository.findAll(example);
Methods count/findOne/findAll that take an instance of org.springframework.data.domain.Example as a parameter (and some of them also take sorting/pagination parameters) are coming from org.springframework.data.repository.query.QueryByExampleExecutor<T> interface, which is extended by org.springframework.data.jpa.repository.JpaRepository<T, ID extends Serializable> interface.
In short, all JpaRepository instances now have these methods.
Today as of Jun 2018, by looking at https://jira.spring.io/browse/DATAJPA-121, the query will automatically form is null if your parameter is null.
I did that in my project, it is true:
compile group: 'org.springframework.data', name: 'spring-data-jpa', version: '2.0.7.RELEASE'
--
public interface AccountDao extends CrudRepository<T, ID> {
//this can accept null and it will become isNull
public List<MyAccount> findByEmail(String email);
}
if parameter is null:
select
myaccount0_.id as id1_0_,
myaccount0_.email as email2_0_,
myaccount0_.password as password3_0_,
myaccount0_.user_id as user_id4_0_
from
my_account myaccount0_
where
myaccount0_.email is null
if parameter is not null:
select
myaccount0_.id as id1_0_,
myaccount0_.email as email2_0_,
myaccount0_.password as password3_0_,
myaccount0_.user_id as user_id4_0_
from
my_account myaccount0_
where
myaccount0_.email=?
11:02:41.623 [qtp1507181879-72] TRACE o.h.type.descriptor.sql.BasicBinder - binding parameter [1] as [VARCHAR] - [testing#hotmail.com]
Then it comes to an interesting question, some developers want better control to ignore the parameter in query if it is null, this is still being under investigating in https://jira.spring.io/browse/DATAJPA-209.
In my case membershipNumber is nullable, and I have handled it this way. This will handle all the cases where table.membershipNumber is null too.
#Query(value = "SELECT pr FROM ABCTable pr " +
"WHERE LOWER(pr.xyz) = LOWER(:xyz) " +
"and LOWER(pr.subscriptionReference) = LOWER(:subscriptionReference) " +
"and pr.billId = :billId " +
"and ((pr.membershipNumber = :membershipId) or (pr.membershipNumber = null and :membershipId = null))")
List<PaymentRequest> getSomething (#Param("xyz") String xyz,
#Param("subscriptionReference") String subscriptionReference,
#Param("billId") Integer billId,
#Param("membershipId") String membershipNumber);
While this has been answered and the accepted answer is relevant to the current question but there is another way to handle your null parameters in a JpaRespository. Posting this here as this can be leveraged when someone wants to query by ignoring fields when null and have dynamic query built.
The below code sample should demonstrate the same
public class User{
private String firstName;
private String lastName;
}
import javax.persistence.criteria.Predicate;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User,Long>{
public Page<AppUser> findAll(Specification<AppUser> user,Pageable page);
public default Page<AppUser> findAll(User user,Pageable page){
return findAll(search(user),page);
}
static Specification<User> search(User entity) {
return (root, cq, cb) -> {
//To ensure we start with a predicate
Predicate predicate = cb.isTrue(cb.literal(true));
if(entity.getFirstName() != null && !entity.getFirstName().isBlank()) {
Predicate _predicate = cb.like(cb.lower(root.get("firstName")), "%"+entity.getFirstName().toLowerCase()+"%");
predicate = cb.and(predicate,_predicate);
}
if(entity.getLastName() != null && !entity.getLastName().isBlank()) {
Predicate _predicate = cb.like(cb.lower(root.get("lastName")), "%"+entity.getLastName().toLowerCase()+"%");
predicate = cb.and(predicate,_predicate);
}
return predicate;
}
}
}
I was able to apply IS NULL appropriately in case of null input using below workaround.
#Query("SELECT c FROM ConfigRLLOAContent c WHERE ((:suffixId IS NOT NULL AND c.suffixId = :suffixId) OR (:suffixId IS NULL AND c.suffixId IS NULL))")
Optional<ConfigRLLOAContent> findByRatableUnitId(#Param("suffixId") String suffixId);
Above approach will apply filters only when suffixId is non-null,
else, IS NULL filter will be applied.
There's also an issue raised on github, to which introduction of #NullMeans is proposed here.
I had the same issue with similar task - one parameter in the query was optional, so to get rid of this error, I managed to use the following query with 2 casts:
#Query(value = "select distinct name from table "
+ "where coalesce(cast(table.field_optional as text) = cast(?1 as text), true) "
+ "and lower(table.non_optional_field) like ?2 "
+ "limit ?3", nativeQuery = true)
List<String> method(String optionalParam, String param, int limit);
This coalesce part would transform into simple 'true' if optionalParam is null

Hibernate restriction in causes an error if the list is empty

If my list is empty, I get the following error:
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ')'
Below is my hibernate related method:
#Override
public List<SomeThing> findByIds(List<Integer> someIds) {
return sessionFactory.getCurrentSession().createCriteria(SomeClass.class)
.add(Restrictions.in("id", someIds))
.list();
}
What should I do to guard against this error?
I know I could short-circuit the call and return an empty list like:
if(someIds == null || someIds.size() == 0) {
return new List<SomeThing>();
}
But is there a more elegant way to do this?
I would say Hibernate needs to fix this issue, and give meaningful message.
I think its responsibility of the provider/hibernate to check for the empty/null List.
One can imagine the cause, it tries to construct where clause, something like id in (), somewhere in org.hibernate.loader.criteria.CriteriaQueryTranslator or similar..But because here the List is empty, it would be throwing an exception. But they already created query with ( and could not complete because of exception/empty List.
NO. If you execute the query with empty parameters for in clause, it will fail (you may verify this by running plain SQL). Better not to execute the query if the input param is null/empty.
Only thing I can advice is to use isEmpty() function and != null in if statement and little restructuring as:
#Override
public List<SomeThing> findByIds(List<Integer> someIds) {
List<Something> result = null; //you may initialize with empty list
if(someIds != null || !someIds.isEmpty() {
result = sessionFactory.getCurrentSession().createCriteria(SomeClass.class)
.add(Restrictions.in("id", someIds))
.list();
}
return result;
}
(This is mostly base on #Yogendra Singh's reply, with a twist to make it more adoptable to commonly-seen situation of multiple optional argument)
Criteria API aims to let you compose your query programmatically. Such kind of dynamic feature is expected to be handled in your code.
Normally we make optional criteria by this:
#Override
public List<SomeThing> findBySearchParams(SearchParam searchParam) {
// create criteria with mandatory search criteria
Criteria criteria = sessionFactory.getCurrentSession()
.createCriteria(SomeClass.class);
.add(Restriction("someField", searchParam.getSomeField()));
// add "id" only if "someId" contains value
if(searchParam.getSomeIds() != null && !searchParam.getSomeIds().empty()) {
criteria.add(Restrictions.in("id", searchParam.getSomeIds()));
}
// add "anotherField" only if "anOptionalField" is not null
if(searchParam.getAnOptionalField() != null) {
criteria.add(Restrictions.in("anotherField", searchParam.getAnOptionalField()));
}
return criteria.list();
}
Edit:
Although Hibernate does not (yet) provide a more elegant way for that, you can write something yourself to make it looks more elegant:
class SmartCriteriaBuilder {
private Criteria criteria;
SmartCriteriaBuilder (Criteria criteria) { this.criteria = criteria;}
SmartCriteriaBuilder in(String field, Collection values) {
if (!empty(values)) {
this.criteria.add(Restrictions.in(field,values));
}
}
// all other kind of restrictions ....
Criteria toCriteria() {
return this.criteria;
}
}
Then you can do something looks smarter:
SmartCriteriaBuilder criteriaBuilder =
new SmartCriteriaBuilder(sessionFactory.getCurrentSession().createCriteria());
criteriaBuilder .in("someField", listPossiblyNullOrEmpty);
return criteriaBuilder .toCriteria().list();

Find concrete class using a dynamic expression (where a Class instance is passed to a DAO)

I have this method signature:
public int nrOfEntities(Class<? extends MailConfirmation> clazz, User user, String email)
I would like nrOfEntities to return the number of entities that:
Are of the concrete class clazz
Have a matching User if user != null
Have a matching email if user == null
It's the class matching I'm having a problem with. I've tried a few statements without any luck.
Can clazz have subtypes that should not be counted?
If not, is it not sufficient to create the query on clazz?
Criteria criteria = session.createCriteria(clazz);
if (user == null) {
criteria.add(Restrictions.eq("email", email);
} else {
criteria.add(Restrictions.eq("user", user);
}
int result = (Integer) criteria.setProjection(Projections.rowCount()).uniqueResult();
Now I am guessing how your mapping looks (that there are "email" and "user" properties).
If that is not working, I know that there is a pseudo property named "class", at least in HQL. Maybe you can experiment with that.
Are you looking for "from " + clazz.getSimpleName() + " where ..."?
If you want to test the class of an object, you should be able to use something like the following:
Object entity = ... // Get the entity however
boolean matchesClass = entity.getClass().equals(clazz);
If this isn't working for you, give some examples of how it fails since it should be this straightforward!

Categories