Mybatis, Enclosed OR with SQL builder - java

I cannot figure out how to get MyBatis to generate an enclosed or statement:
WHERE x.token = ? AND (
(x.scene = 'A' OR x.scene = 'B'))
This is a surprisingly simple operation they've made very difficult. I can't find it in the JavaDoc: http://static.javadoc.io/org.mybatis/mybatis/3.4.5/org/apache/ibatis/jdbc/SQL.html

It can't be done in the current version of mybatis (3.4.6).
You can either use the whole subexpression like this:
WHERE("x.token = ?");
WHERE("(x.scene = 'A' OR x.scene = 'B')");
or create you own function if you have many/variable number of operands to OR:
WHERE("x.token = ?");
WHERE(OR("x.scene = 'A'", "x.scene = 'B'"));
Where OR is defined (using guava Joiner) as:
Joiner OR_JOINER = Joiner.on(" OR ");
private String OR(String ... operands) {
return String.format("(%s)", OR_JOINER.join(operands));
}

Related

How to find the selected id in my List<String> ids arraylist?

Here is my code. I am trying to use JUnit to test the deleteUsers() method, but everytime I write my test, it deletes all the users that I have in the database. How can i delete a single user? Below is the code for the method and for the test.
#Override
public boolean deleteUsers(List<String> ids) throws Exception {
StringBuilder sql = new StringBuilder();
sql.append("delete from user where ");
for (String id : ids) {
sql.append(" id = ? or");
}
String strSql = sql.toString().substring(0, sql.length() - 2) + ";";
PreparedStatement preparedStatement = this.connection.prepareStatement(strSql);
for (int i = 0; i < ids.size(); i++) {
preparedStatement.setInt(1 + i, Integer.parseInt(ids.get(i)));
}
int lines = preparedStatement.executeUpdate();
preparedStatement.close();
return lines > 0;
}
You're missing a check for empty input. In your test you pass an empty list to deleteUsers which results in this SQL statement:
delete from user wher;
I'd expect that the DBMS would reject this as invalid SQL but perhaps there are some where this is interpreted as delete from user which simply deletes all users. (As #SteveBosman pointed out the wher is interpreted as table alias as it is - due to the missing last e - no reserved word anymoere)
Basically you have 2 options. Either deleting all users by passing an empty list is a valid use case - in which case you should handle it properly by producing proper SQL. Or this is not expected and you should adapt your code to throw an Exception if ids is empty.
#Override
public boolean deleteUsers(List<String> ids) throws Exception {
if (ids == null || ids.size() == 0) {
throw new IllegalArgumentException("List of IDs must not be empty");
}
...
}
You could of course return false in case of an empty input as well to indicate no users were deleted.
To pass values to the deleteUsers method in your test you need to add values to the used list:
userDAOImpl.addUser("admin3", "111222");
final List<String> idsToDelete = new ArrayList<>();
idsToDelete.add("111222");
userDAOImpl.deleteUsers(idsToDelete);
The problem is caused by how the SQL is built. When deleteUsers is passed an empty list then the generated SQL will be:
delete from user wher
which will result in all data being deleted (the table user is given the alias "wher"). I highly recommend checking at the start of the method if the collection is empty and either raising an exception or returning.
Add the following check
if (ids == null || ids.isEmpty()) {
throw new IllegalArgumentException("ids must not be empty");
}
StringBuilder sql = new StringBuilder();
sql.append("delete from user where");
String orClause = "";
for (String id : ids) {
sql.append(orClause);
sql.append(" id = ?");
orClause = " or";
}

How to do a like query with a variable set of strings?

I am triying to do a "like query" with a variable set of strings, in order to retrieve in a single query all texts that contains a set of words, that is:
public long countByTextLike(Set<String> strings) {
CriteriaBuilder builder = manager.getCriteriaBuilder();
CriteriaQuery<Long> query = builder.createQuery(Long.class);
Root<Example> root = query.from(Example.class);
query.select(builder.count(root.get("id"))).where(
builder.and(
builder.equal(root.get("lang"), "EN")
)
);
//this does not work
for (String word : strings) {
query.where(builder.or(builder.like(root.get("text"), word)));
}
return manager.createQuery(query).getSingleResult();
}
unfortunately this does not work because the where is overwritten in each loop. Only the last word of loop is used and "AND" restictions are being overwriten.
How is possible to do a "like query" with a variable number of strings? It is not posible?
I am using the spring framework but i think that the question could be extendable to hibernate
You can use predicates, and then add them all with only one where clause
public long countByTextLike(Set<String> strings) {
CriteriaBuilder builder = currentSession().getCriteriaBuilder();
CriteriaQuery<Long> query = builder.createQuery(Long.class);
Root<Example> root = query.from(Example.class);
Predicate[] predicates = new Predicate[strings.size()];
query.select(builder.count(root.get("id")));
Predicate langPredicate = builder.equal(root.get("lang"), "EN");
int cont = 0;
for (String word : strings) {
Predicate pred = builder.like(root.get("text"), "%" + word + "%");
predicates[cont++] = pred;
}
Predicate orPredicate = builder.or(predicates);
Predicate finalPredicate = builder.and(orPredicate, langPredicate);
return manager.createQuery(query).where(finalPredicate).getSingleResult();
}

how to create comma separated string in single quotes from arraylist of string in JAVA

I have requirement in Java to fire a query on MS SQL like
select * from customer
where customer.name in ('abc', 'xyz', ...,'pqr');
But I have this IN clause values in the form of ArrayList of String. For ex: the list look like {"abc","xyz",...,"pqr"}
I created a Prepared Statement :
PreparedStatement pStmt = conn.prepareStatement(select * from customer
where customer.name in (?));
String list= StringUtils.join(namesList, ",");
pStmt.setString(1,list);
rs = pStmt.executeQuery();
But the list is like "abc,xyz,..,pqr", but I want it as "'abc','xyz',..,'pqr'"
so that I can pass it to Prepares Statement.
How to do it in JAva with out GUAVA helper libraries.
Thanks in Advance!!
I know this is a really old post but just in case someone is looking for how you could do this in a Java 8 way:
private String join(List<String> namesList) {
return String.join(",", namesList
.stream()
.map(name -> ("'" + name + "'"))
.collect(Collectors.toList()));
}
List<String> nameList = ...
String result = nameList.stream().collect(Collectors.joining("','", "'", "'"));
For converting the string you can try this:
String list= StringUtils.join(namesList, "','");
list = "'" + list + "'";
But i dont thing it's a good idea to pass one string for multiple params.
Even if you formatted the String as you wish, it won't work. You can't replace one placeholder in the PreparedStatement with multiple values.
You should build the PreparedStatement dynamically to have as many placeholders as there are elements in your input list.
I'd do something like this :
StringBuilder scmd = new StringBuilder ();
scmd.append ("select * from customer where customer.name in ( ");
for (int i = 0; i < namesList.size(); i++) {
if (i > 0)
scmd.append (',');
scmd.append ('?');
}
scmd.append (")");
PreparedStatement stmt = connection.prepareStatement(scmd.toString());
if (namesList.size() > 0) {
for (int i = 0; i < namesList.size(); i++) {
stmt.setString (i + 1, namesList.get(i));
}
}
rs = stmt.executeQuery();
You can use a simple separator for this type of activity. Essentially you want an object that evaluates to "" the first time around but changes after the first request to return a defined string.
public class SimpleSeparator<T> {
private final String sepString;
boolean first = true;
public SimpleSeparator(final String sep) {
this.sepString = sep;
}
public String sep() {
// Return empty string first and then the separator on every subsequent invocation.
if (first) {
first = false;
return "";
}
return sepString;
}
public static void main(String[] args) {
SimpleSeparator sep = new SimpleSeparator("','");
System.out.print("[");
for ( int i = 0; i < 10; i++ ) {
System.out.print(sep.sep()+i);
}
System.out.print("]");
}
}
I did it as following with stream. Almost the same, but a bit shorter.
nameList = List.of("aaa", "bbb", "ccc")
.stream()
.map(name -> "'" + name + "'")
.collect(Collectors.joining(","));
I guess the simplest way to do it is using expression language like that:
String[] strings = {"a", "b", "c"};
String result = ("" + Arrays.asList(strings)).replaceAll("(^.|.$)", "\'").replace(", ", "\',\'" );

SQLlite database, insert punct after second digit

I have a sqlite databse, with alot of tables. Each table has many rows. In one column I have something like this:
[58458, 65856, 75658, 98456, 98578, ... N]
I made a mistake when created the databse (I don't have acces to the initial data anymore ), what I need is to make these numbers with a punct after the second digit and have something like this:
[58.458, 65.856, 75.658, 98.456, 98.578, ... N]
Is there any way I can do this? I prefer Java. Or is it already any tools that can do this?
Use this function to parse the information from each column.
public static String convertColumn(String textF)
{
String textAux = "";
String newText = "[";
int i = 0;
textF = textF.substring(1, textF.length() - 1);
while(i < textF.length())
{
textAux = textF.substring(i, i + 5);
int nrAux = Integer.parseInt(textAux);
i+=7;
int a;
int b;
a = nrAux / 1000;
b = nrAux - a * 1000;
double newNr;
newNr = a + b * 0.001;
newText = newText + newNr + ", ";
}
newText = newText.substring(0, newText.length() - 2);
newText += "]";
return newText;
}
The function will have as parameter a string like [58458, 65856, 75658, 98456, 98578], which you will get from
the SQL table, and the return value will be [58.458, 65.856, 75.658, 98.456, 98.578] which is the value that you need to update the column with.
For SQL the base idea is this:
UPDATE table
SET column = convertColumn(column);
You can use CAST as REAL on the column, and then update as advised in the other answer.
select CAST(YOUR_COL AS REAL) from YOUR_TABLE
Search for CAST in this doc for more info on it: SQLite language guide
This should work if it's a NUMERIC column:
UPDATE <TABLE NAME> SET <COLUMN> = <COLUMN>/1000;
If it is NOT a NUMERIC or REAL column then this should work:
UPDATE <TABLE NAME> SET <COLUMN> = CAST(<COLUMN> AS REAL)/1000;
(Thanks to Goibniu for the pointer)

Hibernate order by with nulls last

Hibernate used with PostgreSQL DB while ordering desc by a column puts null values higher than not null ones.
SQL99 standard offers keyword "NULLS LAST" to declare that null values should be put lower than not nulls.
Can "NULLS LAST" behaviour be achieved using Hibernate's Criteria API?
This feature has been implemented during Hibernate 4.2.x and 4.3.x releases as previously mentioned.
It can be used as for example:
Criteria criteria = ...;
criteria.addOrder( Order.desc( "name" ).nulls(NullPrecedence.FIRST) );
Hibernate v4.3 javadocs are less omissive here.
Given that HHH-465 is not fixed and is not going to get fixed in a near future for the reasons given by Steve Ebersole, your best option would be to use the CustomNullsFirstInterceptor attached to the issue either globally or specifically to alter the SQL statement.
I'm posting it below for the readers (credits to Emilio Dolce):
public class CustomNullsFirstInterceptor extends EmptyInterceptor {
private static final long serialVersionUID = -3156853534261313031L;
private static final String ORDER_BY_TOKEN = "order by";
public String onPrepareStatement(String sql) {
int orderByStart = sql.toLowerCase().indexOf(ORDER_BY_TOKEN);
if (orderByStart == -1) {
return super.onPrepareStatement(sql);
}
orderByStart += ORDER_BY_TOKEN.length() + 1;
int orderByEnd = sql.indexOf(")", orderByStart);
if (orderByEnd == -1) {
orderByEnd = sql.indexOf(" UNION ", orderByStart);
if (orderByEnd == -1) {
orderByEnd = sql.length();
}
}
String orderByContent = sql.substring(orderByStart, orderByEnd);
String[] orderByNames = orderByContent.split("\\,");
for (int i=0; i<orderByNames.length; i++) {
if (orderByNames[i].trim().length() > 0) {
if (orderByNames[i].trim().toLowerCase().endsWith("desc")) {
orderByNames[i] += " NULLS LAST";
} else {
orderByNames[i] += " NULLS FIRST";
}
}
}
orderByContent = StringUtils.join(orderByNames, ",");
sql = sql.substring(0, orderByStart) + orderByContent + sql.substring(orderByEnd);
return super.onPrepareStatement(sql);
}
}
You can configure "nulls first" / "nulls last" in hibernate properties so it will be picked up by any criteria call by default: hibernate.order_by.default_null_ordering=last (or =first).
See this hibernate commit for details.
We can create Pageable object with following Sort parameter:
JpaSort.unsafe(Sort.Direction.ASC, "ISNULL(column_name), (column_name)")
We can prepare HQL as well:
String hql = "FROM EntityName e ORDER BY e.columnName NULLS LAST";
Here's my update to the class by (Pascal Thivent):
for (int i = 0; i < orderByNames.length; i++) {
if (orderByNames[i].trim().length() > 0) {
String orderName = orderByNames[i].trim().toLowerCase();
if (orderName.contains("desc")) {
orderByNames[i] = orderName.replace("desc", "desc NULLS LAST");
} else {
orderByNames[i] = orderName.replace("asc", "asc NULLS FIRST");
}
}
}
This fixes the problem:
This breaks if sql has limit/offset after order by – Sathish Apr 1 '11 at 14:52
Also here's how you can use this within JPA (hibernate):
Session session = entityManager.unwrap(Session.class);
Session nullsSortingProperlySession = null;
try {
// perform a query guaranteeing that nulls will sort last
nullsSortingProperlySession = session.getSessionFactory().withOptions()
.interceptor(new GuaranteeNullsFirstInterceptor())
.openSession();
} finally {
// release the session, or the db connections will spiral
try {
if (nullsSortingProperlySession != null) {
nullsSortingProperlySession.close();
}
} catch (Exception e) {
logger.error("Error closing session", e);
}
}
I've tested this on postgres and it fixes the 'nulls are higher than non-nulls' issue that we were having.
Another variant, if you create SQL on the fly and don't use Criteria API:
ORDER BY COALESCE(,'0') [ASC|DESC]
This works either for varchar or numeric columns.
For future travellers... I solved this by overriding the Hibernate dialect. I needed to add null first for asc and null last for desc by default in CriteriaQuery, which is for some reason not supported. (It's supported in legacy CriteriaAPI)
package io.tolgee.dialects.postgres
import org.hibernate.NullPrecedence
import org.hibernate.dialect.PostgreSQL10Dialect
#Suppress("unused")
class CustomPostgreSQLDialect : PostgreSQL10Dialect() {
override fun renderOrderByElement(expression: String?, collation: String?, order: String?, nulls: NullPrecedence?): String {
if (nulls == NullPrecedence.NONE) {
if (order == "asc") {
return super.renderOrderByElement(expression, collation, order, NullPrecedence.FIRST)
}
if (order == "desc") {
return super.renderOrderByElement(expression, collation, order, NullPrecedence.LAST)
}
}
return super.renderOrderByElement(expression, collation, order, nulls)
}
}
There appears to be a change request/bug ticket open for this

Categories