Pass varargs from Java code to SQL or PL/SQL - java

I need to bind at maximum 8 variables. Each one of them could be null.
Is there any recommended way to achieve this? I know that I could simply check for null, but this seems tedious.
Additional details:
I'm going to call this sql from java code. It may be written using JPA 2.0 Criteria API, but most likely it's going to be a native query. The database is Oracle 10g, so I think I could make use of PL/SQL as well.
Edit1:
Maybe the title is a bit misleading, so I'll try to elaborate.
The resulting SQL would be something like:
...
WHERE var1 = :var1
AND var2 = :var2
...
AND var = :var8
Now I need to bind parameters from java code in the way like:
nativeQuery.setParameter("var1", var1)
...
nativeQuery.setParameter("var8", var8)
Some parameters could be null, so there is no need to bind them. But I see no way I can omit them in SQL.
Edit2:
I'm expecting to see SQL or PL/SQL procedure in your answers (if it's ever possible without null checking).
In fact, all of these variables are of the same type. I think it's not possible to find a solution using ANSI SQL, but maybe there are some PL/SQL procedures which allow to work with varargs?

The use of a criteria query is appropriate in this case, because if I understood correctly, you need to construct the SQL query dynamically. If all the variables except var1 are null, the where clause would be
where var1 = :var1
and if all variables except var2 and var5 are non null you would have
where var2 = :var2 and var5 = :var5
Is that right?
If so, then do what you plan to do, and construct the query dynamically using a criteria query. Something like this must be done:
CriteriaBuilder builder = em.getCriteriaBuilder();
Predicate conjunction = builder.conjunction();
if (var1 != null) {
conjunction = builder.and(conjunction,
builder.equal(root.get(MyEntity_.var1),
var1));
}
if (var2 != null) {
conjunction = builder.and(conjunction,
builder.equal(root.get(MyEntity_.var2),
var2));
}
...
criteria.where(conjunction);

You don't specify the type of the objects you want to pass. So in this example I'm considering you will pass Object.
#Test(expected=IllegalArgumentException.class)
public void testMyMethod() {
List<Object> testList = new ArrayList<Object>();
testList.add("1");
testList.add("2");
testList.add(3);
myMethod(testList);
}
public void myMethod(List<Object> limitedList) {
final int MAX_SIZE = 2;
if (limitedList.size() > MAX_SIZE) {
throw new IllegalArgumentException("Size exceeded");
}
//my logic
}
In this example I'm passing the arguments as a List of Objects but you could use array (varargs) or another type of collection if you need to. If the client sends me more than the expected objects it will throw an IllegalArgumentException.
Also if you don't want to throw an exception you could just continue and iterate the list to bind the parameters but using the list size or MAX_SIZE as your limit. For example:
public void myMethod2(List<Object> limitedList) {
final int MAX_SIZE = 2;
int size = MAX_SIZE;
if (limitedList.size() < MAX_SIZE) {
size = limitedList.size();
}
//Iterate through the list
for (int i = 0; i < size; i++) {
Object obj = limitedList.get(i);
//Logic to bind the obj to the criteria.
}
}

Related

How to make a dynamic query using queryDSL when the number of parameters to be entered changes?

I want to attach an eq() conditional statement to booleanExpression using or() as much as the list length.
What should I do?
For example
QStudyTime.studyTime.user.id.eq(rivalId).or(QStudyTime.studyTime.user.id.eq(rivalId2)).or(QStudyTime.studyTime.user.id.eq(rivalId3))
The above code is handled by method chaining when the number of rivaled is 3.
But my system doesn't have a fixed number of rivalId's.
What should I do in this case?
Can it be handled with a for statement?
Or is BooleanExpression a way to attach using the + operator just like String?
I solved it like this.
private BooleanExpression method(input parameter) {
List<Long> rivalIdList = new ArrayList<>();
for (int i = 0; i < rivalList.size(); i++) {
rivalIdList.add(rivalList.get(i).getRival().getId());
}
return QStudyTime.studyTime.user.id.in(rivalIdList);`
}

JOOQ "IN" Query throws null pointer exception

When we try to fetch data with Null values
field(TABLE_NAME.COLUMN_NAME.in(null))
with IN clause
getting null pointer exception.
Maybe because of this.
#Override
public final Condition in(Collection<?> values) {
Field<?>[] fields = new Field[values.size()];
Iterator<?> it = values.iterator();
for (int i = 0; it.hasNext(); i++)
fields[i] = Tools.field(it.next(), this);
return in(fields);
}
In the database, we can provide null in IN clause.
There is an existing "won't fix" issue in jooq https://github.com/jOOQ/jOOQ/issues/3867
There are some alternatives:
check null before IN(Cant do in my case its a really big select statement)
So if I want to make this possible is there any other workaround.
PS: On a similar note "eq" works perfectly fine:
#Override
public final Condition equal(Field<T> field) {
return compare(EQUALS, nullSafe(field, getDataType()));
}
Edit: 'field(TABLE_NAME.COLUMN_NAME.in(null))' here null is a collection.
Your example code doesn't compile:
TABLE_NAME.COLUMN_NAME.in(null)
There are 5 overloads of this in() method in jOOQ 3.14, and as such, you cannot pass the null literal to the in() method. Your real client code may be using a local variable like this:
Collection<?> collection = null;
TABLE_NAME.COLUMN_NAME.in(collection)
There might be a case for when this should behave the same as passing an empty collection, such as Collections.emptyList(), but this isn't what you seem to want. You probably want to pass actual null values inside of that collection, which you can do:
TABLE_NAME.COLUMN_NAME.in(1, null, 2)
But why would you do it? SQL implements three valued logic, meaning that NULL values have no effect in IN predicates, while they have an unintuitive, hardly desired effect in NOT IN predicates (the entire predicate becomes NULL)

Efficient way of mimicking hibernate criteria on cached map

I have just wrote a code to cach a table in the memory (simple java hashmap). Now one of the code that i am trying to replace is the find the objects based on criteria. it receives multiple field parameters and if those fields are not empty and not null, they were being added as part of hibernate query criteria.
To replace this, what i am thinking to do is
For each valid param (not null and no empty) I will create a HashSet which will satisfy this criteria.
Once i am done making hashsets for all valid criteria, I will call Set.retainAll(second_set) on all sets. So that at the end, I will have only that set which is intersection of all valid criteria.
Does it sound like the best approach or is there any better way to implement this ?
EDIT
Though, My original post is still valid and I am looking for that answer. I ended up implementing it in the following way. The reason is that it was kind a cumbersome with sets since after creating all sets, I had to first figure out which set is non empty so that the retainAll could be called. it was resulting in lots of if-else statements. My current implementation is like this
private List<MyObj> getCachedObjs(Long criteria1, String criteria2, String criteria3) {
List<MyObj> results = new ArrayList<>();
int totalActiveFilters = 0;
if (criteria1 != null){
totalActiveFilters++;
}
if (!StringUtil.isBlank(criteria2)){
totalActiveFilters++;
}
if (!StringUtil.isBlank(criteria3)){
totalActiveFilters++;
}
for (Map.Entry<Long, MyObj> objEntry : objCache.entrySet()){
MyObj obj = objEntry.getValue();
int matchedFilters = 0;
if (criteria1 != null) {
if (obj.getCriteria1().equals(criteria1)) {
matchedFilters++;
}
}
if (!StringUtil.isBlank(criteria2)){
if (obj.getCriteria2().equals(criteria2)){
matchedFilters++;
}
}
if (!StringUtil.isBlank(criteria3)){
if (game.getCriteria3().equals(criteria3)){
matchedFilters++;
}
}
if (matchedFilters == totalActiveFilters){
results.add(obj);
}
}
return results;
}

Java 8: how to use lambda expressions on JoinRowSets?

I'm developing a feature that will be used among some of my company's products so I can't have product-specific code.
I have the results of 2 queries stored on 2 JoinRowSet objects (I do have to use JoinRowSet because I have to join other queries' results with these ones later). Then, I need to intersect those 2 JoinRowSets but I don't know what class they "belong" to (I don't know if the query will return People, Contacts, Departments or any other thing). I was told I should use lambda expressions to do this but I'm finding some problems.
I have converted one of the JoinRowSet to a Collection (I don't know how to use lambda expressions directly on a JoinRowSet), then I'm looping over one of the JoinRowSets and, for each column of this JoinRowSet, I want to get the records on the initial Collection that have the same value on that column. But, as I don't know the class of the data, I can't do something like u.getAge() because I don't have the property name.
This is my method:
public static void testLambdas() throws SQLException {
... //set the needed stuff to connect to database and query data
JoinRowSet jrs1 = ... //result from query 1
JoinRowSet jrs2 = ... //result from query 2
Collection<Row> jrs2Collection = (Collection<Row>) jrs2.getRowSets();//collection to use on lambda expression
while (jrs1.next()) {
ResultSetMetaData metadata = jrs1.getMetaData();
for (int j = 1; j <= metadata.getColumnCount(); j++) {
String dataType = metadata.getColumnTypeName(j);
Object colValue = null;
if (dataType.equals("NUMBER")) {
colValue = jrs1.getBigDecimal(j);
} else if (dataType.equals("VARCHAR2")) {
colValue = jrs1.getString(j);
} else if (dataType.equals("DATE")) {
colValue = jrs1.getTimestamp(j);
}
System.out.println(metadata.getColumnName(j)+ " (" + dataType +"): "+ colValue);
Object o = jrs2Collection.stream()
.filter(u -> {
try {
return (u.getColumnObject(j).equals(colValue));
} catch (Exception e) {
e.printStackTrace();
return false;
}
})
.collect(Collectors.toCollection(TreeSet::new));
}
}
}
Please ignore the fact that this use of a lambda expression is inside a while and not being re-used on each iteration. I will take care of that if I can solve the issues I have now.
I had to add the try-catch to the filter because it was returning Unhandled exception type SQLException. After adding this try-catch, I get a Local variable j defined in an enclosing scope must be final or effectively final on u.getColumnObject(j).
I have no idea if what I'm doing is the best way to do this or if I'm doing something really wrong (2 of my colleagues have looked at my code and also have no idea how to solve this).
I would appreciate any inputs on this, please.
It’s not clear what you are trying to do as you are creating collections inside a loop without using them. So I can’t tell you whether there’s a better solution for this.
Regarding the error, the error message is pretty clear. You can’t use a mutable local variable inside a lambda. However, the solution is quite simple: you can assign the content of the mutable variable to an immutable variable:
for (int j = 1; j <= metadata.getColumnCount(); j++) {
// …
int currentJ = j;
Object currentColValue = colValue;
Object o = jrs2Collection.stream()
.filter(u -> {
try {
return (u.getColumnObject(currentJ).equals(currentColValue));
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
)
.collect(Collectors.toCollection(TreeSet::new));
}
Note that currentJ and currentColValue are effectively final which means you could add a final modifier to them without changing the program. It’s a requirement that local variables you use inside a lambda expression are either final or effectively final.
By the way, don’t think about “re-using lambda between iterations”. In most cases, the JRE will take care of reusing the instances created for lambda expressions where possible. And the JDBC operations outweigh any overhead imposed by temporary objects created for lambda expressions anyway.

How to set list of parameters on prepared statement? [duplicate]

This question already has answers here:
PreparedStatement IN clause alternatives?
(33 answers)
Closed 3 years ago.
i have a list of names e.g.:
List<String> names = ...
names.add('charles');
...
and a statement:
PreparedStatement stmt =
conn.prepareStatement('select * from person where name in ( ? )');
how to do the following:
stmt.setParameterList(1,names);
Is there a workaround? can someone explain why this method is missing?
using: java, postgresql, jdbc3
This question is very old, but nobody has suggested using setArray
This answer might help https://stackoverflow.com/a/10240302/573057
There's no clean way to do this simply by setting a list on the PreparedStatement that I know of.
Write code that constructs the SQL statement (or better replaces a single ? or similar token) with the appropriate number of questions marks (the same number as in your list) and then iterate over your list setting the parameter for each.
this method is missing due to type erasure the parameter type of the List is lost at runtime. Therefore the need to add several methods arires: setIntParameters, setLongParameters, setObjectParameters, etc
For postgres 9 I have used this approach:
jdbcTemplate.query(getEmployeeReport(), new PreparedStatementSetter() {
#Override
public void setValues(PreparedStatement ps) throws SQLException {
ps.setTimestamp(1, new java.sql.Timestamp(from.getTime()));
ps.setTimestamp(2, new java.sql.Timestamp(to.getTime()));
StringBuilder ids = new StringBuilder();
for (int i = 0; i < branchIds.length; i++) {
ids.append(branchIds[i]);
if (i < branchIds.length - 1) {
ids.append(",");
}
}
// third param is inside IN clause
// Use Types.OTHER avoid type check while executing query
ps.setObject(3, ids.toString(), **Types.OTHER**);
}
}, new PersonalReportMapper());
In case the questions' meaning is to set several params in a single call...
Because the type validation is already defined in a higher level, I think the only need is for setObject(...).
Thus, a utility method can be used:
public static void addParams(PreparedStatement preparedStatement, Object... params) throws SQLException {
for (int i = 0; i < params.length; i++) {
Object param = params[i];
preparedStatement.setObject(i+1, param);
}
}
Usage:
SqlUtils.addParams(preparedStatement, 1, '2', 3d);
Feel free converting this to a Java 8 lambda :)
I was reviewing code this morning and one of my colleagues had a different approach, just pass the parameter using setString("name1','name2','name3").
Note: I skipped the single quote at the beginning and end because these are going to be added by the setString.
After examining various solutions in different forums and not finding a good solution, I feel the below hack I came up with, is the easiest to follow and code. Note however that this doesn't use prepared query but gets the work done anyway:
Example: Suppose you have a list of parameters to pass in the 'IN' clause. Just put a dummy String inside the 'IN' clause, say, "PARAM" do denote the list of parameters that will be coming in the place of this dummy String.
select * from TABLE_A where ATTR IN (PARAM);
You can collect all the parameters into a single String variable in your Java code. This can be done as follows:
String param1 = "X";
String param2 = "Y";
String param1 = param1.append(",").append(param2);
You can append all your parameters separated by commas into a single String variable, 'param1', in our case.
After collecting all the parameters into a single String you can just replace the dummy text in your query, i.e., "PARAM" in this case, with the parameter String, i.e., param1. Here is what you need to do:
String query = query.replaceFirst("PARAM",param1); where we have the value of query as
query = "select * from TABLE_A where ATTR IN (PARAM)";
You can now execute your query using the executeQuery() method. Just make sure that you don't have the word "PARAM" in your query anywhere. You can use a combination of special characters and alphabets instead of the word "PARAM" in order to make sure that there is no possibility of such a word coming in the query. Hope you got the solution.
Other method :
public void setValues(PreparedStatement ps) throws SQLException {
// first param inside IN clause with myList values
ps.setObject(1 , myList.toArray(), 2003); // 2003=array in java.sql.Types
}

Categories