How to use IN operator with JDBI?
Asked Answered
B

2

12

I'm trying to do a IN query using MYSQL JDBI on Dropwizard (not relevant, I assume).

@SqlQuery("SELECT id FROM table where field in (<list>)")
List<Integer> findSomething(@BindIn("list") List<String> someList);

As suggested here, I've also annotated the class with

@UseStringTemplate3StatementLocator

But when I'm starting the application, I get the following error:

Exception in thread "main" java.lang.annotation.AnnotationFormatError: Invalid default: public abstract java.lang.Class org.skife.jdbi.v2.sqlobject.stringtemplate.UseStringTemplate3StatementLocator.errorListener()

Does anyone have a good idea on how to solve this issue?

Bifoliolate answered 20/10, 2015 at 15:50 Comment(0)
D
8

There are two ways to achieve it.

1. Using UseStringTemplate3StatementLocator

This annotation expects Group Files with SQL statement in StringTemplate

Say I have this file PersonExternalizedSqlDAO

// PersonExternalizedSqlDAO.java

package com.daoexp.dao;

@@ExternalizedSqlViaStringTemplate3
@RegisterMapper(PersonMapper.class)
public interface PersonExternalizedSqlDAO {
    @SqlQuery
    List<Person> getPersonByNames(@BindIn("names") List<String> names);
}

Since we are using UseStringTemplate3StatementLocator we have to create *.sql.stg file in same class path. For ex: in resources/com/daoexp/dao/PersonExternalizedSqlDAO.sql.stg

group PersonExternalizedSqlDAO;

getPersonByNames(names) ::= <<
  select * from person where name in (<names>)
>>

Now you should be able to query without any issues.


2. Another approach is to use ArgumentFactory that handles your custom data type(in this case List) for JDBI with @Bind. This is most preferable approach.

So create this list argument factory

public class ListArgumentFactory implements ArgumentFactory<List> {
    @Override
    public boolean accepts(Class<?> expectedType, Object value, StatementContext ctx) {
        return value instanceof List;
    }

    @Override
    public Argument build(Class<?> expectedType, final List value, StatementContext ctx) {
        return new Argument() {
            @Override
            public void apply(int position, PreparedStatement statement, StatementContext ctx) throws SQLException {
                String type = null;
                if(value.get(0).getClass() == String.class){
                    type = "varchar";
                } else if(value.get(0).getClass() == Integer.class){
                    // For integer and so on...
                } else {
                    // throw error.. type not handled
                }
                Array array = ctx.getConnection().createArrayOf(type, value.toArray());
                statement.setArray(position, array);
            }
        };
    }
}

What this class does ?

  • accepts instance of List
  • convert the integer/string list to array and binds with prepared statement

Make sure you register this argument factory with your DBI instance.

final DBIFactory factory = new DBIFactory();
final DBI jdbi = factory.build(environment, configuration.getDataSourceFactory(), "h2");
jdbi.registerArgumentFactory(new ListArgumentFactory());

Now you should be able to query using List in more simpler way(i.e) you have to use @Bind. Thats it.

@RegisterMapper(PersonMapper.class)
public interface PersonDAO {
    @SqlQuery("select * from person where name = any(:names)")
    List<Person> getPersonByNames(@Bind("names") List<String> names);
}

Refer:

Additional Info:

// PersonMapper.java
public class PersonMapper implements ResultSetMapper<Person> {

    public Person map(int index, ResultSet r, StatementContext ctx) throws SQLException {
        Person person = new Person();
        person.setId(r.getInt("id"));
        person.setName(r.getString("name"));

        return person;
    }
}
Dottydoty answered 26/10, 2015 at 16:36 Comment(5)
It seems I can not use method two due to my SQL database being MySQL. where array types are not supported (createArrayOf). I made it using your first method, with the sql.stg file. Thank you very much! And btw, ExternalizedSqlViaStringTemplate3 is deprecated, use UseStringTemplate3StatementLocator.Bifoliolate
I am getting exception in solution 1 as, causing: org.skife.jdbi.v2.exceptions.UnableToExecuteStatementException: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'getPersonByNames' at line 1, I think sql string are not getting replaced.Hilliard
Seems like method two is also not supported in Oracle: Caused by: java.sql.SQLException: Unsupported feature at oracle.jdbc.driver.PhysicalConnection.createArrayOf(PhysicalConnection.java:9283)Ingesta
@shiva, solution #1 works only for 3.2.1 version of org.antlr.stringtemplate. It is not working for the latest version.Barbette
I haven't used jdbi for quite a long time. Guess I can't help with that.Dottydoty
N
14

I think, you use the StringTemplate 4. You need to use StringTemplate 3 instead of StringTemplate 4. Add this dependency:

<dependency>
    <groupId>org.antlr</groupId>
    <artifactId>stringtemplate</artifactId>
    <version>3.2.1</version>
</dependency>
Neruda answered 21/10, 2015 at 0:37 Comment(2)
Will check later today, but in the code i pasted it does say version 3? "UseStringTemplate3StatementLocator"Bifoliolate
Thanks, if I didn't add this dependency my Dropwizard app failed to start with an AnnotationFormatError: Invalid default: public abstract java.lang.Class org.skife.jdbi.v2.sqlobject.stringtemplate.UseStringTemplate3StatementLocator.errorListener().Demigod
D
8

There are two ways to achieve it.

1. Using UseStringTemplate3StatementLocator

This annotation expects Group Files with SQL statement in StringTemplate

Say I have this file PersonExternalizedSqlDAO

// PersonExternalizedSqlDAO.java

package com.daoexp.dao;

@@ExternalizedSqlViaStringTemplate3
@RegisterMapper(PersonMapper.class)
public interface PersonExternalizedSqlDAO {
    @SqlQuery
    List<Person> getPersonByNames(@BindIn("names") List<String> names);
}

Since we are using UseStringTemplate3StatementLocator we have to create *.sql.stg file in same class path. For ex: in resources/com/daoexp/dao/PersonExternalizedSqlDAO.sql.stg

group PersonExternalizedSqlDAO;

getPersonByNames(names) ::= <<
  select * from person where name in (<names>)
>>

Now you should be able to query without any issues.


2. Another approach is to use ArgumentFactory that handles your custom data type(in this case List) for JDBI with @Bind. This is most preferable approach.

So create this list argument factory

public class ListArgumentFactory implements ArgumentFactory<List> {
    @Override
    public boolean accepts(Class<?> expectedType, Object value, StatementContext ctx) {
        return value instanceof List;
    }

    @Override
    public Argument build(Class<?> expectedType, final List value, StatementContext ctx) {
        return new Argument() {
            @Override
            public void apply(int position, PreparedStatement statement, StatementContext ctx) throws SQLException {
                String type = null;
                if(value.get(0).getClass() == String.class){
                    type = "varchar";
                } else if(value.get(0).getClass() == Integer.class){
                    // For integer and so on...
                } else {
                    // throw error.. type not handled
                }
                Array array = ctx.getConnection().createArrayOf(type, value.toArray());
                statement.setArray(position, array);
            }
        };
    }
}

What this class does ?

  • accepts instance of List
  • convert the integer/string list to array and binds with prepared statement

Make sure you register this argument factory with your DBI instance.

final DBIFactory factory = new DBIFactory();
final DBI jdbi = factory.build(environment, configuration.getDataSourceFactory(), "h2");
jdbi.registerArgumentFactory(new ListArgumentFactory());

Now you should be able to query using List in more simpler way(i.e) you have to use @Bind. Thats it.

@RegisterMapper(PersonMapper.class)
public interface PersonDAO {
    @SqlQuery("select * from person where name = any(:names)")
    List<Person> getPersonByNames(@Bind("names") List<String> names);
}

Refer:

Additional Info:

// PersonMapper.java
public class PersonMapper implements ResultSetMapper<Person> {

    public Person map(int index, ResultSet r, StatementContext ctx) throws SQLException {
        Person person = new Person();
        person.setId(r.getInt("id"));
        person.setName(r.getString("name"));

        return person;
    }
}
Dottydoty answered 26/10, 2015 at 16:36 Comment(5)
It seems I can not use method two due to my SQL database being MySQL. where array types are not supported (createArrayOf). I made it using your first method, with the sql.stg file. Thank you very much! And btw, ExternalizedSqlViaStringTemplate3 is deprecated, use UseStringTemplate3StatementLocator.Bifoliolate
I am getting exception in solution 1 as, causing: org.skife.jdbi.v2.exceptions.UnableToExecuteStatementException: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'getPersonByNames' at line 1, I think sql string are not getting replaced.Hilliard
Seems like method two is also not supported in Oracle: Caused by: java.sql.SQLException: Unsupported feature at oracle.jdbc.driver.PhysicalConnection.createArrayOf(PhysicalConnection.java:9283)Ingesta
@shiva, solution #1 works only for 3.2.1 version of org.antlr.stringtemplate. It is not working for the latest version.Barbette
I haven't used jdbi for quite a long time. Guess I can't help with that.Dottydoty

© 2022 - 2024 — McMap. All rights reserved.