Find by many columns - spring

I want to perform a query like this:
SELECT * FROM table WHERE
col1 = 'value1' OR
col2 = 'value2' OR
col3 = 'value3' OR
col4 = 'value4';
Using Spring's JpaRepository I should use something like this:
List<MyEntity> findByCol1OrCol2OrCol3OrCol4(
String col1, String col2, String col3, String col4
);
Now imagine I don't want to check 4 columns but 10 or 20, the method name would be really long!
I have seen in this answer that I could use Specification to search the same text in many columns, but I want different text values for every one.
Is there any way to shorten the find method and add the columns (and the corresponding values) dynamically?
Thanks

This can be achieved via Specifications and Map(attribute name, value). Code sample (works for any data):
Map<String, Object> whereClause = new HashMap<>();
whereClause.put("lastname", "Super-Lastname");
whereClause.put("firstname", "Firstńame");
userRepository.findOne(Specifications.where(
CommonSpecifications.attributesEquals(whereClause))
)
public static <T> Specification<T> attributesEquals(Map<String, Object> whereClause) {
return (root, query, builder) -> builder.or(root.getModel().getDeclaredSingularAttributes().stream()
.filter(a -> whereClause.keySet().contains(a.getName()))
.map(a -> builder.equal(root.get(a.getName()), whereClause.get(a.getName())))
.toArray(Predicate[]::new));
}

Related

List of multiple column condition in query (kind of batch)

when trying to search with single record then this query works
#Query(value = "select * from table t where t.column1 = :column1 and t.column2 = :column2 and t.column3 = :column3")
Flux<Invoice> findByMultipleColumn(#Param("column1”) String column1, #Param("column2”) String column2, #Param("column3”) String column3);
But when I have list of criterias instead of a single row condition then I have to loop over the list of criterias & call the above query multiple times which is not feasible solution.
Sudo code
for (Criteria criteria : criteriaList) {
repository.findByMultipleColumn(criteria.getColumn1(), criteria.getColumn2(), criteria.getColumn3());
}
What I am trying to find a way to solve the above query for multiple LIST of all the 3 column criteria pair, something like below (this is not working solution)
#Query(value = "select * from table t where t.column1 = :column1 and t.column2 = :column2 and t.column3 = :column3")
Flux<Invoice> findByMultipleColumn(#Param List<Table> table);
Is there any way somehow we can try to achieve the above case?
Would be doable if column1, 2 and 3 were Embedded, then you could do
#Query(select * from Entity where embeddedProperty in (:values))
Flux<Entity> findByEmbeddedPropertyIn(Collection<EmbeddedClas> values);
Which would generate the following native SQL clause
Where (column1, column2, column3) in ((x, y, z), ...)
If you don't want to pack these fields i to an embeddable class, you can also try to do a workaround
#Query(select * from Entity where Concat(column1, ';', column2, ';', column3) in (:parametersConcatrenatedInJava)
Flux<Entity> findBy3Columns(Collection<String> parametersConcatrenatedInJava);
It's ofcourse not bulletproof, all three columns could have ";" as their values, this might be problematic if their type is not string, etc.
Edit.:
Third option is to use specification api. Using the criteria builder you can concatenate multiple and / or queries. And pass that specification as an argument to the repository that extends JpaSpecificationExecutor (if you're fetching whole entities) or an entity manager if you're using projections. Read more about specifications

EclipseLink with Oracle: "limit by rownum" does not use index

we're facing performance issues with EclipseLink 2.7.7 when accessing Oracle 12.1 tables with paging. Investigation showed that Oracle does not use its indexes with EclipseLink paging.
I've extracted the sql sent to the database and was able to reproduce the issue using a database tool (DataGrip).
Example:
-- #1: without paging
SELECT col1 AS a1, col2 AS a2, col3 AS a3, ...
FROM <TABLE>
WHERE colN > to_timestamp('2021-12-08', 'yyyy-mm-dd'))
ORDER BY col1 DESC;
Explain plan shows that the index on colN is used. Fine.
When the same query is executed with paging, the original query is wrapped in two subselects:
-- #2 with EclipseLink paging
SELECT * FROM (
SELECT a.*, ROWNUM rnum FROM (
SELECT col1 AS a1, col2 AS a2, col3 AS a3, ...
FROM <TABLE>
WHERE colN > to_timestamp('2021-12-08', 'yyyy-mm-dd'))
ORDER BY col1 DESC
) a WHERE ROWNUM <= 100
) WHERE rnum > 0;
For this query, the explain plan shows that the index on colN is not used.
As a result, querying a table with millions of rows takes 50-90 seconds (depending on the hardware).
Side note: on my test database, this query returns 0 records since colN values are before 2021-12-08.
Oracle 12c introduced the OFFSET/FETCH syntax:
-- #3
SELECT col1 AS a1, col2 AS a2, col3 AS a3, ...
FROM <TABLE>
WHERE colN > to_timestamp('2021-12-08', 'yyyy-mm-dd'))
ORDER BY col1 DESC
OFFSET 0 ROWS FETCH NEXT 100 ROWS ONLY;
Using this syntax, indexes are at least sometimes used as expected. When they are used, execution time is below 1s which is acceptable.
However, I could not figure out how to convince EclipseLink to use this syntax.
If ORDER BY col1 DESC is removed from the original paged query (#2), the index is used the query returns fast enough. However, it will not return the desired records, so that does not help.
How can I implement performant paged queries using EclipseLink and Oracle 12?
How can I force oracle to use the index on colN when using paging and order by?
The OraclePlatform printSQLSelectStatement method is responsible for building the query used, nesting the queries to use rownum for the query you've seen. To use a new form, you would extend one of the OraclePlatform classes you are using (maybe Oracle12Platform) and override that method to append the syntax you want instead. Something like:
#Override
public void printSQLSelectStatement(DatabaseCall call, ExpressionSQLPrinter printer, SQLSelectStatement statement) {
int max = 0;
int firstRow = 0;
ReadQuery query = statement.getQuery();
if (query != null) {
max = query.getMaxRows();
firstRow = query.getFirstResult();
}
if (!(this.shouldUseRownumFiltering()) || (!(max > 0) && !(firstRow > 0))) {
super.printSQLSelectStatement(call, printer, statement);
return;
}
call.setFields(statement.printSQL(printer));
printer.printString("OFFSET ");
printer.printParameter(DatabaseCall.MAXROW_FIELD);
printer.printString(" ROWS FETCH NEXT ");
printer.printParameter(DatabaseCall.FIRSTRESULT_FIELD);
printer.printString(" ROWS ONLY");
call.setIgnoreFirstRowSetting(true);
call.setIgnoreMaxResultsSetting(true);
}
You would then specify your custom OraclePlatform class using a persistent property:
<property name="eclipselink.target-database" value="my.package.MyOracle12Platform"/>
If something like that works for you, please submit it as an enhancement request - though you might want to work some way to use the old behaviour into it, as the performance differences you've experienced might depend on the query/data involved.
Thanks to #Chris, I came up with the following Oracle12Platform. This solution currently ignores "Bug #453208 - Pessimistic locking with query row limits does not work on Oracle DB". See OraclePlatform.printSQLSelectStatement for details):
public class Oracle12Platform extends org.eclipse.persistence.platform.database.Oracle12Platform {
/**
* the oracle 12c `OFFSET x ROWS FETCH NEXT y ROWS ONLY` requires `maxRows` to return the row count
*/
#Override
public int computeMaxRowsForSQL(final int firstResultIndex, final int maxResults) {
return maxResults - max(firstResultIndex, 0);
}
#Override
public void printSQLSelectStatement(final DatabaseCall call, final ExpressionSQLPrinter printer, final SQLSelectStatement statement) {
int max = 0;
int firstRow = 0;
final ReadQuery query = statement.getQuery();
if (query != null) {
max = query.getMaxRows();
firstRow = query.getFirstResult();
}
if (!(this.shouldUseRownumFiltering()) || (!(max > 0) && !(firstRow > 0))) {
super.printSQLSelectStatement(call, printer, statement);
} else {
statement.setUseUniqueFieldAliases(true);
call.setFields(statement.printSQL(printer));
if (firstRow > 0) {
printer.printString(" OFFSET ");
printer.printParameter(DatabaseCall.FIRSTRESULT_FIELD);
printer.printString(" ROWS");
call.setIgnoreFirstRowSetting(true);
}
if (max > 0) {
printer.printString(" FETCH NEXT ");
printer.printParameter(DatabaseCall.MAXROW_FIELD); //see #computeMaxRowsForSQL
printer.printString(" ROWS ONLY");
call.setIgnoreMaxResultsSetting(true);
}
}
}
}
I had to override computeMaxRowsForSQL in order to get the row count instead of "lastRowNum" when calling printer.printParameter(DatabaseCall.MAXROW_FIELD);
I also try to deal with missing firstRow xor maxResults

how to use JPQL query to count values from a table & combine with an other table related data?

I have two tables X and Y. In Table X (Oracle sql), an unique column(primary key) code along other columns in X table.. code column may have some records in table Y which have column code_id. I want to get count of rows in table Y for code with code and other columns in table Y
and I have springboot entity called Entity I want to map results to using jpql so I want the query in JPQL:
public class Entity {
private int id;
private char code;
private String name;
// constructor & setters / getters
}
and Y table have entity Counter
public class Counter {
private int codeid;
}
I want to use jpql query equivalent to this Oracle sql query
select x.*,
(select count(*) from Y y where x.code = y.code_id) as theCount
from X x ORDER BY theCount desc , x.name asc ;
Example:
Code "A" has 3 entries, Code "B" has 2 entries and code "C" has 0 entries in table Y.
code name count
A name1 3
B name2 2
C name3 0
I did some assumptions because I miss your project code. Hope my example will fit your needs. It is not the same SQL but it is still just 1 statement producing the same type of output.
Primary entity Code having collection of Counters:
#Data
#Entity
public class Code {
#Id
private Integer id;
private String code;
private String name;
#OneToMany
#JoinColumn(name = "CODE_ID")
private List<Counter> counterList;
}
#Data
#Entity
public class Counter {
#Id
private Integer id;
}
Spring Data repository:
public interface CodeRepository extends JpaRepository<Code, Integer> {
#Query("select c.code, c.name, count(l) as amount from Code c join c.counterList l group by c.code, c.name")
List<Object[]> getSummary();
}
It returns:
A, TESTNAME1, 3
B, TESTNAME2, 2
In case following is inserted into database:
INSERT INTO CODE (ID, CODE, NAME) VALUES (1, 'A', 'TESTNAME1');
INSERT INTO COUNTER (ID, CODE_ID) VALUES (123,1);
INSERT INTO COUNTER (ID, CODE_ID) VALUES (124,1);
INSERT INTO COUNTER (ID, CODE_ID) VALUES (125,1);
INSERT INTO CODE (ID, CODE, NAME) VALUES (2, 'B', 'TESTNAME2');
INSERT INTO COUNTER (ID, CODE_ID) VALUES (234,2);
INSERT INTO COUNTER (ID, CODE_ID) VALUES (235,2);
This is how the result is produced:
codeRepository.getSummary()
.forEach(sum -> System.out.println(sum[0] + ", " + sum[1] + ", " + sum[2]));

jdbctemplate equivalent of following query

I have a long list of argument which I need to send to oracle database. I was able to do it by splitting the query but I am unable to find a way to do similar using jdbcTemplate. my query is:
select name,age from person where personId in (A1,F2,D3...G900)
or personId in (A901, C902 , ... R1800)
or personId in (A1801,G1802 .... H2700)
or personId in (P2701, G2702 ... R3600)
or since oracle allow more than 1000 touple but does not allow in so JDBC equivalent for
SELECT field1, field2, field3
FROM table1
WHERE (1, name) IN ((1, value1), (1, value2), (1, value3),.....(1, value5000));
List<Map<String, Object>> findPeeps(List<Long> personIds) {
String sql = "select name,age from person where personId in (:personIds)";
return namedParameterJdbcTemplate.queryForList(sql, new MapSqlParameterSource("personIds", personIds));
}
As #zaki said you can use that but the error that you are getting is from Oracle since there is limit of records you can put in WHERE IN clause. You can try something like this
insert into TEMP values ( ... );
select * from T where (a,b) in (select x,y from temp);
delete from TEMP;

Select multiple fields using findByField in JPA

I am trying to select mulitple values from a table where input can be any of the 5 columns of the table or a combination of those fields.
In order to use findByField in JPA I have to many combinations of findByField and call them selectively by checking which field is blanks.
Is there an easier solution to this?
I have 5 columns in a table that I would like to query on.
If user enters all 5 then i want to select using all 5 fields like below.
select * from table where field1 = :field1 and field2 = :field2 and field3 = :field3 and field4 = :field4 and field5 = :field5;
But if user enters only 2 fields, then I just want to query on those 2 fields like this.
select * from table where field3 = :field3 and field5 = :field5.
In JPA we can do findByField1AndField2AndField3
But i am having for 5 different input possibilities i will have to create like 120 possibilities.
So is there any simpler solution?
Thank you!
It seems there is no automatic generator to build all possible combinations of JPA queries, but you should use the power of JPA dynamic queries like this :
Build predicates for all the fields and apply them on the basis of user input.
public List<Employee> findByCriteria(String employeeName){
return employeeDAO.findAll(new Specification<Employee>() {
#Override
public Predicate toPredicate(Root<Employee> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
List<Predicate> predicates = new ArrayList<>();
if(employeeName!=null) {
predicates.add(criteriaBuilder.and(criteriaBuilder.equal(root.get("employeeName"), employeeName)));
}
return criteriaBuilder.and(predicates.toArray(new Predicate[predicates.size()]));
}
});
}
Helpful Link: https://javadeveloperzone.com/spring/spring-jpa-dynamic-query-example/
You can use a custom #Query in your #Repository interface and add as many params as you need.
#Query(value="SELECT *
FROM table
WHERE (:field1 IS NULL
OR field1 = :field1)
AND (:field2 IS NULL
OR field2 = :field2)",
nativeQuery=true)
public Table findMatchingTable(#Param("field1") String field1, #Param("field2") String field2)

Resources