I want to create a Search specification where I can select data based on Date range. I tried this:
#Getter
#Setter
public class BillingSummarySearchParams {
private LocalDateTime startDate;
private LocalDateTime endDate;
}
Search Specification
public List<BillingSummaryFullDTO> findBillingInvoicesSummary(BillingSummarySearchParams params)
{
Specification<BillingSummary> spec = (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
if (params.getStartDate() != null | params.getEndDate() != null) {
predicates.add(cb.like(cb.lower(root.get("startDate")), "%" + params.getStartDate() + "%"));
}
return cb.and(predicates.toArray(new Predicate[predicates.size()]));
};
return billingSummaryService.findAll(spec).stream().map(billingSummaryMapper::toFullDTO).collect(Collectors.toList());
}
Search SQL
public List<BillingSummary> findAll(Specification<BillingSummary> spec)
{
String hql = "select e from " + BillingSummary.class.getName() + " e where e.createdAt BETWEEN :startDate AND :endDate ORDER BY e.createdAt DESC";
TypedQuery<BillingSummary> query = entityManager.createQuery(hql, BillingSummary.class).setParameter("startDate", spec).setParameter("endDate", spec);
List<BillingSummary> list = query.getResultList();
return list;
}
It's not clear to me how I can build the specification with 2 dates to work and with only one.
What is the proper way to get the dates from the spec object?
I think you are missunderstanding what Specifications are used for. They aren't meant to be used in combination with "old" typed queries. If you set a Specification as parameter for them it should result in errors because the jpa provider has no idea how to work with that object.
Instead they are implemented to have an easier time to create and use criteria queries in spring data. The first step to make them work would be to implement JpaSpecificationExecutor in your repository. The interface contains methods like findAll(Specification<T> spec) and spring will automatically create these methods for your repository. Spring generates the criteria query in the background and adds the predicate you create in your specification to it.
#Repository
public interface BillingSummaryRepository extends JpaRepository<BillingSummary, Long>, JpaSpecificationExecutor<BillingSummary> {
//Other methods
}
Specification<BillingSummary> spec = (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
if (params.getStartDate() != null | params.getEndDate() != null) {
predicates.add(cb.like(cb.lower(root.get("startDate")), "%" + params.getStartDate() + "%"));
}
return cb.and(predicates.toArray(new Predicate[predicates.size()]));
};
List<BillingSummary> summaries = billingSummaryService.findAll(spec, Sort.by(Sort.Direction.DESC, "createdAt"));
Related
I have an issue with mapping retrieved data via JDBi3 using PostgreSQL query in my DAO interface.
In my Dropwizard app I have Book DTO class which is has Many-To-Many relation with Author and Category DTO classes and have a problem with mapping queried rows onto BookDTO class. Here are the code snippets of DTO classes:
class BookDTO {
private Long bookId;
// other fields are left for code brevity
private List<Long> authors;
private List<Long> categories;
// empty constructor + constructor with all fields excluding Lists + getters + setters
}
class AuthorDTO {
private Long authorId;
// other fields are left for code brevity
private List<Long> books;
// empty constructor + constructor with all fields excluding List + getters + setters
}
class CategoryDTO {
private Long categoryId;
// other fields are left for code brevity
private List<Long> books;
// empty constructor + constructor with all fields excluding List + getters + setters
}
...and since I am using JDBi3 DAO interfaces for performing CRUD operations this is how my method for querying all books in database looks like:
#Transaction
#UseRowMapper(BookDTOACMapper.class)
#SqlQuery("SELECT book.book_id AS b_id, book.title, book.price, book.amount, book.is_deleted, author.author_id AS aut_id, category.category_id AS cat_id FROM book " +
"LEFT JOIN author_book ON book.book_id = author_book.book_id " +
"LEFT JOIN author ON author_book.author_id = author.author_id " +
"LEFT JOIN category_book ON book.book_id = category_book.book_id " +
"LEFT JOIN category ON category_book.category_id = category.category_id ORDER BY b_id ASC, aut_id ASC, cat_id ASC")
List<BookDTO> getAllBooks();
...and this is map method of BookDTOACMapper class look like:
public class BookDTOACMapper implements RowMapper<BookDTO> {
#Override
public BookDTO map(ResultSet rs, StatementContext ctx) throws SQLException {
final long bookId = rs.getLong("b_id");
// normally retrieving values by using appropriate rs.getXXX() methods
Set<Long> authorIds = new HashSet<>();
Set<Long> categoryIds = new HashSet<>();
long authorId = rs.getLong("aut_id");
if (authorId > 0) {
authorIds.add(authorId);
}
long categoryId = rs.getLong("cat_id");
if (categoryId > 0) {
categoryIds.add(categoryId);
}
while (rs.next()) {
if (rs.getLong("b_id") != bookId) {
break;
} else {
authorId = rs.getLong("aut_id");
if (authorId > 0) { authorIds.add(authorId); }
categoryId = rs.getLong("cat_id");
if (categoryId > 0) { categoryIds.add(categoryId); }
}
}
final List<Long> authorIdsList = new ArrayList<>(authorIds);
final List<Long> categoryIdsList = new ArrayList<>(categoryIds);
return new BookDTO(bookId, title, price, amount, is_deleted, authorIdsList, categoryIdsList);
}
}
Problem I encounter is that when invoking my GET method (defined in Resource class which invokes getAllBooks() method from BookDAO class) displays inconsistent results while the query itself returns proper results.
Many questions that I've managed to find on Stackoverflow, official JDBi3 Docs API and Google Groups are considering One-To-Many relationship and using #UseRowReducer annotation which contains class which impelements LinkedHashMapRowReducer<TypeOfEntityIdentifier, EntityName> but for this case I could not find a way to implement it. Any example/suggestion is welcome. :)
Thank you in advance.
Versions of used tools:
Dropwizard framework 1.3.8
PostgreSQL 11.7
Java8
This will be too long for a comment:
This is basically a debugging question. Why?
while (rs.next()) {
if (rs.getLong("b_id") != bookId) {
break;
} else {
The firstif after the while is eating the row after the current (the one that wass current when the row mapper is called). You are skipping the processing there (putting the data in the Java objects) for the bookId, authorId, etc. That's why you get
inconsistent results while the query itself returns proper results.
So you need to revisit how you process the data. I see two paths:
Revisit the logic of the processing loop to store the data when stopping the processing for given bookId. It is possible to achieve this with scrollable ResultSets - i.e. request a scrollable ResultSet and before the brake; call rs.previous(). On the next call to the row mapper the processing will start from the correct line in the result set.
Use the power of the SQL/PostgreSQL and do it properly: https://dba.stackexchange.com/questions/173831/convert-right-side-of-join-of-many-to-many-into-array Aggregate and shape the data in the database. The database is the best tool for this job.
Also take your time and check the other answers of https://dba.stackexchange.com/users/3684/erwin-brandstetter. They give invaluable insights in the SQL and PostgreSQL.
As zloster mentioned in his answer I've chosen 2nd option (by this answer for Many-To-Many relationships) which was to use edit my PostgreSQL query #SqlQuery annotation above List<BookDTO> getAllBooks(); method. Query now uses array_agg aggregate function in SELECT statement to group my results in an ARRAY and now looks like this:
#UseRowMapper(BookDTOACMapper.class)
#SqlQuery("SELECT b.book_id AS b_id, b.title, b.price, b.amount, b.is_deleted, ARRAY_AGG(aut.author_id) as aut_ids, ARRAY_AGG(cat.category_id) as cat_ids " +
"FROM book b " +
"LEFT JOIN author_book ON author_book.book_id = b.book_id " +
"LEFT JOIN author aut ON aut.author_id = author_book.author_id " +
"LEFT JOIN category_book ON category_book.book_id = b.book_id " +
"LEFT JOIN category cat ON cat.category_id = category_book.category_id " +
"GROUP BY b_id " +
"ORDER BY b_id ASC")
List<BookDTO> getAllBooks();
Therefore map(..) method of BookDTOACMapper class had to be edited and now looks like this:
#Override
public BookDTO map(ResultSet rs, StatementContext ctx) throws SQLException {
final long bookId = rs.getLong("b_id");
String title = rs.getString("title");
double price = rs.getDouble("price");
int amount = rs.getInt("amount");
boolean is_deleted = rs.getBoolean("is_deleted");
Set<Long> authorIds = new HashSet<>();
Set<Long> categoryIds = new HashSet<>();
/* rs.getArray() retrives java.sql.Array and after it getArray gets
invoked which returns array of Object(s) which are being casted
into array of Long elements */
Long[] autIds = (Long[]) (rs.getArray("aut_ids").getArray());
Long[] catIds = (Long[]) (rs.getArray("cat_ids").getArray());
Collections.addAll(authorIds, autIds);
Collections.addAll(categoryIds, catIds);
final List<Long> authorIdsList = new ArrayList<>(authorIds);
final List<Long> categoryIdsList = new ArrayList<>(categoryIds);
return new BookDTO(bookId, title, price, amount, is_deleted, authorIdsList, categoryIdsList);
}
Now all results are consistent and here's a screenshot of query in pgAdmin4.
I am using the following code to make a dynamic search by Criteria API
public List<User> findAllByParam(List<SearchCriteria> params) {
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<User> query = builder.createQuery(User.class);
Root<User> r = query.from(User.class);
Predicate predicate = builder.conjunction();
for (SearchCriteria param : params) {
if (param.getOperation().equals(">")) {
predicate = builder.and(predicate,
builder.greaterThan(r.get(param.getKey()),
param.getValue().toString()));
} else if (param.getOperation().equals("<")) {
predicate = builder.and(predicate,
builder.lessThan(r.get(param.getKey()),
param.getValue().toString()));
} else if (param.getOperation().equals(":")) {
predicate = builder.and(predicate,
builder.equal(r.get(param.getKey()), param.getValue()));
}
}
query.where(predicate);
List<User> result = entityManager.createQuery(query).getResultList();
return result;
}
but when I pass a Date type parameter I got the following error:
"Parameter value [1539122400000] did not match expected type [java.util.Date (n/a)]"
Can anyone pls show me an implementation to handle dates as well. Thanks
Ensure you haven't stripped down your parameter to a String. See if you can avoid using the .toString()
I have a query I would write in older Hibernate (utilising the SessionFactory bean). However, I have shifted to Spring Boot and am now utilizing the JPA 2 which is essentially seems like a layer of abstraction over Hibernate. Could anyone guide me on as to how to add restrictions? I believe I will now have to use the EntityManager bean with JPA. Here is the older query.
#Override
#SuppressWarnings("unchecked")
public List<Party> queryPartiesBetweenDates(Date startDate, Date endDate, String sortBy, Integer count) {
Criteria criteria = getCurrentSession().createCriteria(Party.class);
if (startDate != null) {
criteria.add(Restrictions.ge("startDate", startDate));
}
if (endDate != null) {
criteria.add(Restrictions.lt("endDate", endDate));
}
if (count != null) {
criteria.setMaxResults(count);
}
if (sortBy == null || !sortBy.equals("distance")) {
criteria.addOrder(Order.asc("startDate"));
}
return criteria.list();
}
Thanks!
The CriteriaBuilder is quite a bit more verbose than the native hibernate Restriction API, the primary reason is that it is fully typed when you use the MetaModel, which means the code will not compile if the type or name of a column causing a query to be invalid.
Here is an example that does not use the generated MetaModel classes, and mostly resembles the old Hibernate code.
EntityManager em = emf.createEntityManager(); // or injected
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Party> query = cb.createQuery(Party.class);
Root<Party> partyRoot = query.from(Party.class);
query.select(partyRoot);
Predicate predicate = null;
Path<Date> startDatePath = partyRoot.<Date>get("startDate");
if (startDate != null) {
predicate = cb.greaterThanOrEqualTo(startDatePath, startDate);
}
if (endDate != null) {
Predicate additionalPredicate = cb.lessThanOrEqualTo(partyRoot.<Date>get("endDate"), startDate);
if (predicate == null) {
predicate = additionalPredicate;
} else {
predicate = cb.and(predicate, additionalPredicate);
}
}
query.where(predicate);
if (sortBy == null || !sortBy.equals("distance")) {
query.orderBy(cb.asc(startDatePath));
}
return em.createQuery(query).setMaxResults(count).getResultList();
If you have many Predicates, it may be a good idea to add them to a list, or create a utility method for handling them.
Im using Spring data Jpa specification and I would like to obtain non repetead registries, so I use distinct in query like that.
public static Specification<Actividad> criteriosConsultaGruposHandling(final String cif, final String razonSocial, final String nombreComercial, final List<Long> idsHandling) {
Specification<Actividad> spec = new Specification<Actividad>() {
#Override
public Predicate toPredicate(Root<Actividad> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
query.distinct(true);
List<Predicate> predicates = new ArrayList<Predicate>();
if(cif != null && !cif.equals("")){
predicates.add(cb.like(cb.upper(root.join(Actividad_.empresa).get(Empresa_.cif)), "%" + cif.toUpperCase() + "%" ));
}
if(razonSocial != null && !razonSocial.equals("")){
predicates.add(cb.like(cb.upper(root.join(Actividad_.empresa).get(Empresa_.razonsocial)), "%" + razonSocial.toUpperCase() + "%" ));
}
if(nombreComercial != null && !nombreComercial.equals("")){
predicates.add(cb.like(cb.upper(root.join(Actividad_.empresa).get(Empresa_.nombrecomercial)), "%" + nombreComercial.toUpperCase() + "%" ));
}
if(idsHandling != null && !idsHandling.isEmpty()){
Predicate pred = cb.conjunction();
pred.getExpressions().add(root.join(Actividad_.grupohandlingactividads).get(Grupohandlingactividad_.grupohandling).get(Grupohandling_.idsubgrupo).in(idsHandling));
predicates.add(pred);
}
return cb.and(predicates.toArray(new Predicate[0]));
}
};
return spec;
}
}
but when I do the findAll with the specification and pagerequest the page resulting from query is bad
Specification<Actividad> specificationActividad = ActividadSpecifications.criteriosConsultaGruposHandling(actividadForm.getEmpresa().getCif(),
actividadForm.getEmpresa().getRazonsocial(), actividadForm.getEmpresa().getNombrecomercial(),listaGruposAsignados);
PageRequest pageRequest = new PageRequest(actividadForm.getPagina() -1, actividadForm.getMaxRegistros(), actividadForm.getOrder());
Page<Actividad> resultados = actividadRepository.findAll(specificationActividad, pageRequest);
When I call findAll without pageRequest parameter I obtain rows without repetitions but when I call it with it donĀ“t elimininate the repetitive rows.
Anyone could help me? Thanks in advance!
I get the error "Cannot create TypedQuery for query with more than one return using requested result type"
for the following query using JPA on Glassfish, any ideas what is wrong here? I want to get the latest debit record with a certain debit status.
entityManager.createQuery("select dd, MAX(dd.createdMillis) from T_DEBIT dd" +
" where dd.debitStatus in (:debitStatus)" +
" and dd.account = :account" , Debit.class)
.setParameter("debitStatus", false)
.setParameter("account", account)
.getSingleResult();
A generic parameter is normally specified for a TypedQuery. If you declared a TypedQuery you would use an Object[] as the generic parameter for the TypedQuery, since you are projecting columns and not returning a complete entity.
However, since you have not declared a TypedQuery (your using a concise coding style), you need to change Debit.class to Object[].class since your not selecting an object, but instead only two fields.
Object[] result = entityManager.createQuery("select dd, MAX(dd.createdMillis) from T_DEBIT dd" +
" where dd.debitStatus in (:debitStatus)" +
" and dd.account = :account" , Object[].class) //Notice change
.setParameter("debitStatus", false)
.setParameter("account", account)
.getSingleResult();
Executing this query will return a Object[] where each index in the Object[] corresponds with a field in your select statement. For example:
result[0] = dd
result[1] = max(dd.createdMillis)
To avoid using the Object[] you could create a new class to retrieve these values in a more strongly typed fashion. Something like:
public class Result {
String dd;
Date createdMillis;
public Result(String dd, Date createdMillis) {
super();
this.dd = dd;
this.createdMillis = createdMillis;
}
public String getDd() {
return dd;
}
public void setDd(String dd) {
this.dd = dd;
}
public Date getCreatedMillis() {
return createdMillis;
}
public void setCreatedMillis(Date createdMillis) {
this.createdMillis = createdMillis;
}
}
Then in your JPQL statement you could call the constructor:
Result result = entityManager.createQuery("select NEW fully.qualified.Result(dd, MAX(dd.createdMillis)) from T_DEBIT dd" +
" where dd.debitStatus in (:debitStatus)" +
" and dd.account = :account" , Result.class)
.setParameter("debitStatus", false)
.setParameter("account", account)
.getSingleResult();
Recently, I have blogged about this exact topic. I encourage you to view this video tutorial I created: https://tothought.cloudfoundry.com/post/16