Searching with only some criteria

I have a database table with six columns, all of which are nullable.

The user has a search screen with one input for each column,

and I'd like to return all matching values. I'm using a prepared

statement that looks something like:

select *

from MY_TABLE

where COLUMN1 like ?

and COLUMN2 like ?

...

My problem arises when a user doesn't enter values for all six

search conditions. I can't put in "" or null for the parameters, because

then it will explicitly search for those values. I can't not call setString()

on the column numbers I'm not interested in.

Right now I'm stuck with:

privatefinalstatic String SEARCH =

"select * from TABLE where";

publicvoid myMethod(){

final StringBuffer buffer =new Stringbuffer(SEARCH);

final String name = httpRequest.getParameter("name").trim();

if (!name.equals("")){

buffer.append("NAME like '");

buffer.append(name);

buffer.append("'");

}

// magic 'and' insertion here if required

final String id = httpRequest.getParameter("id").trim();

if (!id.equals("")){

buffer.append("ID like '");

buffer.append(id);

buffer.append("'");

}

}

That gets really ugly over multiple items. Is there no better way?

My coworkers insist this is my only choice, but I don't believe them.

Thanks!

[2235 byte] By [es5f2000a] at [2007-11-27 3:50:15]
# 1

The LIKE query generally works as follows:SELECT * FROM table WHERE column LIKE '%value%'

Where the % just represents zero to more matches. I don't see that you're using them, so your LIKE queries are going to behave the same as EQUAL (column = 'value') queries.

The profit of using the % signs is that the following query also works:SELECT * FROM table WHERE column LIKE '%%'

resulting in any rows being returned.

So all you need is -summarized- to do the following:preparedStatement.setString(1, "%" + input.trim() + "%");

BalusCa at 2007-7-12 8:54:10 > top of Java-index,Database Connectivity,Java Database Connectivity (JDBC)...
# 2

Given the following table:

USEREMAIL

Bobnull

this query returns no rows:

SELECT * FROM table WHERE user like '%' and email like '%'

but this query returns one row:

SELECT * FROM table WHERE user like '%'

The problem is that % is a wildcard which doesn't include null, so

I need a dynamic query based on which criteria are filled in.

es5f2000a at 2007-7-12 8:54:10 > top of Java-index,Database Connectivity,Java Database Connectivity (JDBC)...
# 3
What aboutSELECT * FROM table WHERE (column LIKE '%' OR column IS NULL)
BalusCa at 2007-7-12 8:54:10 > top of Java-index,Database Connectivity,Java Database Connectivity (JDBC)...
# 4
That did it. I knew there was an easy way.BalusC++Thanks.
es5f2000a at 2007-7-12 8:54:10 > top of Java-index,Database Connectivity,Java Database Connectivity (JDBC)...
# 5

Thank you :)

But I must say that I just realize that this will also return null values while the user has actually entered something in the LIKE.

SELECT * FROM table WHERE column LIKE '%value%' OR column IS NULL

Not sure if this will be an issue for you.

BalusCa at 2007-7-12 8:54:10 > top of Java-index,Database Connectivity,Java Database Connectivity (JDBC)...
# 6

>

> That gets really ugly over multiple items. Is there

> no better way?

> My coworkers insist this is my only choice, but I

> don't believe them.

Generally that is the only solution.

Consider these cases

1. User fills in 2 fields(A,B)

2. User fills in 3 fields(A,B,C)

In case 1 does it matter whether field C is null or not? Does the user expect when they don't fill in C that only null values will be returned?

If the answer is no then the only solution is dynamically constructing the query.

You can certainly define this at the requirements level but I expect, for example, that when users fill in B they will not expect to get back answers where B is null.

If users are really aware that null is a possibility then another complication is that usually the only solution is to either add a check box in the gui that lets them specify null or allow them to enter a special value that is then translated into null.

Note that most situations that I have seen users are not going to know what null is.

jschella at 2007-7-12 8:54:10 > top of Java-index,Database Connectivity,Java Database Connectivity (JDBC)...
# 7

I wound up writing a helper class to handle the situation. Right now

it only works with Strings.

/**

*

This class wraps any search query with a dynamic 'where' clause.

*

*

Create a ConditionalQuery instance with the static clauses which

* appear before ("select FOO from BAR") and after ("order by BAZ") your

* dynamic 'where' clause. For each search parameter you may be using,

* invoke addCondition() with the column name and the search value. If

* the value is "", it will be ignored. Otherwise, the value will be

* added to the search criteria.

*

*

Once you have passed in all criteria, invoke getStatement() to

* get a PreparedStatement you can query. The values for the query

* parameters will already be added to the PreparedStatement.

*

*

Sample usage:

* <code><pre>

private static final String baseQuery = "select MAIL from LD_USERS_DUMP";

private static final String orderClause = "order by MAIL";

public void getEmailAddress(final String lastName, final String comitID) {

final ConditionalQuery query =

new ConditionalQuery(baseQuery, orderClause);

query.addCondition("SN", lastName);

query.addCondition("LDAP_UID", comitID);

Connection connection = null;

PreparedStatement preparedStatement = null;

ResultSet resultSet = null;

try {

connection = DatabaseFactory.getConnection("Oracle");

preparedStatement = query.getStatement(connection);

resultSet = preparedStatement.executeQuery();

} catch (final SQLException se) {

LOGGER.error("Database error.", se);

} finally {

DatabaseUtilities.close(resultSet);

DatabaseUtilities.close(preparedStatement);

DatabaseUtilities.close(connection);

}

}

</pre></code>

*

*

This class is final because it is not designed to be extended.

*

This class is <strong>not</strong> thread-safe.

*

* @author Eric Stein (xbbjgns)

*/

public final class ConditionalQuery {

/**

* Contains the contents of this query up to the end of the

* 'where' clause.

*/

private final StringBuffer query = new StringBuffer();

/** Clause which goes after the 'where' clause. May not be null.*/

private final String postConditionClause;

/**

* String list of condition values, in the order they were added. */

private final List values = new ArrayList();

/**

* True if this query has one or more conditions, false if there

* are zero conditions.

*/

private boolean hasConditions = true;

/**

* Constructor.

* @param baseQuery clause to put before the 'where' clause in

* the query. May not be null.

* @throws NullPointerException if the baseQuery parameter is null.

*/

public ConditionalQuery(final String baseQuery)

throws NullPointerException {

this(baseQuery, "");

}

/**

* Constructor.

* @param baseQuery clause to put before the 'where' clause in

* the query. May not be null.

* @param postConditionClause clause to put after the 'where' clause

* in the query. May not be null.

* @throws NullPointerException if either parameter is null.

*/

public ConditionalQuery(final String baseQuery,

final String postConditionClause)

throws NullPointerException {

ValidationUtilities.checkNotNull(baseQuery, "baseQuery");

ValidationUtilities.checkNotNull(postConditionClause,

"postConditionClause");

this.query.append(baseQuery.trim());

this.query.append(" ");

this.postConditionClause = postConditionClause;

}

/**

* Adds a condition to this query. If the value to search for is

* "", this method will do nothing. If there is a search value,

* the column and value get added to the prepared statement. By

* default, conditions are <strong>not</strong> case-sensitive.

* @param columnName the name of the column a condition is being

* added for. May not be null.

* @param value the value for the condition being added to the query.

* May not be null.

* @throws NullPointerException if either parameter is null

*/

public void addCondition(final String columnName, final String value)

throws NullPointerException {

this.addCondition(columnName, value, false);

}

/**

* Adds a condition to this query. If the value to search for is

* "", this method will do nothing. If there is a search value,

* the column and value get added to the prepared statement.

* @param columnName the name of the column a condition is being

* added for. May not be null.

* @param value the value for the condition being added to the query.

* May not be null.

* @param caseSensitive true for string comparisons to be

* case-sensitive, false for them to ignore case.

* @throws NullPointerException if the columnName or value parameters

* are null.

*/

public void addCondition(final String columnName, final String value,

boolean caseSensitive)

throws NullPointerException {

ValidationUtilities.checkNotNull(columnName, "columnName");

ValidationUtilities.checkNotNull(value, "value");

final String trimmedValue = value.trim();

if (trimmedValue.equals("")) {

return;

}

if (this.hasConditions) {

this.query.append("and ");

} else {

this.query.append("where ");

this.hasConditions = true;

}

if (caseSensitive) {

this.query.append(columnName);

} else {

this.query.append("upper(");

this.query.append(columnName);

this.query.append(") ");

}

this.query.append(" like ");

if (caseSensitive) {

this.query.append("? ");

} else {

this.query.append("upper(?) ");

}

this.values.add(value);

}

/**

* Creates a PreparedStatement from this query. The parameters

* for the PreparedStatement are already set.

* query. Will never return null.

* @param connection the connection a prepared statement is being

* created for. May not be null.

* @return a PreparedStatement whose values are set. Will never return

* null.

* @throws NullPointerException if the connection parameter is null.

* @throws SQLException if an error occurs while preparing the

* statement.

*/

public PreparedStatement getStatement(final Connection connection)

throws NullPointerException, SQLException {

ValidationUtilities.checkNotNull(connection, "connection");

final PreparedStatement statement =

connection.prepareStatement(this.getQuery());

for (int i = 0; i < this.values.size(); i++) {

statement.setString(i + 1, (String) this.values.get(i));

}

return statement;

}

/**

* Returns true if this query has at least one search condition,

* or false if it has no conditions.

* @return true if this query has at least one search condition,

* or false if it has no conditions.

*/

public boolean hasConditions() {

return this.hasConditions;

}

/**

* Returns a string representation of this query.

* @return a string representation of this query. Will never

* return null.

*/

public String getQuery() {

return this.query.toString() + this.postConditionClause;

}

}

es5f2000a at 2007-7-12 8:54:10 > top of Java-index,Database Connectivity,Java Database Connectivity (JDBC)...
# 8

Yuck.

Just use a prepared statement.

PreparedStatement ps = conn.prepareStatement("select x,y,z from foo where ((? like bar) or (? is null))");

ps.setParam(1,searchTerm);

ps.setParam(2,searchTerm);

...

If you supply null as the variable searchTerm, all records will be returned. If you supply a value, it will be compared with the contents of the bar column.

Yes it's ungainly supplying the parameter twice like that, but nothing like as ungainly and risky as the behemoth you're trying to construct.

dcmintera at 2007-7-12 8:54:10 > top of Java-index,Database Connectivity,Java Database Connectivity (JDBC)...
# 9
> > Yes it's ungainly supplying the parameter twice like> that, but nothing like as ungainly and risky as the> behemoth you're trying to construct.Unless of course one doesn't want to search on all the fields all of the time.
jschella at 2007-7-12 8:54:10 > top of Java-index,Database Connectivity,Java Database Connectivity (JDBC)...
# 10
When null is supplied as both parameters to the query it becomes possible to short circuit the term "((? like bar) or (? is null))" to "true". I've been assuming that most databases will do that, but I have to admit that I've no hard evidence to support this assumption.
dcmintera at 2007-7-12 8:54:10 > top of Java-index,Database Connectivity,Java Database Connectivity (JDBC)...
# 11
You cannot join on nulls so:null = null will always evaluate to false.andnull != null will also evaluate to false
ChristopherAngela at 2007-7-12 8:54:10 > top of Java-index,Database Connectivity,Java Database Connectivity (JDBC)...
# 12
Edit: nevermind.Message was edited by: BalusC
BalusCa at 2007-7-12 8:54:10 > top of Java-index,Database Connectivity,Java Database Connectivity (JDBC)...
# 13

> You cannot join on nulls so:

> null = null will always evaluate to false.

> and

> null != null will also evaluate to false

I'm not sure if your "nevermind" was about the above, but if not:

You can join on nulls as long as you use the IS keyword.

With respect to jschell's point, I think he's right. PostgreSQL 8.1 at least doesn't appear to optimise this condition. So my suggestion is probably not a great one.

dcmintera at 2007-7-12 8:54:10 > top of Java-index,Database Connectivity,Java Database Connectivity (JDBC)...
# 14

> > Yes it's ungainly supplying the parameter twice

> like

> > that, but nothing like as ungainly and risky as

> the

> > behemoth you're trying to construct.

>

> Unless of course one doesn't want to search on all

> the fields all of the time.

So, if I can get identical results using a single query, I'd be happier

doing that. This is an inward-facing app with a small client base,

so minor query inefficiencies aren't a big deal.

With respect to my class above, it certainly isn't risky. It isn't

ungainly *yet*, but it'll be hard to keep it such as I add the ability

to add types besides String to the PreparedStatement.

If you have specific concerns, I'd love to hear them. Out-of-hand

dismissal isn't constructive.

es5f2000a at 2007-7-12 8:54:10 > top of Java-index,Database Connectivity,Java Database Connectivity (JDBC)...