Spring Data JDBC and specification pattern - spring-data

We got an Spring Boot 2 application using Spring Data JDBC. Its displaying a list of orders which needs to be filtered for many different aspects and combinations. Therefore, our Repository is growing more and more, because for new combinations find* methods are added or new parameters are applied.
public interface OrderJdbcRepository extends PagingAndSortingRepository<OrderEntity, Long> {
String ORDER_FILTER = "WHERE ..."; // big custom sql statement
String NOT_BILLED = "...";
Optional<OrderEntity> findById(String orderId);
#Query(
"SELECT o.* FROM `ORDER` AS o "
+ ORDER_FILTER
+ "ORDER BY o.ORDER_ID "
+ "LIMIT :limit OFFSET :offset"
)
List<OrderEntity> findFiltered(String customerId, LocalDate startDate,
LocalDate endDate, ...some other filter criterias, int limit, long offset);
#Query(
"SELECT o.* FROM `ORDER` AS o "
+ ORDER_FILTER + NOT_BILLED // additional filter for not billed orders
+ "ORDER BY o.ORDER_ID "
+ "LIMIT :limit OFFSET :offset"
)
List<OrderEntity> findFilteredAndNotBilled(String customerId, LocalDate startDate,
LocalDate endDate, ...some other filter criterias, int limit, long offset);
// many other methods which looks similar
I'm afraid of that this leads to unmaintainable code. Therefore I'm trying to use the composite specification pattern like its described in the link. For example i just need to combine CustomerIdSpecification, DateBetweenSpecification and NotBilledSpecification. However, the linked tutorial is for Spring Data JPA with Criteria API, which does not exist in Spring Data JDBC. So I'm thinking of how i could solve this problem in Spring Data JDBC.
I need a way to define the custom sql in each specification and pass it to my OrderJdbcRepository. An alternative I thought is to use JdbcTemplate for building a custom sql and accessing my data by hand. Are there other solutions, which I did not think of?
Thanks so far!

Related

Is it possible to return custom Java objects combining multiple aggregates in Spring Data JDBC?

I have multiple aggregate classes, such as Request, Scribe, Candidate, and Exam.
Sample schema:
Request (id, scribe_id, candidate_id, exam_id, status)
Scribe (id, name)
Candidate (id, name)
Exam (id, name, schedule)
As you can see, Request table has references to Scribe, Candidate, and Exam tables.
For one of the requirements, I need to return all requests based on a condition by including all the corresponding details of scribe, candidate, and exam.
For this, the query in my repository class will be similar to the following:
SELECT r.id, r.status, c.name, s.name,
e.schedule, e.name
FROM request r
JOIN candidate c ON r.candidate=c.id
JOIN scribe s ON r.scribe=s.id
JOIN exam e ON r.exam=e.id
WHERE <some-condition>
Now, is there a way to map the result of this query directly to a custom Java object and return the same in Spring Data JDBC?
I believe another alternative is to use the Spring JDBC template.
Curious, any out-of-the-box support from Spring Data JDBC?
Thanks.
I am able to return custom Java object by setting rowMapperClass value of org.springframework.data.jdbc.repository.query.Query annotation. For this need to define RowMapper for custom Java object.
Changes look similar to the following:
public class RequestResourceRowMapper implements RowMapper<RequestResource> {
#Override
public RequestResource mapRow(ResultSet resultSet, int rowNumber) throws SQLException { ... }
}
In repository class, need to set rowMapper value.
#Query(value = """
SELECT r.id, r.status, c.name, s.name,
e.schedule, e.name
FROM request r
JOIN candidate c ON r.candidate=c.id
JOIN scribe s ON r.scribe=s.id
JOIN exam e ON r.exam=e.id
WHERE <some-condition>
""",
rowMapperClass = RequestResourceRowMapper.class)
List<RequestResource> searchRequestResources(...);
This could have even been possible without using a custom row mapper as well, but in that case, you will have to assign different names to the columns across the tables. You could have defined a simple class and defined all the fields in there and for mapping the java fields with the corresponding columns in the table, you could have used the #Column attributes example:
public class RequestData {
#Column("id")
private Integer requestId;
#Column("scribe_id")
private String scribeId;
#Column("candidate_id")
private Integer candidateId;
#Column("scribe_name")
private String scribeName;
#Column("candidate_name")
private String candidateName;
#Column("exam_name")
private String examName;
#Column("exam_schedule")
private String examSchedule;
}
However, for such case, you need to have different column names across the schema's which might not be possible in your case as you have same column names in multiple schemas.

Dynamic like query in spring data jpa

Can a dynamic query be written in spring data rest as follows. If not then how to achieve a similar functionality:
#Query("select s from Screen s where s.#searchColumn like:searchValue%")
#RestResource(path="byString")
Page findAll(
#Param("searchColumn") String searchColumn,
#Param("searchValue") String searchValue,
Pageable pageable);
Solved
Repo
#Query("select o from Screen o where "
+ "(o.screenName like :val% and :prop = 'screenName') or "
+ "(o.address like :val% and :prop = 'address')")
#RestResource(path="byString")
Page findAll(
#Param("prop") String prop,
#Param("val") String val,
Pageable pageable);
Query:
/api/screens/search/byString?prop=address&val=a
Tested it with prop=address as well as prop=screenName. Working :)
No. Spring Data JPA support only entityName variables inside SpEL based query templates
For dynamic queries use:
Specifications
Query by Example
Querydsl Extension

Calling StoredProcedure using JPA in Spring Boot

I am trying to call a stored procedure, which is built in mysql, in my Spring boot app using JPA. My stored procedure returns the result which cant be contain in single model as it fetches data from combination of tables.
I can do this with "call " but i guess that is not JPA's way. COuld you please let me know what is the best way to do it?
In case you're using plain JPA you need to do a native query call. Something like below.
Query q = em.createNativeQuery("select my_store_pro(?, ?)");
List<Object[]> results = q.getResultList();
for (Object[] a : results) {
System.out.println("result " + a[0] + " " + a[1]);
}
If you're using Spring Data repositories then you want something like below.
#Query(nativeQuery = true, value = "select my_store_pro(?, ?)")
Date callMyStoreProc(int val1, int val2);

Reduce number of queries for JPQL POJO containing an entity

Entity relation: Transaction(#ManyToOne - eager by default) -> Account
String sql = "SELECT new com.test.Pojo(t.account, SUM(t.value)) FROM Transaction t GROUP BY t.account";
List list = entityManager.createQuery(sql).getResultList();
By default JPA using Hibernate implementation will generate 1 + n queries. The n queries are for lazy loading of the account entities.
How can I make this query eager and load everything with a single query? The sql equivalent would be something like
SELECT account.*, SUM(t.value) FROM transactions JOIN accounts on transactions.account_id = accounts.id GROUP BY account.id
, a syntax that works well on PostgreSQL. From my findings Hibernate is generating a query that justifies the lazy loading.
SELECT account.id, SUM(t.value) FROM transactions JOIN accounts on transactions.account_id = accounts.id GROUP BY account.id
Try marking the #ManyToOne field as lazy:
#ManyToOne(fetch = FetchType.LAZY)
private Account account;
And change your query using a JOIN FETCH of the account field to generate only one query with all you need, like this:
String sql = "SELECT new com.test.Pojo(acc, SUM(t.value)) "
+ "FROM Transaction t JOIN FETCH t.account acc GROUP BY acc";
UPDATE:
Sorry, you're right, the fetch attribute of #ManyToOne is not required because in Hibernate that is the default value. The JOIN FETCH isn't working, it's causing a QueryException: "Query specified join fetching, but the owner of the fetched association was not present".
I have tried with some other approaches, the most simple one that avoids doing n + 1 queries is to remove the creation of the Pojo object from your query and process the result list, manually creating the objects:
String hql = "SELECT acc, SUM(t.value)"
+ " FROM " + Transaction.class.getName() + " t"
+ " JOIN t.account acc"
+ " GROUP BY acc";
Query query = getEntityManager().createQuery(hql);
List<Pojo> pojoList = new ArrayList<>();
List<Object[]> list = query.getResultList();
for (Object[] result : list)
pojoList.add(new Pojo((Account)result[0], (BigDecimal)result[1]));
Well PostgreSQL (And any other SQL database too) will block you from using mentioned query: you have to group by all columns of account table, not by id. That is why Hibernate generates the query, grouping by ID of the account - That is what is intended to be, and then fetching the other parts. Because it cannot predict in general way, what else will be needed to be joined and grouped(!!!), and in general this could produce situation, when multiple entities with the same ID are fetched (just create a proper query and take a look at execution plan, this will be especially significant when you have OneToMany fields in your Account entity, or any other ManyToOne part of the Account entity) that is why Hibernate behaves this way.
Also, having accounts with mentioned IDs in First level cache, will force Hibernate to pick them up from that. Or IF they are rarely modified entities, you can put them in Second level cache, and hibernate will not make query to database, but rather pick them from Second level cache.
If you need to get those from database in single hint, but not use all the goodness of Hibernate, just go to pure JPA Approach based on Native queries, like this:
#NamedNativeQuery(
name = "Pojo.groupedInfo",
query = "SELECT account.*, SUM(t.value) as sum FROM transactions JOIN accounts on transactions.account_id = accounts.id GROUP BY account.id, account.etc ...",
resultClass = Pojo.class,
resultSetMapping = "Pojo.groupedInfo")
#SqlResultSetMapping(
name = "Pojo.groupedInfo",
classes = {
#ConstructorResult(
targetClass = Pojo.class,
columns = {
#ColumnResult(name = "sum", type = BigDecimal.class),
/*
* Mappings for Account part of entity.
*/
}
)
}
)
public class Pojo implements Serializable {
private BigDecimal sum;
/* .... */
public Pojo(BigDecimal sum, ...) {}
/* .... */
}
For sure this will work for you well, unless you will use the Account, fetched by this query in other entities. This will make Hibernate "mad" - the "entity", but not fetched by Hibernate...
Interesting, the described behaviour is as if t instances are returned from the actual query and t.account association in the first argument of Pojo constructor is actually navigated on t instances when marshalling results of the query (when creating Pojo instances from the result rows of the query). I am not sure if this is a bug or intended feature for constructor expressions.
But the following form of the query should work (no t.account navigation in the constructor expression, and no join fetch without the owner of the fetched association because it does not make sense to eagerly initialize something that is not actually returned from the query):
SELECT new com.test.Pojo(acc, SUM(t.value))
FROM Transaction t JOIN t.account acc
GROUP BY acc
EDIT
Very good observation by Ilya Dyoshin about the group by clause; I completely oversaw it here. To stay in the HQL world, you could simply preload all accounts with transactions before executing the query with grouping:
SELECT acc FROM Account acc
WHERE acc.id in (SELECT t.account.id FROM Transaction t)

How to write JPA Query to the equivalent mysql query

I want to write equivalent JPA query to the following mysql query
select active_package,sum(duration),sum(charge),count(*)
from User
where call_type="MO"
and start_date between '2012-02-01' and '2012-02-09'
group by active_package;
For JPA Query the corresponding Attributes are below.
activePackage,callType,duration,charge,startDate
Entity class is User.
I want to use the createQuery() of JPA.
Can any one tell me or give me the link where can i find the solution for this.
Try this one, it should work, if not, please comment, we will get it work :).
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Tuple> cq = cb.createQuery(Tuple.class);
Root<User> entity = cq.from(User.class);
cq.multiselect(entity.get("activePackage"),
cb.sum(entity.get("duration").as(Long.class)),
cb.sum(entity.get("charge").as(Double.class),
cb.count(entity).as(Long.class)));
cq.where(cb.equal(entity.get("callType"), "MO"),
cb.between(entity.get("startDate").as(Date.class),
new Date(), new Date()));
cq.groupBy(entity.get("activePackage"));
List<Tuple> resultList = entityManager.createQuery(cq).getResultList();
for (Tuple result : resultList) {
System.out.println(result.get(0) + " " + result.get(1)
+ " " + result.get(2) + " " + result.get(3));
}
Also if you want to filter only by date, but have timestamp in your model, you can check this Compare date's date part only with Timestamp in Hibernate answer.
Also JPA provides constructing result classes as return values, so you can group your columns. Read more.