Mybatis-Error setting null parameter - mybatis

I used mybatis-spring-1.0.3-SNAPSHOT mybatis-3.0.6 spring3.0.6.I tried to delete record from a table like this:
<delete id="deleteNote" parameterType="hashMap">
DELETE FROM BBSCS_NOTE
<where>
<if test="ids !=null and ids.length > 0">
<foreach collection="ids" item="id" open="(" close=")" separator=",">
ID IN #{id}
</foreach>
</if>
<if test="toID != null and toID != ''">AND TOID = #{toID}</if>
<if test="fromID != null and fromID != ''">AND FROMID = #{fromID}</if>
<if test="noteType != -1">AND NOTETYPE = #{noteType}</if>
</where>
</delete>
As you have seen,it's a dynamic sql.The java test code like this:
Map map = new HashMap();
String ids[] = {"1","2","3"};
map.put("ids", ids);
noteService.del(map);
When I executed java test code,there was some exception like this:
org.springframework.jdbc.UncategorizedSQLException: Error setting null parameter. Most JDBC drivers require that the JdbcType must be specified for all nullable parameters. Cause: java.sql.SQLException: Invalid column type
; uncategorized SQLException for SQL []; SQL state [null]; error code [17004]; Invalid column type; nested exception is java.sql.SQLException: Invalid column type
Why?Can you give me some advice to solve this problem?Thanks.

OK I see a few problems. First, when setting a null parameter into a Prepared Statement or a Callable Statement MyBatis needs to know the jdbc type.
Like this,
#{myNullParamenter, jdbcType=VARCHAR}
You're also generating your 'in clause incorrectly. You need to use the foreach tag to only generate list of the values. Move the "ID IN" part out of the foreach tag.
<if test="ids !=null and ids.length > 0">
ID IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</if>
I would also recommend against using HashMaps. The new Mapper classes are much better.

The problem is that since the 3.0.x versions the default JDBC type for null parameters is Types.OTHER which not supported by some JDBC drivers like Oracle 10g.
Here a post that explain this issue.
The solution I found is very simple, I set jdbcTypeForNull to NULL in the configuration file.
<configuration>
<properties resource="mybatis-config.properties" />
<settings>
<setting name="jdbcTypeForNull" value="NULL" />
</settings>
<environments default="development">
....
</environments>
<mappers>
....
</mappers>
</configuration>

Related

Mybatis type handler not fount

I have:
<delete id="deleteTaskScheduling" parameterType="List">
DELETE FROM ct_task_scheduling WHERE taskid IN
<foreach collection="list" item="taskid" open="(" close=")" index="index" separator=",">
#{taskid}
</foreach>
</delete>
and my POJO has taskid
#Data
#NoArgsConstructor
public class TaskDTO implements Serializable {
private String taskid;
...
}
and I am passing List of TaskDTO to the interface:
List <TaskDTO> taskDTOs = new ArrayList <> ();
...
taskDAO.deleteTaskScheduling(taskDTOs);
but I am getting this exception:
java.lang.IllegalStateException: Type handler was null on parameter mapping for property '__frch_taskid_0'. It was either not specified and/or could not be found for the javaType (com.mycompany.DTO.TaskDTO) : jdbcType (null) combination.] with root cause
Can someone help me? Thanks
In <foreach />, the variable specified in item represents each element of the list which is TaskDTO in your case.
<delete id="deleteTaskScheduling">
DELETE FROM ct_task_scheduling WHERE taskid IN
<foreach collection="list" item="x" open="(" close=")" separator=",">
#{x.taskid}
</foreach>
</delete>
Please read the documentation for the details.
https://mybatis.org/mybatis-3/dynamic-sql.html#foreach

Use an property of an object which is array of strings in mapper XML

I want to refer to a property in an object in my mapper file which is an array of strings in a SQL IN criteria. The query does a count, so all it needs to return is a numeric value. The query needs to adjust its count based on a flexible set of criteria defined in a filter object. Some filters will be present (ie. not null), and others will be absent.
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="TotalUniqueUsers">
<select id="getTotalUniqueUsers"
resultType="int"
parameterType="RequestFilter">
SELECT *
FROM MY_TABLE
WHERE
<if test="quarterList!=null and quarterList.length>0">
AND trim(FISCAL_QUARTER_NAME) IN #{quarterList javaType=list}
</if>
</select>
</mapper>
public class RequestFilter {
private String[] quarterList;
public String[] getQuarterList(){
return this.quarterList;
}
public void setQuarterList(String[] quarterList){
this.quarterList=quarterList;
}
}
Note, there is no type handler for RequestFilter. I did not think I needed one. I'm not trying to take an object and condense it into say one field in some weird way. All I want to do is have an input parameter to the
With the above, I get
org.apache.ibatis.exceptions.PersistenceException:
...
Caused by: org.apache.ibatis.reflection.ReflectionException: There is no getter for property named 'quarterList javaType=list' in 'class RequestFilter'
I tried javaType=Array also, but get the same result. If I change the
#{quarterList javaType=list}
to
#{quarterList}
it says the typeHandler is null for the RequestFilter.
There is no portable way to set the list or array to IN prepared statement parameter in JDBC and therefore in mybatis (there are ways to do that if you are using postgres).
So in the general case you need to dynamically generate the query with a parameter per element in the list:
<select id="getTotalUniqueUsers"
resultType="int"
parameterType="RequestFilter">
SELECT *
FROM MY_TABLE
WHERE
<if test="quarterList!=null and quarterList.length>0">
trim(FISCAL_QUARTER_NAME) IN (
<foreach item='quarter' collection='quarterList' separator=','>
#{quarter}
</foreach>
)
</if>
</select>

How can I skip query if where_in clause is empty in MyBatis 3?

select * from users where id in ()
The query is shown above.
<select id="getByIds" resultMap="BaseResultMap">
SELECT
<include refid="BaseColumnList"/>
FROM users
WHERE id IN
<foreach item="id" collection="ids"
open="(" separator="," close=")">
#{id}
</foreach>
</select>
If the Param ids is empty, Mybatis will throw BadSqlGrammarException, which generate a query like 'select * from users where id in'.
How can I skip query and return empty list if ids is empty?
How can I skip query and return empty list if ids is empty?
To skip the query (not execute it), just don't call Mybatis.
The calling code should check if ids is empty:
return null == ids || ids.isEmpty() ? new ArrayList<User>() : session.select("getByIds", ids);
This is exactly what is asked in the question.
If you really want Mybatis to handle this, then produced query must be valid because must be executed (then not skipped) to return empty result quickly. that means forget something like id = <!-- a value that will never exist in the table --> because it could surely involve a (free and useless) full scan to search the unexisting value.
Then:
WHERE
<choose>
<when test="ids==null || ids.isEmpty()">
1 = 0 <!-- a test returning false, to adapt depending on you DB vendor -->
</when>
<otherwise>
id IN <foreach item="id" collection="ids" open="(" separator="," close=")">#{id}</foreach>
</otherwise>
</choose>
Another option to confirm would consist in using interceptors to "cancel" the query before its execution, but this is definitely overkill complexity for what has to be achieved here.
java code function
List<ApiPriceChlogEntity> getApiAndDevPrice(#Param("apiKeys") List<String> currentApiKey, #Param("devKeys") List<String> currentDevKey, #Param("startDate") Date startDate);
the mapper file
<select id="getApiAndDevPrice" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM t_api_price_chlog tab1
<where>
<if test="apiKeys.size() > 0">
tab1.api_key IN
<foreach collection="apiKeys" item="item" separator="," open="(" close=")" index="">
#{item}
</foreach>
</if>
<if test="devKeys.size() > 0">
AND tab1.dev_key IN
<foreach collection="devKeys" item="item" separator="," open="(" close=")" index="">
#{item}
</foreach>
</if>
<if test="startDate != null">
AND tab1.change_date >= #{startDate}
</if>
</where>
I have test it,hope to help u.
Using test:
<select id="getByIds" resultMap="BaseResultMap">
SELECT
<include refid="BaseColumnList"/>
FROM users
<if test="ids!= null">
WHERE id IN
<foreach item="id" collection="ids"
open="(" separator="," close=")">
#{id}
</foreach>
</if>
</select>
Use mybatis Interceptor, create a nil PreparedStatement Object and return it.
#Intercepts({#Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),
#Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})})
#Slf4j
public class MyBatisInterceptor implements Interceptor {
#Override
public Object intercept(Invocation invocation) throws Throwable {
//...your code
//return invocation.proceed();
return new NullExecutor();
}
}
MyBatis wants to get a PreparedStatement Object as a return value, the "NullExecutor" is:
public class NullExecutor implements PreparedStatement {}
Nothing todo, just write few thing, like:
#Override
public void setDouble(int parameterIndex, double x) throws SQLException {
//empty here
}
#Override
public boolean execute() throws SQLException {
//it's ok,do noting.
return false;
}
...etc
Add your configuration:
#Bean
public MyBatisInterceptor myBatisInterceptor() {
return new MyBatisInterceptor();
}
You need to get the "BoundSql" in Interceptor, then you can get sql and args, just google it.
I'm not sure it can work, just for learning.

MyBatis - Returning a HashMap

I want the returned result of the select statement below to be Map<String, Profile>:
<select id="getLatestProfiles" parameterType="string" resultMap="descProfileMap">
select ml.layerdescription, p1.*
from ( select max(profile_id) as profile_id
from SyncProfiles
group by map_layer_id) p2
inner join SyncProfiles p1 on p1.profile_id = p2.profile_id
inner join maplayers ml on ml.LAYERID = p1.MAP_LAYER_ID
where ml.maxsite = #{site}
</select>
I have seen this post which maps a String to a custom class, but the key was part of the custom class. In my query above, the layerdescription field is not part of the Profile class since I'm aiming to have the Profile class strictly represent the syncprofiles table and the layerdescription field is in another table.
My interface looks like:
public Map<String, Profile> getLatestProfiles(final String site);
How should descProfileMap be defined? I want to do something like:
<resultMap id="descProfileMap" type="java.util.HashMap">
<id property="key" column="layerdescription" />
<result property="value" javaType="Profile"/>
</resultMap>
But this is clearly wrong. Thanks for your help!
Achieving this requires 2 steps:
-Use association and nested resultMap:
<resultMap type="Profile" id="profileResultMap">
<!-- columns to properties mapping -->
</resultMap
<resultMap type="map" id="descProfileMap">
<id property="key" column="layerdescription" />
<association property="value" resultMap="profileResultMap" />
</resultMap>
-Add every record to a Map with expected structure using ResultHandler:
final Map<String, Profile> finalMap = new HashMap<String, Profile>();
ResultHandler handler = new ResultHandler() {
#Override
public void handleResult(ResultContext resultContext) {
Map<String, Object> map = (Map) resultContext.getResultObject();
finalMap.put(map.get("key").toString()), (Profile)map.get("value"));
}
};
session.select("getLatestProfiles", handler);
If you run that as is, expect this exception will likely be raised:
org.apache.ibatis.executor.ExecutorException: Mapped Statements with
nested result mappings cannot be safely used with a custom
ResultHandler. Use safeResultHandlerEnabled=false setting to bypass
this check or ensure your statement returns ordered data and set
resultOrdered=true on it.
Then following the suggestion, you can either disable the check globally in Mybatis config:
According to the documentation:
safeResultHandlerEnabled: Allows using ResultHandler on nested statements. If allow, set the
false. Default: true.
<settings>
<setting name="safeResultHandlerEnabled" value="false"/>
</settings>
or specify your result is ordered in the statement:
The documentation states:
resultOrdered This is only applicable for nested result select
statements: If this is true, it is assumed that nested results are
contained or grouped together such that when a new main result row is
returned, no references to a previous result row will occur anymore.
This allows nested results to be filled much more memory friendly.
Default: false.
<select id="getLatestProfiles" parameterType="string" resultMap="descProfileMap" resultOrdered="true">
But I have not found anyway to specify this statement option when using annotations.

MyBatis include same <sql> fragment multiple times for joined tables of same type

Update 2016-06-07 - see my answer below for solution
Trying to find out if there is a way to reuse same fragment in one query.
Consider this:
<sql id="personFields">
per.id person_id,
per.created_at person_created_at,
per.email_address person_email_address,
per.first_name person_first_name,
per.last_name person_last_name,
per.middle_name person_middle_name
</sql>
The "per." alias is used to avoid column name clashing when using in queries with muiltiple joined tables.
It is included like this:
SELECT
<include refid="com.acme.data.mapper.PersonMapper.personFields"/>
FROM Person per
The problem is that it cannot be used more than once per query because we have the "per." alias.
Would be great to have something like this:
<sql id="personFields">
#{alias}.id #{alias}_person_id,
#{alias}.created_at #{alias}_person_created_at,
#{alias}.email_address #{alias}_person_email_address,
#{alias}.first_name #{alias}_person_first_name,
#{alias}.last_name #{alias}_person_last_name,
#{alias}.middle_name #{alias}_person_middle_name
</sql>
And include it like this:
SELECT
<include refid="com.acme.data.mapper.PersonMapper.personFields" alias="per1"/>,
<include refid="com.acme.data.mapper.PersonMapper.personFields" alias="per2"/>
FROM Person per1
JOIN Person per2 ON per2.parent_id = per1.id
This is currently possible (not sure since what version):
Define it:
<sql id="AddressFields">
${alias}.id ${prefix}id,
${alias}.created_at ${prefix}created_at,
${alias}.street_address ${prefix}street_address,
${alias}.street_address_two ${prefix}street_address_two,
${alias}.city ${prefix}city,
${alias}.country ${prefix}country,
${alias}.region ${prefix}region,
${alias}.sub_region ${prefix}sub_region,
${alias}.postal_code ${prefix}postal_code
</sql>
Select it:
<sql id="PurchaseSelect">
SELECT
purchase.*,
<include refid="foo.bar.mapper.entity.AddressMapper.AddressFields">
<property name="alias" value="billing_address"/>
<property name="prefix" value="billing_address_"/>
</include>,
<include refid="foo.bar.mapper.entity.AddressMapper.AddressFields">
<property name="alias" value="shipping_address"/>
<property name="prefix" value="shipping_address_"/>
</include>
FROM purchase
LEFT JOIN address billing_address ON purchase.billing_address_id = billing_address.id
LEFT JOIN address shipping_address ON purchase.shipping_address_id = shipping_address.id
</sql>
Map it:
<resultMap id="PurchaseResult" type="foo.bar.entity.sales.Purchase">
<id property="id" column="id"/>
<!-- any other purchase fields -->
<association property="billingAddress" columnPrefix="billing_address_" resultMap="foo.bar.mapper.entity.AddressMapper.AddressResult"/>
<association property="shippingAddress" columnPrefix="shipping_address_" resultMap="foo.bar.mapper.entity.AddressMapper.AddressResult"/>
</resultMap>
Unfortunately you can't do that, others have already tried (see some issues here or here). The includes are inlined and take no parameters.
One solution off the top of my head would be something like this:
<sql id="fragment">
<foreach collection="list" separator="," item="alias">
${alias}.id ${alias}_person_id,
${alias}.created_at ${alias}_person_created_at,
${alias}.email_address ${alias}_person_email_address,
${alias}.first_name ${alias}_person_first_name,
${alias}.last_name ${alias}_person_last_name,
${alias}.middle_name ${alias}_person_middle_name
</foreach>
</sql>
include it just once like:
<select id="getPersons" parameterType="java.util.List" ... >
SELECT
<include refid="fragment"/>
FROM Person per1
JOIN Person per2 ON per2.parent_id = per1.id
</select>
and have a parameterType="java.util.List" sent from the mapper interface:
public interface PersonMapper {
public List<String> getPersons(List<String> aliases);
// called with aliases = ["per1", "per2"]
}
This is ugly because your (higher level) code will have to know the aliases used inside the (lower) queries and also uses string substitutions for the fragment (${...} instead of #{...}) which can be dangerous if not handled properly... but if you can live with that...
This feature is asked to be implemented for more than 2 years (https://code.google.com/p/mybatis/issues/detail?id=652).
This static parameters in include can be found implemented in this fork: https://github.com/kmoco2am/mybatis-3
It is fully working and it has the same syntax as standard configuration parameters or static variables:
<sql id="sometable">
${prefix}Table
</sql>
<select id="select" resultType="map">
select
field1, field2, field3
from
<include refid="sometable">
<placeholder name="prefix" value="Some"/>
</include>
</select>
Hopefully, it will be soon accepted for the main source repository.