I have a complex native query and I am trying to map its result to a non-entity DTO class. I am trying to use JPA's SqlResultSetMapping with ConstructorResult
My DTO class
#Data
public class Dto {
private Long id;
private String serial;
private Long entry;
private int numOfTasks;
}
My entity class, which has the repository interface that I will call this native query result.
#SqlResultSetMapping(
name = "itemDetailsMapping",
classes = {
#ConstructorResult(
targetClass = Dto.class,
columns = {
#ColumnResult(name = "ID"),
#ColumnResult(name = "SERIAL"),
#ColumnResult(name = "ENTRY"),
#ColumnResult(name = "TASKS")
}
)
}
)
#NamedNativeQuery(name = "getItemDetails", query = "complex query is here", resultSetMapping = "itemDetailsMapping")
#Entity
#Data
public class Item {}
Repository
#Repository
public interface ItemRepository extends JpaRepository<Item, Long> {
...
List<Dto> getItemDetails();
}
When I call the getItemDetails() from ItemRepository I have the following error:
org.springframework.data.mapping.PropertyReferenceException: No
property itemDetails found for type Item
What is the proper way to use SqlResultSetMapping and ConstructorResult
and solve this problem.
Any help would be appreciated.
To use named queries the name of the named query must have the entity name as prefix:
#NamedNativeQuery(name = "Item.getItemDetails",
query = "complex query is here", resultSetMapping = "itemDetailsMapping")
Then the interface method must have the same name as the named query without the prefix:
List<Dto> getItemDetails();
-
Read more about Spring Data JPA and named queries in the reference doc https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.named-queries
Related
I was wondering if anyone knows the cause of the Exception being thrown? I have the following entities below. Is it because some Employees returned aren't DriverEmployees and thus do not have routes?
#Entity
public class Employee {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", nullable = false)
private Integer id;
private String name;
}
#Entity
public class DriverEmployee extends Employee {
#OneToMany(fetch = FetchType.LAZY)
private List<Routes> routes;
}
#Entity
public class Routes {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", nullable = false)
private Integer id;
private String name;
private String description;
}
CriteriaQuery<Tuple> criteria = criteriaBuilder.createQuery(Tuple.class);
Root<Employee> employee = criteria.from(Employee.class);
Root<DriverEmployee> driverEmployee = criteriaBuilder.treat(employee, DriverEmployee.class);
ListJoin<DriverEmployee, Routes> routes = driverEmployee.joinList("routes");
// Want routes to be returned as a list in the multiselect.
criteria.multiselect(employee.get("name").alias("name"), routes.alias("routes"));
TypedQuery<Tuple> query = em.createQuery(criteria);
query.getResultList().forEach((t) -> {
process(t.get("name", String.class));
processList(t.get("routes", List.class).size());
});
The error I'm receiving is below. Anyone know how I can get the following to run successfully?
Caused by: java.lang.IllegalStateException: No data type for node: org.hibernate.hql.internal.ast.tree.IdentNode
+-[IDENT] IdentNode: 'routes' {originalText=routes}
In this current example you have tuple which contains one object of DriverEmployee and one object of Routes (I recommend you to rename this entity to Route and set #Table(name = "routes"). According to JavaDoc of multiselect():
If the type of the criteria query is CriteriaQuery (i.e., a criteria query object created by either the createTupleQuery method or by passing a Tuple class argument to the createQuery method), a Tuple object corresponding to the arguments of the multiselect method, in the specified order, will be instantiated and returned for each row that results from the query execution.
So it means that you CAN'T make a Tuple like Tuple<DriverEmployee, List<Routes>>
The way how you can reach such behavior of your current query is to do it by yourself. For instance:
your method with criteria:
CriteriaQuery<Tuple> criteria = criteriaBuilder.createQuery(Tuple.class);
Root<Employee> employee = criteria.from(Employee.class);
Root<DriverEmployee> driverEmployee = criteriaBuilder.treat(employee, DriverEmployee.class);
ListJoin<DriverEmployee, Routes> routes = driverEmployee.joinList("routes");
criteria.multiselect(employee.get("name").alias("name"), routes.alias("routes"));
TypedQuery<Tuple> query = em.createQuery(criteria);
List<Tuple> resultList = query.getResultList();
Map<String, List<Routes>> resultMap = getMapFromResultList(resultList);
resultMap.entrySet().forEach((name, routesList) -> {
process(name);
processList(routesList);
});
and the method of obtaining the map:
private Map<String, List<Routes>> getMapFromResultList(List<Tuple> tuples) {
Map<String, List<Routes>> map = new HashMap<>();
tuples.forEach(tuple -> {
String name = tuple.get("name", String.class);
Routes routes = tuple.get("routes", Routes.class);
map.computeIfPresent(name, (key, value) -> {
value.add(routes);
return value;
});
map.computeIfAbsent(name, (key) -> {
List<Routes> routesList = new ArrayList<>();
routesList.add(routes);
return routesList;
});
});
return map;
}
I guess the Hibernate JPA Criteria implememtation doesn't support that. If you really want to use the JPA Criteria API to do this, you are probably out of luck. In JPQL/HQL you could model this like SELECT e.name, r FROM DriverEmployee e LEFT JOIN e.routes r. On top of that you have to extract the values like Andrew Kolesnyk mentioned.
However, this is is the perfect use case for Blaze-Persistence Entity Views.
Blaze-Persitence is a query builder on top of JPA which supports many of the advanced DBMS features on top of the JPA model. I created Entity Views on top of it to allow easy mapping between JPA models and custom interface defined models, something like Spring Data Projections on steroids. The idea is that you define your target structure the way you like and map attributes(getters) via JPQL expressions to the entity model. Since the attribute name is used as default mapping, you mostly don't need explicit mappings as 80% of the use cases is to have DTOs that are a subset of the entity model.
A mapping for your model could look as simple as the following
#EntityView(DriverEmployee.class)
interface EmployeeDto {
#IdMapping
Integer getId();
String getName();
List<RoutesDto> getRoutes();
}
#EntityView(Routes.class)
interface RoutesDto {
#IdMapping
Integer getId();
String getName();
}
Querying is a matter of applying the entity view to a query, the simplest being just a query by id.
EmployeeDto dto = entityViewManager.find(entityManager, EmployeeDto.class, id);
The Spring Data integration allows you to use it almost like Spring Data Projections: https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features and it can also be saved back. Here a sample repository
#Repository
interface EmployeeRepository {
EmployeeDto findOne(Long id);
}
It will only fetch the mappings that you tell it to fetch.
Here you can see an example project: https://github.com/Blazebit/blaze-persistence/tree/master/examples/spring-data-webmvc
I am working with spring boot. I want to apply a join in #Query and get the results in List.
objDto.java
#ToString
#Setter
#Getter
#AllArgsConstructor
public class objDto {
public int id;
private String title;
}
ObjDomain.java
#ToString
#Getter
#Setter
#Entity
#NoArgsConstructor
public class ObjDomain implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
public int id;
public String title;
}
ShowDetailsRepository.java
#Repository
public interface ShowDetailsRepository extends JpaRepository<ObjDomain, Long> {
#Query(value ="//JOIN QUERY HERE//", nativeQuery = true)
List<ObjDto> showDetails();
}
My Join query is working well when I used it in MYSQL CLI.
Error : No converter found capable of converting from type [org.springframework.data.jpa.repository.query.AbstractJpaQuery$TupleConverter$TupleBackedMap] to type [com.example.com.data.transfer.objects.ObjDto]
Expected Result : [{id: "1", title: "DEF"}]
You can create an interface projection instead of a DTO and in that interface you can only have the getter methods and pass that interface as the type of List.
#Query(value ="//JOIN QUERY HERE//", nativeQuery = true)
List<YourInterface> showDetails();
and your interface will look like this:
public interface YourInterface {
int getid();
String gettitle();
}
And make sure the getter methods in interface are in the same sequence as the values query results.
You will have to create sqlResultsetMapping..by doing that you are telling the sql in which class you want map the resultset. And also u can create the projection with help of interface. which can take less efforts compare to sqlResultsetMapping.
Below is the example
Lets say i want fetch few data from multiple table using native join query.
For Example- i will have to fetch empname and his dept from emp and dept table so my native query looks like below:
#Query(value = "SELECT e.name as empName,d.name as deptName FROM emp e left join dept d on d.empid=e.id WHERE e.id=:Id ,nativeQuery = true)
List<CustomData> findByRestaurantMenuGrouping(#Param("menuId") Long menuId);
Now put below resultsetmapping above any entity i can either emp or dept.
#SqlResultSetMapping(name = "customDataMapping",
classes = #ConstructorResult(
targetClass = CustomData.class,
columns = {
#ColumnResult(name = "empName", type = String.class),
#ColumnResult(name = "deptName", type = String.class),
}
)
)
-And create CustomData class as response class with 2 fields and parameterized constructer.
After you can call query using entitymanager by providing resultsetmapping which we created above.
List<CustomData> customdata= (List<CustomData>) em.createNativeQuery(quiStringBuilder.toString().trim(), "customDataMapping").getResultList();
You should use the combine of annotation SqlResultSetMapping and NamedNativeQuery.
For example:
In your entity:
#ToString
#Getter
#Setter
#Entity
#NoArgsConstructor
#SqlResultSetMappings({
#SqlResultSetMapping(
name = "ObjDtoMapping",
classes = #ConstructorResult(
targetClass = objDto.class,
columns = {
#ColumnResult(name = "id"),
#ColumnResult(name = "title")
}
)
)
})
#NamedNativeQuery(name = "showDetails", query = "SELECT a.id, a.title from
ObjDomain a", resultSetMapping = "ObjDtoMapping")
public class ObjDomain implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
public int id;
public String title;
}
And then, in your repository:
#Repository
public interface ShowDetailsRepository extends JpaRepository<ObjDomain, Long> {
#Query(name ="showDetails", nativeQuery = true)
List<ObjDto> showDetails();
}
change List<ObjDto> to
List<Object[]>
then in Service layer
List<Object[]> objectList = repositoryObj.showDetails();
for (Object[] obj : objectList) {
//.......
}
So I am writing native queries because they are very complex to get all the needed data and so on. I am currently facing a problem which is normally done by Hibernate/JPA etc.
Imagine the following:
#Entity
FooEntity{
#Id
public Long id;
#ManyToMany
public List<FeeEntity> feeEntities;
}
#Entity
FeeEntity{
#Id
public Long id;
#Column
public String name;
}
And some DTO
FooDTO{
private final Long id;
private final List<FeeDTO> someStrings;
public FooDTO(Long id, List<FeeDTO> feeDtos){
...
}
}
My #SqlResultSetMapping looks basically like
#SqlResultSetMapping(name = "FooDTO",
classes = #ConstructorResult(targetClass = FooDTO.class, columns = {
#ColumnResult(name = "id", type = Long.class),
//TODO #ColumnResult(name = "feeDtos", type = FeeDtos.class)
})
)
The named native query looks something like:
#NamedNativeQueries({
#NamedNativeQuery(
name = "FooData",
query = "SELECT MAINSELECT.ID AS id, " +
"???" +
" FROM Foo MAINSELECT WHERE ... " +
...,
resultSetMapping = "FooDTO")
})
How do I have to write the native query? Is this even possible without a subquery or do I have to do/execute a subquery for each datarow of the result? I was not able to find something on beloved google.
That's not possible.
The result of SQL query is always a table. So there are no nested tables and hence you cannot map it to a Collection.
I've got a value object for which i want to map my native query resultset.
I'm using #SQLResultSetMapping to map object fields but when value of any column is null, exception is throw:
Exception [EclipseLink-6177] (Eclipse Persistence Services - 2.6.1.v20150916-55dc7c3): org.eclipse.persistence.exceptions.QueryException
Exception Description: The column result [custom_properties] was not found in the results of the query.
My Entity Class and Mapping
#Entity
#Multitenant(MultitenantType.TABLE_PER_TENANT)
#Table(name = "account_master")
#SqlResultSetMapping(name = "DTO_MAPPING", classes = #ConstructorResult(
targetClass = AccountDTO.class,
columns = {#ColumnResult(name = "id"),
#ColumnResult(name = "name"),
#ColumnResult(name = "custom_properties")
})
)
public class Account implements Serializable {
// fields, setters and getters
}
Value Object:
public class AccountDTO {
public AssetDTO(){
}
public AssetDTO(int id, String name, String customProperties) {
this.id = id;
this.name = name;
this.customProperties = customProperties;
}
}
And the execution statement
List<AccountDTO> accList = entityManager.createNativeQuery("SELECT id, name, custom_properties FROM account_master WHERE acc_hierarchy <# 2.3", "DTO_MAPPING").getResultList()
If custom_properties (which is nullable) is replaced by a static value in query, mapping works perfectly fine. Is there something wrong with the implementation? As mapping null value seems like a common scenario.
This was submitted as a bug in EclipseLink: https://bugs.eclipse.org/bugs/show_bug.cgi?id=484276
As a workaround, you can specify the type in the
#ColumnResult
annotation.
So you should change your code to:
#Entity
#Multitenant(MultitenantType.TABLE_PER_TENANT)
#Table(name = "account_master")
#SqlResultSetMapping(name = "DTO_MAPPING", classes = #ConstructorResult(
targetClass = AccountDTO.class,
columns = {#ColumnResult(name = "id"),
#ColumnResult(name = "name"),
#ColumnResult(name = "custom_properties", type=String.class)
})
)
public class Account implements Serializable {
// fields, setters and getters
}
I want to create a class that can be mapped to a result extracted from the database using JPA native query. Is there a way to map an entity without an underlying table to the result?
I referred to this link which allows it for hibernate. Can this be done using JPA instead?
This is my class for which I want the result to be mapped.
import java.math.BigDecimal;
import javax.persistence.Entity;
#Entity
public class OpUsage {
String username;
BigDecimal number_of_clicks;
String accordion;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public BigDecimal getNumber_of_clicks() {
return number_of_clicks;
}
public void setNumber_of_clicks(BigDecimal number_of_clicks) {
this.number_of_clicks = number_of_clicks;
}
public String getAccordion() {
return accordion;
}
public void setAccordion(String accordion) {
this.accordion = accordion;
}
}
JPA 2.1 specification defines the means to return the result from a native query to a non entity class
You should checkout the heading 3.10.16.2 Returning Unmanaged Instances especially the
3.10.16.2.2 Constructor Results
The mapping to constructors is specified using the ConstructorResult
annotation element of the SqlResultSetMapping annotation. The
targetClass element of the ConstructorResult annotation specifies the
class whose constructor corresponds to the specified columns. All
columns corresponding to arguments of the intended constructor must be
specified using the columns element of the ConstructorResult
annotation in the same order as that of the argument list of the
constructor. Any entities returned as constructor results will be in
either the new or the detached state, depending on whether a primary
key is retrieved for the constructed object.
example
Query q = em.createNativeQuery(
"SELECT c.id, c.name, COUNT(o) as orderCount, AVG(o.price) AS
avgOrder" +
"FROM Customer c, Orders o " +
"WHERE o.cid = c.id " +
"GROUP BY c.id, c.name",
"CustomerDetailsResult");
#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)})
})