I have a complex SQL query which may have different number of parameters in where section. So, query sql should be constructed manually. As a result, I get array of objects which contains 45 fields, each of them I will have to case, convert, etc. I can't use result set mapping because it requires a stable SQL which I should specify in annotation. So the question is is there a way to return pojo or at least map with columns names rather than access all objects by index?
String sql = "select col1 as column1, ...., columnN as columnN from table where col1=2 ";
if(param1!=null){
sql+=" AND param1="+param1;
}
....
Query q = manager.createNativeQuery(sql);
//getting list on object arrays of 45 fields, would like to have POJO or at least map
List list = q.getResultList();
If the table from where you retrieve your data is mapped in a POJO, you can specify manually the POJO by doing the following:
Query query = em.createNativeQuery("SELECT * FROM table", MyPojoMappingTable.class);
#SuppressWarnings("unchecked")
List<MyPojoMappingTable> items = (List<MyPojoMappingTable>) query.getResultList();
On the contrary, if the table is not mapped in any POJO and you're in Spring, you could use jdbcTemplate to get at least a map:
#Autowired
NamedParameterJdbcTemplate jdbcTemplate;
String query;
Map<String, Object> queryParameters;
List<Map<String, Object>> rows = jdbcTemplate.queryForList(
query,
queryParameters
);
Related
Suppose I have a table which contains all the accounts of user and type.
I want to make a Jpa Repository method which returns an array of total number of each type of user (USER, ADMIN, MASTER).
Here is how I did it in JpaRepository:
#Query(value="SELECT (SELECT COUNT(*) FROM account WHERE account_role='USER'),"
+ "(SELECT COUNT(*) FROM account WHERE account_role='ADMIN'),"
+ "(SELECT COUNT(*) FROM account WHERE account_role='MASTER')"
+ "FROM account LIMIT 1",
nativeQuery=true)
public List<Integer> getTotalAccountType();
The code executed fine, but the result wasn't what I expected.
Result:
[2]
Expected result: [2,10,30]
Any idea how would I use nested SQL with JPQL? Thank you in advance!
If repository method returns List of Integers it means that query result row contains an Integer value. But you expect to get sequence of Integers in one row.
You can get same result different way:
#Query(value="SELECT COUNT(*) FROM account WHERE account_role=?", nativeQuery=true)
public Integer getTotalAccountType(String role);
and then:
Integer userCount = repository.getTotalAccountType("USER");
Integer adminCount = repository.getTotalAccountType("ADMIN");
Integer masterCount = repository.getTotalAccountType("MASTER");
or if you have mapped entity:
create Pair<K,V> class with constructor Pair(K key, V value) or use it from any external library
repository method based on hql query
#Query(value="select new javafx.util.Pair(a.accountRole, count(a)) from Account a group by a.accountRole")
public List<Pair<String, Integer>> getRoleCountList();
convert repository result to a Map<String, Integer> in service
javafx.util.Pair<String, Integer> result = repository.getRoleCountList();
Map<String, Integer> map = result.stream().collect(Collectors.toMap(r-> r.getKey(), r-> r.getValue()));
Try returning Object[] rather than a List<Integer>. I think returning List<Integer> would indicate multiple rows of an Integer value are being returned, whereas you're getting back one row with multiple Intger columns.
From the resulting Object[] you would pull out the first value (indicating a row). This should be another Object[], which will have your values in the order returned.
You can also remove that last "FROM account LIMIT 1" line, as it has no bearing on the result.
I would recommend casting all of this to an object though. As seen here -
How to return a custom object from a Spring Data JPA GROUP BY query
I have a leaveList containing 4 leave names.This leaveList is passed as map value.I want to get leave details from CompanyLeave Table by passing leaveList in hql query.Let be considered,my Company Leave Table contains 6 leave details.leaveList has 3 leave names.I want to get details of these 3 leaves from CompanyLeave Table.
Code for Hql query here leaveNameList is a list as well as map
public List<CompanyLeaveType> getByValidLeave(Map<String, Object> params) {
Query query = sessionfactory.getCurrentSession().createQuery("from CompanyLeaveType WHERE companyCode = :companyCode and leaveName IN (:leaveNames)");
query.setParameter("companyCode", params.get("companyCode"));
query.setParameter("leaveNames", params.get("leaveNameList"));
List<CompanyLeaveType> validLeaveDetails = query.list();
return validLeaveDetails;
}
N.B: I have got java.util.ArrayList cannot be cast to java.lang.String error.How can I pass list in hql query?
Use query.setParameterList(), Check the documentation here.
Query query = sessionfactory.getCurrentSession().createQuery("from CompanyLeaveType WHERE companyCode = :companyCode and leaveName IN (:leaveNames)");
query.setParameter("leaveNames", params.get("leaveNameList"));
Here you are trying to add a list object to the Hql query.
Here in this case the generated query by hibernate looks like this(actually its not happened and is just to make you to understand whats going on here)
1) Select *from companyLeveType_Table where companyCode=someX and leaveName in(ListObject)
But here the leaveName is of type java.lang.String and hence hibernte frameworks expects the values should be the string only. see the sample code (Hibernte expects this)
2) Select *from companyLeveType_Table where companyCode=someX and leaveName in("A","B","C");
from first query its obvious that hibernate framework tries to convert the java.util.ArrayList to java.lang.String and hence exception throws.
Solution 1)
public List<CompanyLeaveType> getByValidLeave(Map<String, Object> params) {
Query query = sessionfactory.getCurrentSession().createQuery("from CompanyLeaveType WHERE companyCode = :companyCode and leaveName IN (:leaveNames)");
query.setParameter("companyCode", params.get("companyCode"));
query.setParameterList("leaveNames", params.get("leaveNameList")); // changes here only remaining is same
List<CompanyLeaveType> validLeaveDetails = query.list();
return validLeaveDetails;
}
Solution 2:
Use Criteria api.
public List<CompanyLeaveType> getByValidLeave(Map<String, Object> params) {
Criteria criteria=session.createCriteria(CompanyLeaveType.class);
criteria.addCriteria(Restrictions.eq("companyCode",params.get("companyCode")))
.addCriteria(Restrictions.in("leaveName",params.get("leaveNameList")));
List<CompanyLeaveType> validLeaveDetails =criteria.list();
return validLeaveDetails;
}
I hope this helps you
I have some very complicated SQL (does some aggregation, some counts based on max value etc) so I want to use SQLQuery rather than Query. I created a very simple Pojo:
public class SqlCount {
private String name;
private Double count;
// getters, setters, constructors etc
Then when I run my SQLQuery, I want hibernate to populate a List for me, so I do this:
Query hQuery = sqlQuery.setResultTransformer(Transformers.aliasToBean(SqlCount.class));
Now I had a problem where depending on what the values are for 'count', Hibernate will variably retrieve it as a Long, Double, BigDecimal or BigInteger. So I use the addScalar function:
sqlQuery.addScalar("count", StandardBasicTypes.DOUBLE);
Now my problem. It seems that if you don't use the addScalar function, Hibernate will populate all of your fields with all of your columns in your SQL result (ie it will try to populate both 'name' and 'count'). However if you use the addScalar function, it only maps the columns that you listed, and all other columns seem to be discarded and the fields are left as null. In this instance, it wouldn't be too bad to just list both "name" and "count", but I have some other scenarios where I need a dozen or so fields - do I really have to list them all?? Is there some way in hibernate to say "map all fields automatically, like you used to, but by the way map this field as a Double"?
Is there some way in hibernate to say "map all fields automatically.
No, check the document here, find 16.1.1. Scalar queries section
The most basic SQL query is to get a list of scalars (values).
sess.createSQLQuery("SELECT * FROM CATS").list();
sess.createSQLQuery("SELECT ID, NAME, BIRTHDATE FROM CATS").list();
These will return a List of Object arrays (Object[]) with scalar values for each column in the CATS table. Hibernate will use ResultSetMetadata to deduce the actual order and types of the returned scalar values.
To avoid the overhead of using ResultSetMetadata, or simply to be more explicit in what is returned, one can use addScalar():
sess.createSQLQuery("SELECT * FROM CATS")
.addScalar("ID", Hibernate.LONG)
.addScalar("NAME", Hibernate.STRING)
.addScalar("BIRTHDATE", Hibernate.DATE)
i use this solution, I hope it will work with you.
with this solution you can populate what you select from the SQL, and return it as Map, and cast the values directly.
since hibernate 5.2 the method setResultTransformer() is deprecated but its work fine to me and works perfect.
if you hate to write extra code addScalar() for each column from the SQL, you can implement ResultTransformer interface and do the casting as you wish.
ex:
lets say we have this Query:
/*ORACLE SQL*/
SELECT
SEQ AS "code",
CARD_SERIAL AS "cardSerial",
INV_DATE AS "date",
PK.GET_SUM_INV(SEQ) AS "sumSfterDisc"
FROM INVOICE
ORDER BY "code";
note: i use double cote for case-sensitive column alias, check This
after create hibernate session you can create the Query like this:
/*Java*/
List<Map<String, Object>> list = session.createNativeQuery("SELECT\n" +
" SEQ AS \"code\",\n" +
" CARD_SERIAL AS \"cardSerial\",\n" +
" INV_DATE AS \"date\",\n" +
" PK.GET_SUM_INV(SEQ) AS \"sumSfterDisc\"\n" +
"FROM INVOICE\n" +
"ORDER BY \"code\"")
.setResultTransformer(new Trans())
.list();
now the point with Trans Class:
/*Java*/
public class Trans implements ResultTransformer {
private SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US);
#Override
public Object transformTuple(Object[] objects, String[] strings) {
Map<String, Object> map = new LinkedHashMap<>();
for (int i = 0; i < strings.length; i++) {
if (objects[i] == null) {
continue;
}
if (objects[i] instanceof BigDecimal) {
map.put(strings[i], ((BigDecimal) objects[i]).longValue());
} else if (objects[i] instanceof Timestamp) {
map.put(strings[i], dateFormat.format(((Timestamp) objects[i])));
} else {
map.put(strings[i], objects[i]);
}
}
return map;
}
#Override
public List transformList(List list) {
return list;
}
}
here you should override the two method transformTuple and transformList, in transformTuple you have two parameters the Object[] objects its the columns values of the row and String[] strings the names of the columns the hibernate Guaranteed the same order of of the columns as you order it in the query.
now the fun begin, for each row returned from the query the method transformTuple will be invoke, so you can build the row as Map or create new object with fields.
I have #OneToMany association between 2 entities (Entity1 To Entity2).
My sqlQueryString consists of next steps:
select ent1.*, ent2.differ_field from Entity1 as ent1 left outer join Entity2 as ent2 on ent1.item_id = ent2.item_id
Adding some subqueries and writing results to some_field2, some_field3 etc.
Execute:
Query sqlQuery = getCurrentSession().createSQLQuery(sqlQueryString)
.setResultTransformer(Transformers.aliasToBean(SomeDto.class));
List list = sqlQuery.list();
and
class SomeDto {
item_id;
some_filed1;
...
differ_field;
...
}
So the result is the List<SomeDto>
Fields which are highlighted with grey are the same.
So what I want is to group by, for example, item_id and
the List<Object> differFieldList would be as aggregation result.
class SomeDto {
...fields...
List<Object> differFieldList;
}
or something like that Map<SomeDto, List<Object>>
I can map it manually but there is a trouble:
When I use sqlQuery.setFirstResult(offset).setMaxResults(limit)
I retrieve limit count of records. But there are redundant rows. After merge I have less count actually.
Thanks in advance!
If you would like to store the query results in a collection of this class:
class SomeDto {
...fields...
List<Object> differFieldList;
}
When using sqlQuery.setFirstResult(offset).setMaxResults(n), the number of records being limited is based on the joined result set. After merging the number of records could be less than expected, and the data in List could also be incomplete.
To get the expected data set, the query needs to be broken down into two.
In first query you simply select data from Entity1
select * from Entity1
Query.setFirstResult(offset).setMaxResults(n) can be used here to limit the records you want to return. If fields from Entity2 needs to be used as condition in this query, you may use exists subquery to join to Entity2 and filter by Entity2 fields.
Once data is returned from the query, you can extract item_id and put them into a collection, and use the collection to query Entity 2:
select item_id, differ_field from Entity2 where item_id in (:itemid)
Query.setParameterList() can be used to set the item id collection returned from first query to the second query. Then you will need to manually map data returned from query 2 to data returned from query 1.
This seems verbose. If JPA #OneToMany mapping is configured between the 2 entity objects, and your query can be written in HQL (you said not possible in comment), you may let Hibernate lazy load Entity2 collection for you automatically, in which case the code can be much cleaner, but behind the scenes Hibernate may generate more query requests to DB while lazy loading the entity sitting at Many side.
The duplicated records are natural from a relational database perspective. To group projection according to Object Oriented principles, you can use a utility like this one:
public void visit(T object, EntityContext entityContext) {
Class<T> clazz = (Class<T>) object.getClass();
ClassId<T> objectClassId = new ClassId<T>(clazz, object.getId());
boolean objectVisited = entityContext.isVisited(objectClassId);
if (!objectVisited) {
entityContext.visit(objectClassId, object);
}
P parent = getParent(object);
if (parent != null) {
Class<P> parentClass = (Class<P>) parent.getClass();
ClassId<P> parentClassId = new ClassId<P>(parentClass, parent.getId());
if (!entityContext.isVisited(parentClassId)) {
setChildren(parent);
}
List<T> children = getChildren(parent);
if (!objectVisited) {
children.add(object);
}
}
}
The code is available on GitHub.
I'm using Spring's JdbcTemplate and running a query like this:
SELECT COLNAME FROM TABLEA GROUP BY COLNAME
There are no named parameters being passed, however, column name, COLNAME, will be passed by the user.
Questions
Is there a way to have placeholders, like ? for column names? For example SELECT ? FROM TABLEA GROUP BY ?
If I want to simply run the above query and get a List<String> what is the best way?
Currently I'm doing:
List<Map<String, Object>> data = getJdbcTemplate().queryForList(query);
for (Map m : data) {
System.out.println(m.get("COLNAME"));
}
To populate a List of String, you need not use custom row mapper. Implement it using queryForList.
List<String>data=jdbcTemplate.queryForList(query,String.class)
Use following code
List data = getJdbcTemplate().queryForList(query,String.class)
Is there a way to have placeholders, like ? for column names? For example SELECT ? FROM TABLEA GROUP BY ?
Use dynamic query as below:
String queryString = "SELECT "+ colName+ " FROM TABLEA GROUP BY "+ colName;
If I want to simply run the above query and get a List what is the best way?
List<String> data = getJdbcTemplate().query(query, new RowMapper<String>(){
public String mapRow(ResultSet rs, int rowNum)
throws SQLException {
return rs.getString(1);
}
});
EDIT: To Stop SQL Injection, check for non word characters in the colName as :
Pattern pattern = Pattern.compile("\\W");
if(pattern.matcher(str).find()){
//throw exception as invalid column name
}
You can't use placeholders for column names, table names, data type names, or basically anything that isn't data.