The #SqlResultSetMapping usage cause the schema validation problem - spring

I have to extract data for statistic purpose. I've created a native query and used #SqlResultSetMapping to map the resultset to an object.
Hibernate needs to declare this class (Elaboration) as #Entity BUT IS NOT A TABLE, and I don't want a table because I have only to extract data on the fly when needed.
The code works fine but the gitlab pipeline fails during validation with
schemaManagementException: Schema-validation: missing table [elaboration].
Here my code so far:
SqlResultSetMapping(name="ValueMapping",
classes={
#ConstructorResult(
targetClass=Elaboration.class,
columns={
#ColumnResult(name="areadesc", type=String.class),
#ColumnResult(name="subsectordesc", type=String.class),
#ColumnResult(name="eurovalue", type=BigDecimal.class),
#ColumnResult(name="eurotch", type=BigDecimal.class),
}
)
})
#Entity
public class Elaboration{
#Id
private Long id;
private String areadesc;
private String subsectordesc;
private Integer dossiercount;
private BigDecimal eurovalue;
private BigDecimal eurotch;
....
and the custom query:
String statisticValueQuery = "select a.mdescr as areadesc, s.mdescr as subsectordesc, sum(euro_value) as eurovalue,
sum(euro_value_tch) as eurotch " +
"from dossier d " +
"join dossier_document dd on d.id = dd.dossier_id " +
"join dossier_country dc on d.id = dc.dossier_id " +
"join country c on dc.country_id = c.id " +
"join area a on c.area_id = a.id " +
"join dossier_subsector ds on d.id = ds.dossier_id " +
"join subsector s on ds.subsector_id = s.id " +
"where dd.document_id = :document " +
"and d.submission_date >= :startdate and d.submission_date <= :enddate " +
"group by s.id, a.id;";
public List<Elaboration> getValueElaboration(ElaborationRequestDTO elaborationRequestDTO){
Query resultMapping = em.createNativeQuery(statisticValueQuery, "ValueMapping");
resultMapping.setParameter("startdate", elaborationRequestDTO.getElaborateFromEquals());
resultMapping.setParameter("enddate", elaborationRequestDTO.getElaborateToEquals());
resultMapping.setParameter("document", elaborationRequestDTO.getDocumentIdEquals());
return resultMapping.getResultList();
Is there a way to pass the validation test?
Thanks

This is wrong statement.
Hibernate needs to declare this class (Elaboration) as #Entity
You should just put your #SqlResultSetMapping declaration above some #Entity but it can be some other entity not related to the Elaboration.
#SqlResultSetMapping(name="ValueMapping",
classes={
#ConstructorResult(
targetClass=Elaboration.class,
columns={
#ColumnResult(name="areadesc", type=String.class),
#ColumnResult(name="subsectordesc", type=String.class),
#ColumnResult(name="eurovalue", type=BigDecimal.class),
#ColumnResult(name="eurotch", type=BigDecimal.class),
}
)
})
#Entity
public class SomeEntity {
}
And if Elaboration is not an entity you should not annotate it as such.

Related

How to make JPA use a single join to get columns with conditions for both sides

My problem is the following,
There are two entity classes, let's call them Entity1 and Entity2 with One-to-Many relationship in between, i.e. one Entity1 contains multiple Entity2s, and Entity2 may have only one Entity1:
#Entity
#Table(name = "entity1")
public class Entity1 {
int x;
int y;
...
#LazyCollection(LazyCollectionOption.TRUE)
#OneToMany(mappedBy = "e1", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Entity2> entity2s = new HashSet<>();
}
#Entity
#Table(name = "entity2")
public class Entity2 {
int a;
int b;
...
#ManyToOne
#JoinColumn(name = "entity1_id")
#JsonBackReference
private Entity1 e1;
}
Now I would like to issue a query for retrieving Entity2s with WHERE conditions for both Entity2 and its corresponding Entity1:
#Query("SELECT " +
" e2 " +
"FROM " +
" Entity2 e2 " +
"WHERE " +
" e2.a = '<val1>' AND e2.b = '<val2>' AND e2.e1.x = '<val3>' AND e2.e1.y ='<val4>'")
List<Entity2> findMyEntity2s(
#Param...,
#Param...,
);
So the problem with this approach is that, it indeed gets desired Entity2s by cross joining entity1 and entity2 tables with specified WHERE conditions BUT it fetches e1s for each of those Entity2s in the result with a separate query.
So for example if the result of join is 5 Entity2s, there will be 5 additional queries to entity1 table.
I tried to set #ManyToOne in Entity2 as #ManyToOne(fetch = FetchType.LAZY) but it didn't help. I guess that's expected because LAZY would simply postpone the retrieval of e1s but wouldn't eliminate it completely.
Next, I read about #EntityGraph, and added it to Entity2:
#Entity
#Table(name = "entity2")
#NamedEntityGraph(name = "graph.entity2.entity1",
attributeNodes = { #NamedAttributeNode("e1") })
public class Entity2 {
int a;
int b;
...
#ManyToOne
#JoinColumn(name = "entity1_id")
#JsonBackReference
private Entity1 e1;
}
and in the repository, I added it as:
#EntityGraph(value = "graph.entity2.entity1")
#Query("SELECT " +
" e2 " +
"FROM " +
" Entity2 e2 " +
"WHERE " +
" e2.a = '<val1>' AND e2.b = '<val2>' AND e2.e1.x = '<val3>' AND e2.e1.y ='<val4>'")
List<Entity2> findMyEntity2s(
#Param...,
#Param...,
);
In this case, the separate SQL queries disappear, EntityGraph does left join and its result contains columns from both entity1 and entity2, BUT because the conditions for e2.e1 are still in WHERE clause, it adds ONE MORE unnecessary cross join with entity1 table (e2.e1 conditions are checked in that cross join).
I couldn't find a way to get rid of that extra cross join, so now I'm using the following query:
#EntityGraph(value = "graph.entity2.entity1")
#Query("SELECT " +
" e2 " +
"FROM " +
" Entity2 e2 " +
"WHERE " +
" e2.a = '<val1>' AND e2.b = '<val2>'")
List<Entity2> findMyEntity2s(
#Param...,
#Param...,
);
So basically I get Entity2s and in the application I filter out based on conditions of Entity1 (e2.e1.x = '<val3>' AND e2.e1.y ='<val4>').
Is there a way to make it work with a single join only, for both entity's conditions, not only Entity2 conditions? The way I'm doing it now, does not seem correct and efficient to me, and I feel there's a way to do that using repository method only, without involving the app. Would appreciate any help on this
UPD. Read about nativeQuery option (nativeQuery = true) for #Query annotation, which allows specifying a raw query and thus bypassing entity-based query, but the query still fetches many-to-one e1 field, using entity1_id (entity graph was disabled). I tried to enable entity graph but it dropped exception stating that entity graph cannot be used with native query, which is expected
This is the classic n + 1 query problem.
You can read the detail here: https://vladmihalcea.com/n-plus-1-query-problem/
In your query, append:
LEFT JOIN FETCH e2.e1 e2e1
This will fetch e1 with e2 in the first and single query.
Don't forget; always use FetchType.LAZY and fetch your entities with JOIN FETCH. Otherwise, you will get into a big mass while the scope of the project enlarges.
In addition, why do you use Jaxson annotations in your Entity classes? Use entities for only DAO access and map them to another DTOs to use elsewhere.

search collection by index in JPA #query

I have 2 entities with OneToMany Relation as below,
public class Visit{
#OneToMany(mappedBy = "visit", fetch = FetchType.LAZY)
#OrderBy("updated_on DESC")
private List<StatusChange> statusHistory;
}
public class StatusChange{
#ManyToOne
#JoinColumn(name = "ms_visit_id")
private Visit visit;
#Column(name = "to_status")
#Enumerated(EnumType.STRING)
private VisitStatus toStatus;
}
Visits can have multiple status like created, canceled, deleted etc.. whenever the status change there will be a new row added to the StatusChange table with a entry associated to that visit (toStatus will become canceled) . Now I want a query to filter the visits for which the latest status is canceled and with the matching user id.
I am using the #query of JPA. I already have got the result with the following query.
#Query(value = "select vs1.visit from StatusChange vs1 where vs1.id in (" +
" select max(vs2.id) from StatusChange vs2 " +
" where vs2.visit.user.id = :userId" +
" group by vs2.visit.id)" +
" and vs1.toStatus in :status")
public List<MSVisit> findByUserAndStatus(#Param("userId") Long userId, #Param("status") List<Visit.VisitStatus> status);
But I feel the query can be improved or is there any way to query some thing like,
"from Visit visit" +
" where visit.statusHistory.get(0).toStatus in :status" +
" and visit.user.id = :userId
Thanks for your help.
Usually, these queries are best modeled with lateral joins, but Hibernate does not support that yet. Note that unless you meant something different, your query is not correct, as the max-aggregation is based on the value of the id rather than the updated_on value. Not sure if that is on purpose, but I would suggest the following query in case you really want the latest visit.
#Query(value = "from Visit vs1 where vs1.user.id = :userId and (select max(h.updatedOn) from vs1.statusHistory h) >= ALL (" +
" select max(h.updatedOn) from vs1.statusHistory h " +
" and vs1.toStatus in :status")
public List<MSVisit> findByUserAndStatus(#Param("userId") Long userId, #Param("status") List<Visit.VisitStatus> status);

How to solve SQLException - Data conversion error in Spring Boot

I have two tables that are connected via class name (1:n).
Domain: Product (1)
Domain: HistoryPrice (n)
Product
#Entity
#Table
public class Product extends AbstractBaseDomain<Long> {
#NotBlank
#Size(min = 2, max = 50)
#Column(name ="name", unique = true)
private String name;
HistoryPrice
#Entity
#Table(name = "historyPrice")
public class HistoryPrice extends AbstractBaseDomain<Long> {
#NotNull
#ManyToOne
#JoinColumn(name ="product")
private Product product;
This is my repository
#Repository
public interface HistoryPriceRepository extends JpaRepository<HistoryPrice, Long> {
#Query(value = "SELECT h.product " +
"FROM history_price h " +
"INNER JOIN product p ON h.product = p.name " +
"WHERE p.name = :name", nativeQuery = true)
List<?> findProductByName(#Param("name") String name);
}
This is my Controller
#PostMapping(value = "/historyPrice")
public String searchForProducts(Model model, #RequestParam String namePart) {
List<?> productList = historyPriceService.findProductName(namePart);
model.addAttribute(HISTORYPRICE_VIEW, productList);
return HISTORYPRICE_VIEW;
}
This is my SQL output of my table creation:
2019-04-11 18:39:20 DEBUG org.hibernate.SQL - create table history_price (id bigint not null, version integer, price decimal(19,2) not null, valid_since timestamp not null, product bigint not null, primary key (id))
2019-04-11 18:39:20 DEBUG org.hibernate.SQL - create table product (id bigint not null, version integer, current_price decimal(19,2) not null, manufacturer varchar(50), name varchar(50), primary key (id))
This is my shortened error that I always get:
Caused by: org.hibernate.exception.DataException: could not extract ResultSet
Caused by: org.h2.jdbc.JdbcSQLException: Datenumwandlungsfehler beim Umwandeln von "HAMMER"
Data conversion error converting "HAMMER"; SQL statement:
SELECT h.product FROM history_price h INNER JOIN product p ON h.product = p.name WHERE p.name = ? [22018-197]
at org.h2.message.DbException.getJdbcSQLException(DbException.java:357)
at org.h2.message.DbException.get(DbException.java:168)
Caused by: java.lang.NumberFormatException: For input string: "HAMMER"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
I do not know whether my problem is in my repository or somewhere else.
Maybe someone can give me a the right solution or a good hint.
Thank you very much.
The problem indicated by your stacktrace is your join. You try to join h.product which is the id of the product object internally to h.product.name which is a string. Spring tries to parse the string as number afterwards thus resulting in the NumberFormatException.
I assume you want to get the HistoryPrice objects. Thus you have three options in your repository:
Use native query as you do now but fix tablenames and join, I assume this could work:
"SELECT h.* " +
"FROM historyPrice h " +
"INNER JOIN product p ON h.product = p.id " +
"WHERE p.name = :name"
Use a JPQL query:
"SELECT h " +
"FROM historyPrice h " +
"INNER JOIN product p " +
"WHERE p.name = :name"
Use the method name to let spring data generate your queries:
List<HistoryPrice> findAllByProductName(String name);
do you have any stack ?
Can you copy the error ?
In your log stack you should see some caused by label which will give you the place where the exception is throwed
It seems in the native query you are trying to equate a product object with a string name.
#Query(value = "SELECT h.product " +
"FROM history_price h " +
"INNER JOIN product p ON h.product.name = p.name " +
"WHERE p.name = :name", nativeQuery = true)
List<?> findProductByName(#Param("name") String name);
If the Product Entity contains a name variable, then the above query might execute.

JPA native query with same column twice

I am a JPA newbie and wanted to have a JPA native query for a single table (below) which I would like to fetch in my #Entity based class called TestRequest. It has a column 'RequestTime' that is fetched with DAYNAME() and then with DATEDIFF() functions.
SELECT TestRequest.Id AS Id
, TestRequest.RequestTime AS RequestTime
, DAYNAME(TestRequest.RequestTime) AS RequestDay
, TestRequest.StatusMessage AS StatusMessage
, DATEDIFF(CURDATE(), TestRequest.RequestTime) AS HowLongAgo
FROM TestRequest
LEFT JOIN TestRun
ON TestRequest.TestRunId = TestRun.Id
WHERE Requestor = '[NAME]'
ORDER BY Id DESC
Is there any way in which the column (fetched second time as HowLongAgo) be set into a property which is not mapped to a table column within the TestRequest class? Are there any field level annotations for this?
You need to use Interface-based projections:
You will need to create an interface that define the getters for each field in your projection like:
public interface RequestJoinRunProjection {
int getId();
LocalDate getRequestTime();
String getMessage();
String getRequestDay();
Long getHowLongAgo();
}
Then you define a method on your Repository that has the native query you want to run:
public interface TestRequestRepository extends CrudRepository<TestRequest, Long> {
// Any other custom method for TestRequest entity
#Query(value = "SELECT trq.Id AS id " +
" , trq.RequestTime AS requestTime " +
" , DAYNAME(trq.RequestTime) AS requestDay " +
" , trq.StatusMessage AS statusMessage " +
" , DATEDIFF(YEAR, CURDATE(), trq.RequestTime) AS howLongAgo " +
"FROM TestRequest trq " +
" LEFT JOIN TestRun tr " +
" ON trq.TestRunId = tr.Id " +
"WHERE Requestor = ?1 ORDER BY Id DESC"), nativeQuery = true)
List<RequestJoinRunProjection> findTestSumary(String name);
}
Notice query must be native since you are using database functions, also the column names must match the setters of your projection interface(following bean rules), so use AS in order to change the names in your query.
I strongly suggest you test your query on h2 before injecting into #Query annotation. DATEDIFF requires 3 parameters.

spring data jpa custom query fails to recognize class type

Not able to use custom POJO classes for my spring data jpa queries. Repeatedly fails with the following exception
"org.hibernate.MappingException: Unknown entity:
com.app.mycompany.AgileCenterServices.entities.ComponentDetailedInfo"*
Tried replacing the custom ComponentDetailedInfo.class and not mentioning anything during the call to entityManager.createNativeQuery(componentQuery.toString()), but then Object List returned fails to be converted to the specific POJO class after the query.
#Override
public ComponentListResponsePaginated findComponentByProjectId(String projectId, Pageable pageable) {
logger.info(" Inside findComponentByProjectId() API in IssueComponentServiceImpl");
String componentQuery = "select c.*, u.fullname "
+ "from issue_component c "
+ "left join user u on c.component_lead = u.username "
+ "where "
+ "upper(c.project_id) = upper(" + projectId + ")";
List<ComponentDetailedInfo> compList = new ArrayList<ComponentDetailedInfo>();
try {
logger.info(" ************* Printing query ******************************* ");
logger.info(componentQuery.toString());
compList = entityManager.createNativeQuery(componentQuery.toString(), ComponentDetailedInfo.class) .setFirstResult(pageable.getOffset())
.setMaxResults(pageable.getPageSize())
.getResultList();
}
}
Also tried the following
List<? extends Object> objList = null;
objList = entityManager.createNativeQuery(componentQuery.toString()) .setFirstResult(pageable.getOffset())
.setMaxResults(pageable.getPageSize())
.getResultList();
if(objList != null && objList.size() > 0) {
for(Object rec: objList) {
logger.info(" Printing Object ::: " + rec.toString());
compList.add((ComponentDetailedInfo)rec);
}
}
However the compList fails with the
java.lang.ClassCastException
The custom query returned should get typecast to the specific class type passed to the entityManager.createNativeQuery. However, I am facing the exception as mentioned above when I pass the class to createNativeQuery().
Even tried by totally removed the class in the createNativeQuery...
You have to define a constructor result mapping if you want to use a POJO as a result of a native query.
Here is an example query:
Query q = em.createNativeQuery(
"SELECT c.id, c.name, COUNT(o) as orderCount, AVG(o.price) AS avgOrder " +
"FROM Customer c " +
"JOIN Orders o ON o.cid = c.id " +
"GROUP BY c.id, c.name",
"CustomerDetailsResult");
And that's the mapping you have to add to your Entity:
#SqlResultSetMapping(name="CustomerDetailsResult",
classes={
#ConstructorResult(targetClass=com.acme.CustomerDetails.class,
columns={
#ColumnResult(name="id"),
#ColumnResult(name="name"),
#ColumnResult(name="orderCount"),
#ColumnResult(name="avgOrder", type=Double.class)})
})
If you don't like that approach you could use QLRM. Learn more about it here: https://github.com/simasch/qlrm

Resources