How to make string with fragments from different strategies static? - java

When we concatenate compile-time static Strings using the + operator, the compiler will join them together and store a new static string in its place.
Now I have a strategy pattern that supplies SQL fragments that should be concatenated into a larger SQL statement. All the pieces are static, but since there are method calls involved the above compile-time joining does not happen.
Example:
class MagicDao {
void Object daoMethod(GetStuff cmd) {
String sql = "select ... where " + cmd.getWhereClause() + " order by ...";
// ...
}
}
public interface GetStuff {
String getWhereClause();
Object[] getParameters();
}
class GetFunStuff implements GetStuff {
public String getWhereClause() {
return " abc = ? ";
}
// ...
}
I am wondering is there any pattern/trick that will make the SQL string concatenation compile-time again while retaining the simplicity/readability of the code above.
(If caching is the only solution, could one suggest a readable, thread-safe one liner?)
(Motivation for doing this: making things static will save countless garbage being created each second during the string building process and saves the time looking up the string in the PreparedStatement cache.)

I don't really know how you're making SQL queries, but you could consider to use JDBC PreparedStatement, like:
PreparedStatement pstmt = con.prepareStatement("select ... where ? order by ...");
pstmt.setString(1, cmd.getWhereClause());
in this case your String will be precompiled

Related

Having trouble reading String from MySQL table to Eclipse

I have a project of coupons but I have an issue when trying to read a coupon to Eclipse. I have a table of categories which are connected to my coupons table in row "CATEGORY_ID" which is an int. when using add Method I convert my ENUM to int in order to add it to CATEGORY_ID with no problem.
my issue is when trying to read it, I try and convert it to STRING to get a text value, however, I get an exception.
here is my code:
ENUM CLASS:
public enum Category {
FOOD(1), ELECTRICITY(2), RESTAURANT(3), VACATION(4), HOTEL(5);
private Category(final int cat) {
this.cat = cat;
}
private int cat;
public int getIDX() {
return cat;
}
private Category(String cat1) {
this.cat1 = cat1;
}
private String cat1;
public String getName() {
return cat1;
}
}
A Method to add coupon to table COUPONS:
// sql = "INSERT INTO `couponsystem`.`coupons` (`COMPANY_ID`,`CATEGORY_ID`,`TITLE`, `DESCRIPTION`,
`START_DATE`, `END_DATE`, `AMOUNT`, `PRICE`, `IMAGE`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);";
#Override
public void addCoupon(Coupon coupon) throws SQLException {
Connection connection = pool.getConnection();
try {
PreparedStatement statement = connection.prepareStatement(ADD_COUPON);
statement.setInt(1, coupon.getCompanyID());
statement.setInt(2, coupon.getCategory().getIDX());
statement.setString(3, coupon.getTitle());
statement.setString(4, coupon.getDescription());
statement.setDate(5, (Date) coupon.getStartDate());
statement.setDate(6, (Date) coupon.getEndDate());
statement.setInt(7, coupon.getAmount());
statement.setDouble(8, coupon.getPrice());
statement.setString(9, coupon.getImage());
statement.execute();
} finally {
pool.restoreConnection(connection);
}
}
Method to get coupon:
// GET_ONE_COUPON = "SELECT * FROM `couponsystem`.`coupons` WHERE (`id` = ?);";
#Override
public Coupon getOneCoupon(int couponID) throws SQLException {
Connection connection = pool.getConnection();
Coupon result = null;
List<Category> cats = new ArrayList<Category>(EnumSet.allOf(Category.class));
try {
PreparedStatement statement = connection.prepareStatement(GET_ONE_COUPON);
statement.setInt(1, couponID);
ResultSet resultSet = statement.executeQuery();
resultSet.next();
result = new Coupon(resultSet.getInt(1), resultSet.getInt(2), Category.valueOf(resultSet.getString(3)),
resultSet.getString(4), resultSet.getString(5), resultSet.getDate(6), resultSet.getDate(7),
resultSet.getInt(8), resultSet.getDouble(9), resultSet.getString(10));
} finally {
pool.restoreConnection(connection);
}
return result;
on column index (3) I try a and convert ENUM to string to get a text value, here is where I get an exception.
EXCEPTION:
Exception in thread "main" java.lang.IllegalArgumentException: No enum constant coupon.beans.Category.5
at java.base/java.lang.Enum.valueOf(Enum.java:240)
at coupon.beans.Category.valueOf(Category.java:1)
at coupon.dbdao.CouponsDBDAO.getOneCoupon(CouponsDBDAO.java:125)
at coupon.Program.main(Program.java:65)
Hope I am clear with my question. I have no issue adding any more information.
valueOf expects a string that corresponds to the name of the enum element, like "FOOD" but it looks like you pass a number. If you want to pass the id (number) from your enum you need a method to translate between the number and the enum element. Something like this
//in the enum Category
public static Category categoryFor(int id) {
switch (id) {
case 1:
return FOOD;
case 2:
return ELECTRICITY;
//... more case
default:
return HOTEL;
}
}
and then call it like
Category.categoryFor(resultSet.getInt(2))
or you need to store the actual name of the element in your table.
Also you shouldn't use *, "SELECT * ...", in your query but a list of column names so it is clear what column you map in your java code, "SELECT COMPANY_ID, CATEGORY_ID,TITLE,..."
As I understood correctly you're storing the category in your coupon as an enum constant in your code model. While storing it to the database you're mapping it to an integer value with the methods provided by you.
The culprit is in the Category.valueOf(resultSet.getString(3)) method/ part. The Enum.valueOf method is a default method on enums provided by Java and it's working with a String as a parameter - probably therefore you're also using resultSet.getString(3) instead of resultSet.getInt(3) which would have been more intuitive.
From the JavaDoc (which you can find here) it says:
... The name must match exactly an identifier used to declare an enum
constant in this type. (Extraneous whitespace characters are not
permitted.) ...
This means for to get the valueOf method working you need to call it exactly with one of the following values as its arguments: FOOD, ELECTRICITY, RESTAURANT, VACATION, HOTEL. Calling it with the int values like 1, 2, ... or 5 will lead to the IllegalArgumentException you face.
There a two solutions to fix the problem:
Either change your database model to store the enum values/ constants as strings in the table by calling the toString method on the category value before the insert into the database (then your code reading the coupons from the database can stay unchanged).
Or you need to provide your own custom implementation of the "valueOf" method - e.g. findCategoryById - which will work with integer values as its arguments. By writing your own findCategoryById method your code inserting the coupons into the database can remain unchanged.
To implement your own findCategoryById the signature of the method in the Category enum should look like:
public static Category findCategoryById(int index)
Then you can iterate through all available constants by Category.values() and compare the cat with the argument passed to the method and return the matching value based on it.
In case none matches you can simply return null or also throw an IllegalArgumentException. The latter one I'd personally prefer since it follows the "fail fast" approach and can avoid nasty and time consuming search for bugs.
Note: Enums in Java also have an auto generated/ auto assigned ordinal. You can simply request it by calling the ordinal method on a value of your enum. In your case the ordinals are matching the self assigned cat values, so that you could make use of them, instead of maintaining the cat attributes yourself.
When working with the ordinals it's worth mentioning that the order in which you specify your constants in the enum matters! When you change the order of the constants so the ordinals will. Therefore you also need to be careful when working with ordinals. Therefore you might prefer sticking with your current approach (which is not bad at all and widely used), since it avoids the ordering problems ordinals have.

Is it possible to assign the return of a lambda method to a variable in java?

I have a function that builds a SQL query with a dynamic amount of where clauses.
The SQL query exeuction is stored into a ResultSet, I would like to declare this result set in the try with resource. However, this requires the declared elements to implements AutoCloseable, and the type String does not.
My idea was to use a lambda method, pass it as a parameter of executeQuery in order to use its return as the String parameter, like so:
try (Statement s = getStatement();
String test = "test";
ResultSet rs = s.executeQuery(() -> {
String query = "build y query here";
return query;
});
)
But this gives the following error on executeQuery:
The method executeQuery(String) in the type Statement is not
applicable for the arguments (() -> {})
AS well as this on () ->:
he target type of this expression must be a functional interface
Is there a way to do what I want to achieve ?
executeQuery takes a string. It does not take a Supplier<String> or anything similar. It requires the query to be supplied eagerly. The closest thing to what you're suggesting would be:
Supplier<String> supplier = () -> {
String query = "build y query here";
return query;
};
ResultSet rs = s.executeQuery(supplier.get());
Seeing as the supplier cannot be evaluated lazily, constrained by the signatures that Statement provides, there is absolutely no value in doing this. You may as well just extract that to a method.
ResultSet rs = s.executeQuery(buildQuery());
// ...
String buildQuery() {
String query = "build y query here";
return query;
}
Theoretically, they could add a Supplier<String> signature which would allow the Statement to defer construction of the query up until the last possible moment. This would likely be in the order of milliseconds later than if it has been constructed eagerly.
I believe you can use try-with-resources for result set itself:
try (PreparedStatement ps = connection.prepareStatement(sql)) {
setupParams(ps, params);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
// process the row
}
}
}
In general instead of working with fairly low-level (and hence painful ;) ) JDBC you could use libraries like JDBC Template of spring or JDBI
If you totally need to run the queries with plain JDBC, consider reading this article
It goes a couple of steps further in this direction and comes up with pretty interesting solution

Bad grammar SQL Exception while reading the values using rowmapper

This is my Model class
//Model
public class CustomerData {
private String locomotive_id;
private String customer_name;
private String road_number;
private String locomotive_type_code;
private String in_service_date;
private String part_number;
private String emission_tier_type;
private String airbrake_type_code;
private String lms_fleet;
private String aar_road;
private String locomotive_status_code;
// Getters and Setters
Here is my RowMapper implementation
//RowMapper
public class CustomerDataResponseMapper implements RowMapper {
#Override
public Object mapRow(ResultSet rs, int count) throws SQLException {
CustomerData customerData = new CustomerData();
customerData.setLocomotive_id(rs.getString("locomotive_id"));
customerData.setCustomer_name(rs.getString("customer_name"));
customerData.setRoad_number(rs.getString("road_number"));
customerData.setLocomotive_type_code(rs.getString("locomotive_type_code"));
customerData.setIn_service_date(rs.getString("in_service_date"));
customerData.setPart_number(rs.getString("part_number"));
customerData.setEmission_tier_type(rs.getString("emission_tier_type"));
customerData.setAirbrake_type_code(rs.getString("airbrake_type_code"));
customerData.setLms_fleet(rs.getString("lms_fleet"));
customerData.setAar_road(rs.getString("aar_road"));
customerData.setLocomotive_status_code(rs.getString("locomotive_status_code"));
return customerData;
}
}
And finally, I got my DaoImpl class here
//DaoImpl
public String getCustomersData(String locoId, String custName, String roadNumber) {
CustomerData resultSet = null;
String str = "";
if (locoId != null && locoId.length() > 0 && !(locoId.equals("0"))) {
str = "select locomotive_id,customer_name,road_number,model_type as locomotive_type_code,to_char(in_service_date,'yyyy-mm-dd') as in_service_date,loco_part_number as part_number, emission_tier_type as emission_tier_type, "
+ "air_brake_type as airbrake_type_code,lms_fleet,aar_road,locomotive_status_code from get_rdf_explorer.get_rdf_locomotive_detail where locomotive_id = ?";
resultSet = (CustomerData) jdbcTemplate.queryForObject(str, new CustomerDataResponseMapper(), locoId);
} else if ((custName != null && custName.length() > 0)
&& (roadNumber != null && roadNumber.length() > 0 && roadNumber != "0")) {
str = "select locomotive_id,customer_name,road_number,model_type as locomotive_type_code,to_char(in_service_date,'yyyy-mm-dd') as in_service_date,loco_part_number as part_number, emission_tier_type as emission_tier_type, "
+ "air_brake_type as airbrake_type_code,lms_fleet,aar_road,locomotive_status_code from get_rdf_explorer.get_rdf_locomotive_detail where customer_name = ? and road_number= ?";
resultSet = (CustomerData) jdbcTemplate.queryForObject(str, new CustomerDataResponseMapper(), custName, roadNumber);
} else {
str = "select distinct customer_name from get_rdf_explorer.get_rdf_locomotive_detail order by customer_name asc";
resultSet = (CustomerData) jdbcTemplate.queryForObject(str, new CustomerDataResponseMapper());
}
return resultSet.toString();
}
How can I conditionally get the values from the resultSet based on whether a particular column is present in the resultSet or not. As I am not getting all the columns all the time through my queries.
I am getting SQL bad grammar exception when specific column is not present in resultSet. For example when the third query to get distinct customer names get executed, in the resultSet only customerName would be there, but not the other columns.
It would be really a great help. Thanks a lot in advance.
Since you already have 3 separate queries why not have 3 separate RowMappers, one for each query. Your queries "know" what columns they return, so you can easily create those classes for RowMapper.
If you really want High-Fidelity solution you could create abstract base RowMapper for common parts and 3 subclasses for parts specifig to the query.
You can use a generic method which investigates the ResultSet's columns
#SuppressWarnings("unchecked")
public <T> T getColIfPresent(ResultSet rs, String columnName) throws SQLException {
ResultSetMetaData metaData = rs.getMetaData();
for (int i = 1; i <= metaData.getColumnCount(); i++) {
if (columnName.equals(metaData.getColumnName(i))) {
return (T) rs.getObject(columnName);
}
}
return null;// not present
}
Then, in row mapper.
customerData.setLocomotive_id(getColIfPresent(rs, "locomotive_id"));
...
This has O(n*m) complexity where n - the number of columns checked, m - the number of columns returned in ResultSet.
If you choose to ignore the SQLException, at least log it in DEBUG or TRACE level in case a different subtype of SQLException occurs, so that it's not lost.
Rather than conditionnaly getting columns, you could modify your SQL to match your mapper, like setting other field to empty string or null (I don't remmember if getString() crashes on null or something).
For example your third query would look like:
select distinct customer_name, null as "locomotive_id",'' as "road_number", null as model_type, [etc.] from get_rdf_explorer.get_rdf_locomotive_detail order by customer_name asc
So each query would have the same columns and you don't have to adapt. This is the solution if you d'ont really want/can't change the rowMapper (or want to have only one for this object).
But honestly I would go with ikketu's solution. You should make a separate mapper for the thrid query (plus, it wouldn't be complicated). Not goign with an ORM is a choice but you'll have redundancy problem anyway. I would even add that you should separate some of the logic in your code, this methods seems to be doing different thing (business logic depending on input, and database access) it's not very clear (after the third if, create a method like "getdistinctName()" or something).
Santosh, a quick workaround could be passing a flag to your rowmapper while supplying it to jdbcTemplate. I've done so many times to avoid multiple rowmapper.
resultSet = (CustomerData) jdbcTemplate.queryForObject(str, new CustomerDataResponseMapper(1), custName, roadNumber);
For the above changes, you need to overload constructor with the default one. Then you need to use your flag i.e. instance variable in mapRow() method to handle each situation separately.
You can use BeanPropertyRowMapper which will directly map field names of target class. Here is the javadoc.
The names are matched either directly or by transforming a name separating the parts with underscores to the same name using "camel" case. So, you can use it any other classes whenever you want to map directly to a class. Just have to make sure selected fields are remain in target class. And a default or no-arg constructor.
Following example to get CustomerData using BeanPropertyRowMapper
RowMapper<CustomerData> mapper = new BeanPropertyRowMapper<>(CustomerData.class);
List<CustomerData> result = jdbc.query("your query string...", mapper, query_args...);
So, then you can return first object or whatsoever.
My advice is to split your getCustomersData into three different methods. If you definitely want to ignore this advice, the quick and dirty solution is to protect the rs.getString(...) calls inside your rowMapper. Something like this:
try {
customerData.setLocomotive_id(rs.getString("locomotive_id"));
} catch (SQLException e) {
// ignore this exception. DO NOT TRY THIS AT HOME!
}
If numbers of columns are not fix then you should go with ColumnMapRowMapper based on its implementation even you do not require to create separate concrete class of RowMapper (i.e. CustomerDataResponseMapper ) you just need to pass instance of ColumnMapRowMapper in query as given below:
ColumnMapRowMapper rowMapper = new ColumnMapRowMapper();
List<Map<String, Object>> customerDataList = jdbcTemplate.query(sql,rowMapper, args);
Now you should create one method to manipulate this map like
private CustomerData fillCustomerDataFromMap(List<Map<String, Object>> customerDataList){
CustomerData customerData = new CustomerData();
for(Map<String, Object> map: customerDataList ){
customerData.setColumn(map.get("columnName"));
customerData.setColumn(map.get("columnName"));
customerData.setColumn(map.get("columnName"));
customerData.setColumn(map.get("columnName"));
.........
.........
.........
}
return customerData;
}
This is more readable and remove the boilerplate codes and not throw any exception if column name is not present in map (it will simply returns null if column name is not present in map)
Reference of ColumnMapRowMapper :
https://github.com/spring-projects/spring-framework/blob/master/spring-jdbc/src/main/java/org/springframework/jdbc/core/ColumnMapRowMapper.java
Based on the logic of your queries i see that before executing sql query in get_rdf_explorer.get_rdf_locomotive_detail there are some records and 3 options of getting of necessary (unique) record are possible:
by locomotive_id
by customer_name and road_number
ANY record (all records must have same customer_name, else SQL distinct without any conditions return more than 1 row)
So, in the 3rd option you can get any 1 record with all attributes equal to NULL and NOT NULL customer_name value:
str = "select null as locomotive_id, customer_name, null as road_number,
<other attributes> from get_rdf_explorer.get_rdf_locomotive_detail where
rownum = 1";`

MySQL query called from Java - configure the "WHERE" parameter

I am connecting to a mysql db from standalone java application. My application uses several filters that determine, which data will be selected from db.
In some cases, I would like to construct the select command in a way, that its "WHERE" parameter is ignored and selects all values from db instead.
This is my example:
String query = "SELECT * from messages WHERE type='" + type + "' ORDER BY id DESC";
The variable type can contain some specific type that matches the Varchar of the items in my db. However, a user can set the type to "all values" (or something like that, I hope this is clear enough), in which case, the query would select ALL values from db (it will ignore the where parameter).
I know I could do this by simply putting some if statements in my code and call a different select command in every branch, but this would be highly ineffective in case that several specifications (attributes inside WHERE parameter) are used.
For example:
String query = "SELECT * from messages WHERE type='" + type + "' AND time='" + time + "' ORDER BY id DESC";
I am not sure whether this is even possible to do. If not, sorry about dumm question... Thanks in advance!
I think you will have to do it through code, nothing in SQL to do what you want to do. Typically, people use ORM like Hibernate and construct the query in more secure way (to avoid SQL injection) instead of using String concatenation.
This is how it is done in Hibernate: http://docs.jboss.org/hibernate/orm/3.3/reference/en/html/querycriteria.html#querycriteria-narrowing
You could create a Type class and a Time class and so on. This classes would contain a function called getSQL() which returns the type or in case of all types "".
The WHERE clause let you filter the dataset according to several predicates. Not specifying it means no filtering, hence all possible values for any predicates you would add.
If you consider the WHERE clause as a predicate, and predicates as either sets of predicates or atomic predicates, you can easily produce your clause by walking through the predicate nest and generate the needed string. If the string comes back empty, just elide the WHERE clause altogether.
interface Predicate {
public String toString();
}
// further derive this class for specific predicates
class AtomPredicate implements Predicate {
public AtomPredicate(){}
public toString() {}
}
// basic predicate set
class SetPredicate implements Predicate {
public SetPredicate(String connector){this.connector = connector;}
private ArrayList<Predicate> set;
private String connector;
public toString(){
String res, tmp;
int i;
if (set.size() == 0) return "";
while (res.size() == 0 && i < set.size()) {
res = set[i++].toString();
}
for (; i < set.size(); ++i) {
tmp = set[i].toString();
if (tmp.size() > 0)
res += connector + " " + tmp;
}
if (res.size() > 0)
return "(" + res + ")";
}
class WhereClause {
public WhereClause() {}
private Predicate predicate;
public toString(){
String res = predicate.toString();
if (res.size() > 0) return "WHERE " + res;
return "";
}
}
You can start from that basic outline, and expand as needed. You should however try to look for an existing solution first, like the jboss library linked in another answer, to avoid reinventing the wheel.

How to use Annotations with iBatis (myBatis) for an IN query?

We'd like to use only annotations with MyBatis; we're really trying to avoid xml. We're trying to use an "IN" clause:
#Select("SELECT * FROM blog WHERE id IN (#{ids})")
List<Blog> selectBlogs(int[] ids);
MyBatis doesn't seem able to pick out the array of ints and put those into the resulting query. It seems to "fail softly" and we get no results back.
It looks like we could accomplish this using XML mappings, but we'd really like to avoid that. Is there a correct annotation syntax for this?
I believe the answer is the same as is given in this question. You can use myBatis Dynamic SQL in your annotations by doing the following:
#Select({"<script>",
"SELECT *",
"FROM blog",
"WHERE id IN",
"<foreach item='item' index='index' collection='list'",
"open='(' separator=',' close=')'>",
"#{item}",
"</foreach>",
"</script>"})
List<Blog> selectBlogs(#Param("list") int[] ids);
The <script> element enables dynamic SQL parsing and execution for the annotation. It must be very first content of the query string. Nothing must be in front of it, not even white space.
Note that the variables that you can use in the various XML script tags follow the same naming conventions as regular queries, so if you want to refer to your method arguments using names other than "param1", "param2", etc... you need to prefix each argument with an #Param annotation.
I believe this is a nuance of jdbc's prepared statements and not MyBatis. There is a link here that explains this problem and offers various solutions. Unfortunately, none of these solutions are viable for your application, however, its still a good read to understand the limitations of prepared statements with regards to an "IN" clause. A solution (maybe suboptimal) can be found on the DB-specific side of things. For example, in postgresql, one could use:
"SELECT * FROM blog WHERE id=ANY(#{blogIds}::int[])"
"ANY" is the same as "IN" and "::int[]" is type casting the argument into an array of ints. The argument that is fed into the statement should look something like:
"{1,2,3,4}"
Had some research on this topic.
one of official solution from mybatis is to put your dynamic sql in #Select("<script>...</script>"). However, writing xml in java annotation is quite ungraceful. think about this #Select("<script>select name from sometable where id in <foreach collection=\"items\" item=\"item\" seperator=\",\" open=\"(\" close=\")\">${item}</script>")
#SelectProvider works fine. But it's a little complicated to read.
PreparedStatement not allow you set list of integer. pstm.setString(index, "1,2,3,4") will let your SQL like this select name from sometable where id in ('1,2,3,4'). Mysql will convert chars '1,2,3,4' to number 1.
FIND_IN_SET don't works with mysql index.
Look in to mybatis dynamic sql mechanism, it has been implemented by SqlNode.apply(DynamicContext). However, #Select without <script></script> annotation will not pass parameter via DynamicContext
see also
org.apache.ibatis.scripting.xmltags.XMLLanguageDriver
org.apache.ibatis.scripting.xmltags.DynamicSqlSource
org.apache.ibatis.scripting.xmltags.RawSqlSource
So,
Solution 1: Use #SelectProvider
Solution 2: Extend LanguageDriver which will always compile sql to DynamicSqlSource. However, you still have to write \" everywhere.
Solution 3: Extend LanguageDriver which can convert your own grammar to mybatis one.
Solution 4: Write your own LanguageDriver which compile SQL with some template renderer, just like mybatis-velocity project does. In this way, you can even integrate groovy.
My project take solution 3 and here's the code:
public class MybatisExtendedLanguageDriver extends XMLLanguageDriver
implements LanguageDriver {
private final Pattern inPattern = Pattern.compile("\\(#\\{(\\w+)\\}\\)");
public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
Matcher matcher = inPattern.matcher(script);
if (matcher.find()) {
script = matcher.replaceAll("(<foreach collection=\"$1\" item=\"__item\" separator=\",\" >#{__item}</foreach>)");
}
script = "<script>" + script + "</script>";
return super.createSqlSource(configuration, script, parameterType);
}
}
And the usage:
#Lang(MybatisExtendedLanguageDriver.class)
#Select("SELECT " + COLUMNS + " FROM sometable where id IN (#{ids})")
List<SomeItem> loadByIds(#Param("ids") List<Integer> ids);
I've made a small trick in my code.
public class MyHandler implements TypeHandler {
public void setParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {
Integer[] arrParam = (Integer[]) parameter;
String inString = "";
for(Integer element : arrParam){
inString = "," + element;
}
inString = inString.substring(1);
ps.setString(i,inString);
}
And I used this MyHandler in SqlMapper :
#Select("select id from tmo where id_parent in (#{ids, typeHandler=ru.transsys.test.MyHandler})")
public List<Double> getSubObjects(#Param("ids") Integer[] ids) throws SQLException;
It works now :)
I hope this will help someone.
Evgeny
Other option can be
public class Test
{
#SuppressWarnings("unchecked")
public static String getTestQuery(Map<String, Object> params)
{
List<String> idList = (List<String>) params.get("idList");
StringBuilder sql = new StringBuilder();
sql.append("SELECT * FROM blog WHERE id in (");
for (String id : idList)
{
if (idList.indexOf(id) > 0)
sql.append(",");
sql.append("'").append(id).append("'");
}
sql.append(")");
return sql.toString();
}
public interface TestMapper
{
#SelectProvider(type = Test.class, method = "getTestQuery")
List<Blog> selectBlogs(#Param("idList") int[] ids);
}
}
In my project, we are already using Google Guava, so a quick shortcut is.
public class ListTypeHandler implements TypeHandler {
#Override
public void setParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, Joiner.on(",").join((Collection) parameter));
}
}
In Oracle, I use a variant of Tom Kyte's tokenizer to handle unknown list sizes (given Oracle's 1k limit on an IN clause and the aggravation of doing multiple INs to get around it). This is for varchar2, but it can be tailored for numbers (or you could just rely on Oracle knowing that '1' = 1 /shudder).
Assuming you pass or perform myBatis incantations to get ids as a String, to use it:
select #Select("SELECT * FROM blog WHERE id IN (select * from table(string_tokenizer(#{ids}))")
The code:
create or replace function string_tokenizer(p_string in varchar2, p_separator in varchar2 := ',') return sys.dbms_debug_vc2coll is
return_value SYS.DBMS_DEBUG_VC2COLL;
pattern varchar2(250);
begin
pattern := '[^(''' || p_separator || ''')]+' ;
select
trim(regexp_substr(p_string, pattern, 1, level)) token
bulk collect into
return_value
from
dual
where
regexp_substr(p_string, pattern, 1, level) is not null
connect by
regexp_instr(p_string, pattern, 1, level) > 0;
return return_value;
end string_tokenizer;
You could use a custom type handler to do this. For example:
public class InClauseParams extends ArrayList<String> {
//...
// marker class for easier type handling, and avoid potential conflict with other list handlers
}
Register the following type handler in your MyBatis config (or specify in your annotation):
public class InClauseTypeHandler extends BaseTypeHandler<InClauseParams> {
#Override
public void setNonNullParameter(final PreparedStatement ps, final int i, final InClauseParams parameter, final JdbcType jdbcType) throws SQLException {
// MySQL driver does not support this :/
Array array = ps.getConnection().createArrayOf( "VARCHAR", parameter.toArray() );
ps.setArray( i, array );
}
// other required methods omitted for brevity, just add a NOOP implementation
}
You can then use them like this
#Select("SELECT * FROM foo WHERE id IN (#{list})"
List<Bar> select(#Param("list") InClauseParams params)
However, this will not work for MySQL, because the MySQL connector does not support setArray() for prepared statements.
A possible workaround for MySQL is to use FIND_IN_SET instead of IN:
#Select("SELECT * FROM foo WHERE FIND_IN_SET(id, #{list}) > 0")
List<Bar> select(#Param("list") InClauseParams params)
And your type handler becomes:
#Override
public void setNonNullParameter(final PreparedStatement ps, final int i, final InClauseParams parameter, final JdbcType jdbcType) throws SQLException {
// note: using Guava Joiner!
ps.setString( i, Joiner.on( ',' ).join( parameter ) );
}
Note: I don't know the performance of FIND_IN_SET, so test this if it is important
I had done this with postgresql.
#Update('''
UPDATE sample_table
SET start = null, finish = null
WHERE id=ANY(#{id});
''')
int resetData(#Param("id") String[] id)
ANY works like the IN.
Code above is using groovy but can be converted into java by replacing the single quotes into double.

Categories

Resources