Kotlin + Spring + JPA + Projections + Dynamic SQL Statement - spring

I've been successful in creating the following projection in combination with the #Query annotation:
Approach 1
interface Deployment{
val app: String
val platform: String
val org: String
val space: String
val instances: Int
val state: String
}
#Query(value="select a.name as app, p.name as platform, o.name as org, s.name as space, a.instances as instances, a.state as state " +
"from apps a " +
"inner join spaces s on a.space_id = s.id " +
"inner join orgs o on s.org_id = o.id " +
"inner join platforms p on o.platform_id = p.id " +
"where o.name = :eai " +
"and lower(a.name) = lower(:app) " +
"and lower(a.state) = lower(:state) " +
"order by p.name, s.name",
nativeQuery = true)
fun getDeploymentsByEaiAndAppAndState(eai: String, app: String?, state: String?): List<Deployment>
Approach 2
However, I need to dynamically create the SQL statement based on information supplied in a filter:
data class DeploymentFilter(
val eai: String
)
{
var app: String? = null
var state: String? = null
}
override fun getByFilter(filter: DeploymentFilter): List<Deployment> {
var sql =
"select a.name as app, p.name as platform, o.name as org, s.name as space, a.instances as instances, a.state as state " +
"from apps a " +
"inner join spaces s on a.space_id = s.id " +
"inner join orgs o on s.org_id = o.id " +
"inner join platforms p on o.platform_id = p.id " +
"where o.name = :eai"
if (!filter.app.isNullOrBlank()) { sql += " and lower(a.name) = lower(:app)" }
if (!filter.state.isNullOrBlank()) { sql += " and lower(a.state) = lower(:state)" }
sql += " order by p.name, s.name"
val query = em.createNativeQuery(sql)
query.setParameter("eai", filter.eai)
if (!filter.app.isNullOrBlank()) { query.setParameter("app", filter.app) }
if (!filter.state.isNullOrBlank()) { query.setParameter("state", filter.state) }
#Suppress("UNCHECKED_CAST")
return query.resultList as List<Deployment>
}
Although the code runs and executes, it's as if Spring (or Kotlin) is no longer automatically converting the Java List<Object[]> into a Kotlin List.
When I convert the List<Deployment> to JSON using Approach 1, I correctly see:
{
"eai": "6949",
"deployments": [
{
"app": "CisBidLine",
"platform": "edcbo1",
"instances": 3,
"org": "6949",
"state": "STARTED",
"space": "production"
},
...
However, with Approach 2, the Deployment data is treated as an array instead of an Object:
{
"eai": "6949",
"deployments": [
[
"CisPicture",
"edcbo1",
"6949",
"production",
1,
"STARTED"
],
...
There's no difference in the environment or logic between both approaches, other than one explicitly using #Query and the other em.createNativeQuery.
Any help would be greatly appreciated!
UPDATE
If I understand this correctly, Kotlin can effectively transform a Map to the Interface, as long as the keys of the Map match the properties of the Interface. Because query.resultList returns a List<Object[]>, the trick might be to transform that to a Map using the aliases as the keys. As I'm using Hibernate under the hood, I found the following worked:
query
.unwrap(org.hibernate.query.NativeQuery::class.java)
.setResultTransformer(org.hibernate.transform.Transformers.ALIAS_TO_ENTITY_MAP)
Now I can simply convert the Map to the Interface using:
#Suppress("UNCHECKED_CAST")
return query.resultList as List<Deployment>
For those not using Hibernate, I'm assuming one could manually convert the List to a Map first, then transform using as List<Deployment>.
I'm open to other approaches, but this seems to make reasonable sense!

Related

Spring JPA #Query : check if parameter is null before to use it - bad hibernate translation?

I am currently facing an issue with JPA Query.
I have a class Event and a class Race, an event has a list of races.
Event and Race have an attribute type Point from lib org.locationtech.jts.geom.Point to handle Geometry object of my PostgreSQL (extended with PostGis) database.
public class Event {
#OrderBy("rank")
#OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "event", orphanRemoval = true)
protected List<Race> races;
private Point locapoint;
...
}
public class Race {
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JsonIgnore
private Event event;
private boolean hasAddressEvent;
private Point locapoint; //null if hasAddressEvent is true
...
}
This query works fine in PGSQL (extended with Postgis) :
SELECT distinct e.* FROM Event e inner join Race r on e.id = r.event_id
where ((:radius is null or :point is null)
or (r.has_address_event = TRUE and st_distancesphere(:point, e.locapoint) < :radius)
or (r.has_address_event = FALSE and st_distancesphere(:point, r.locapoint) < :radius))
it returns all events that have at least one race located in the circle drawn with these two entries :point (example of value : ST_Point(3.2727,50.2788)) and :radius (integer that represents meters).
Problem is to use it in #Query JPA in my EventRepository :
#Query(value = "SELECT distinct e.* "
+ "FROM Event e inner join Race r on e.id = r.event_id " +
"where " +
"((:radius is null or :point is null) " +
"or (r.has_address_event = TRUE and st_distancesphere(:point, e.locapoint) < :radius) " +
"or (r.has_address_event = FALSE and st_distancesphere(:point, r.locapoint) < :radius))"
, nativeQuery = true)
Page<Event> search(Pageable page, #Param("point") Point point, #Param("radius") Integer radius);
It works only if point and radius params are not null.
For example if radius is null, I have this error :
PSQLException: ERROR: operator does not exist : double precision < bytea
It's like if conditions where not short-circuited : even if the first condition returns false it continues to treat the condition.
(:radius is null or :point is null)
With some research, I found this :
http://randomthoughtsonjavaprogramming.blogspot.com/2015/10/making-mistakes-in-jpql.html
This lets me think that the problem is my query is turning in SQL and completly changing my condition's structure.
I tried to do equivalent query in JPQL (instead of native PGSQL) and it's the same result.
How can I fix it or just workaround.
Sorry for my english
Thanks in advance,
Thomas
EDIT :
As suggest #hqrd, I can do the null condition treatment inside my service and use two differents queries :
default Page<Event> search(Pageable page, Point point, Integer radius) {
if (point == null || radius == null) {
return findAll(page);
} else {
return searchAround(page, point, radius);
}
}
It will be a good solution for the example I introduced in my post.
But this query is just a POC, my final query will be more sophisticated (with more conditions, with 6 parameters that can potentially be null).
I don't know how to solve this in the #Query, but I suggest you split your issue using java.
#Query(value = "SELECT distinct e.* FROM Event e inner join Race r on e.id = r.event_id " +
"where (r.has_address_event = TRUE and st_distancesphere(:point, e.locapoint) < :radius) " +
"or (r.has_address_event = FALSE and st_distancesphere(:point, r.locapoint) < :radius))"
, nativeQuery = true)
Page<Event> searchAround(Pageable page, #Param("point") Point point, #Param("radius") Integer radius);
default Page<Event> search(Pageable page, Point point, Integer radius) {
if (point == null || radius == null) {
return findAll(page);
} else {
return searchAround(page, point, radius);
}
}
This way you have more control on your requests.

Adding parameters optionally to Spring Data JPA Native query

I am using Spring Data JPA with native queries like below
public interface ItemRepository extends JpaRepository<ItemEntity, Long> {
#Query(value = "select * from items i where i.category = :itemCategory and i.item_code = :itemCode", nativeQuery = true)
Page<ItemEntity> search(#Param("itemCategory") String itemCategory, #Param("itemCode") String itemCode, Pageable pageable);
}
Now, my use case is
It itemCode is available, only the item with that code from that category should be returned.
But if itemCode is not available then all items in that category should be returned.
So the problem with the above category is when itemCode is passed as NULL no data is returned since it does not match anything. whereas the requirement is it should be ignored.
So is there a way to optionally add a clause to Spring Data JPA native query. I know it is possible with CriteriaQuery but can we do something similar for native queries?
Thanks
Yes, it's feasible with native query too. Very Good explanation here read this
#Approach1
#NamedQuery(name = "getUser", query = "select u from User u"
+ " where (:id is null or u.id = :id)"
+ " And :username"
:itemCode is null or i.item_code = :itemCode
#Approach2
# UserDao.java
public User getUser(Long id, String usename) {
String getUser = "select u from user u where u.id " + Dao.isNull(id)
+ " And u.username " + Dao.isNull(username);
Query query = Dao.entityManager.createQuery(getUser);
}
# Dao.java
public static String isNull(Object field) {
if (field != null) {
if (field instanceof String) {
return " = " + "'" + field + "'";
} else {
return " = " + field;
}
} else {
return " is NULL ";
}
}
How to handle null value of number type in JPA named query
Just modify the where condition
i.item_code = :itemCode
to
:itemCode is null or i.item_code = :itemCode

Spring Data JPA Query by Example with access to nested Objects Attributes

I use Query by Example and want to know how I can find objects with certain properties in the nested objects.
A plan anyone?
Here is my example Code:
ExampleMatcher matcher = ExampleMatcher.matching()
.withMatcher("offer2product.id.productId", match -> match.exact()
);
Offer2ProductId id = new Offer2ProductId();
id.setProductId(1337L);
Offer2Product offer2Product = new Offer2Product();
offer2Product.setId(id);
Set<Offer2Product> offer2productSet = new HashSet<>();
offer2productSet.add(offer2Product);
Offer probe = new Offer();
probe.setOffer2productSet(offer2productSet);
Example<Offer> example = Example.of(probe, matcher);
List<Offer> offerList = offerRepository.findAll(example);
Quoting Spring data documentation: https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#query-by-example
Currently, only SingularAttribute properties can be used for property matching.
In your example, you want to search by a property that is a Set<> (offer2productSet), which is a PluralAttribute - it is not possible to search by this field. It will be ignored when building a query, as can be seen here:
https://github.com/spring-projects/spring-data-jpa/blob/master/src/main/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilder.java#L112
man actually can do it, follow the example below, where demand has a list of labels, I don't have time to explain everything but soon I'll update this post.
#Repository
#RequiredArgsConstructor
public class DemandaFilterRepository {
private final EntityManager entityManager;
public Page<Demanda> findAll(DemandaFilterDTO demandaFilter, Pageable pageable) {
String query = "select d from Demanda d where 1=1";
Map<String, Object> parameters = new HashMap<>();
if (demandaFilter.getId() != null) {
query += " and d.id = :id";
parameters.put("id", demandaFilter.getId());
}
if (!StringUtils.isEmpty(demandaFilter.getTenant())) {
query += " and upper(d.tenant) = upper(:tenant)";
parameters.put("tenant", demandaFilter.getTenant());
}
if (!StringUtils.isEmpty(demandaFilter.getAssunto())) {
query += " and upper(d.assunto) like upper(:assunto)";
parameters.put("assunto", "%" + demandaFilter.getAssunto() + "%");
}
if (!StringUtils.isEmpty(demandaFilter.getDescricao())) {
query += " and upper(d.descricao) like upper(:descricao)";
parameters.put("descricao", "%" + demandaFilter.getDescricao() + "%");
}
if (!StringUtils.isEmpty(demandaFilter.getEtiqueta())) {
query = query.replace("Demanda d", "Demanda d inner join d.etiquetas etiqueta");
query += " and upper(etiqueta.descricao) = upper(:etiqueta)";
parameters.put("etiqueta", demandaFilter.getEtiqueta());
}
if (!StringUtils.isEmpty(demandaFilter.getNomeDemandante())) {
query += " and upper(d.demandante.nome) like upper(:nomeDemandante)";
parameters.put("nomeDemandante", "%" + demandaFilter.getNomeDemandante() + "%" );
}
if (!StringUtils.isEmpty(demandaFilter.getDtInclusao())) {
query += " d.dtInclusao like :dtInclusao";
parameters.put("dtInclusao", "%" + demandaFilter.getDtInclusao() + "%");
}
query += " ORDER BY d.id DESC ";
TypedQuery<Demanda> typedQuery = entityManager.createQuery(query, Demanda.class)
.setMaxResults(pageable.getPageSize())
.setFirstResult(pageable.getPageNumber() * pageable.getPageSize());
parameters.forEach(typedQuery::setParameter);
return new PageImpl<>(typedQuery.getResultList());
}
}
this is a class for one of a small project that I'm working on ...

Database column data having linebreaks are ignored when retrieved from Hibernate?

A CLOB database column data has linebreaks :
When I retrieve it and display the data inside a html table cell then the linebreaks are ignored :
#Override
#Transactional
public String getPrevisionRessourceAutreForProduit(Objectif produit) {
String hql = "select to_char(r.ress_comment_1) " +
"from ressource r join type_ressource t on r.type_ress_code = t.type_ress_code " +
"left join objectif o on r.obj_code = o.obj_code " +
"where o.obj_code = " + produit.getCode().toString() + " and upper(t.type_ress_code) = 'AUT'";
Session sessionDynamic = Utils.createDynamicSession(env);
Query query = sessionDynamic.createSQLQuery(hql);
#SuppressWarnings("unchecked")
List<String> list = (List<String>) query.list();
sessionDynamic.close();
if (list.isEmpty())
return "";
else
return list.get(0) == null ? "" : list.get(0);
}
So how to fix it ?
I found the solution by enclosing the data inside the tags <pre>...</pre>

LINQ & Entity Framework : issue with concatenating NULL value

I have an INT fields called TradeNo that may contain NULL value. The requirement is to display on the dropdown the "contract Id" and the "trade no" in bracket and if "trade no" is null, then display N/A.
Example:
44444 (2222)
55555 ( N/A )
Here’s what I thought would work. This is my functions that returns a SelectList
public IEnumerable<SelectListItem> GetContractsBySupplierDropDown(string supplier)
{
var result = from c in context.Contracts
where c.Supplier==supplier
orderby c.ContractID
select new {
Text = c.ContractID.ToString() + " (" +
(c.TradeNo.HasValue ?
c.TradeNo.Value.ToString() :
" N/A ").ToString() +
" )",
Value = c.ContractID };
return new SelectList(result, "Text", "Value");
}
The error being return is:
LINQ to Entities does not recognize the method 'System.String ToString()'
method, and this method cannot be translated into a store expression.
From what I can tell, the error displayed means that EF is trying to convert ToString to a database function?
Unfortunately you cannot use ToString in a number field coming from the database.
As a work around you can cast ToList before doing it, so then the contents are in the memory.
public IEnumerable<SelectListItem> GetContractsBySupplierDropDown(string supplier)
{
var query = from c in context.Contracts
where c.Supplier==supplier
orderby c.ContractID
select new {
c.ContractID,
c.TradeNo,
};
var result = from c in query.ToList()
select new {
Text = c.ContractID.ToString() + " (" +
(c.TradeNo.HasValue ?
c.TradeNo.Value.ToString() :
" N/A ") +
" )",
Value = c.ContractID
};
return new SelectList(result, "Text", "Value");
}

Resources