MyBatis Performance of bulk operator - mybatis

I have used MyBatis-spring + Java. I need to insert >10000 records into table in one transaction. To do it, I have used a mapper:
<insert id="saveBulk" parameterType="List">
INSERT INTO "quote" ("id", "mi_id", "timestamp", "open", "close", "low", "high", "volume", "period")
VALUES
<foreach collection="list" item="item" separator=",">
( #{item.key}, #{item.marketInstrumentKey}, #{item.timestamp}, #{item.open}, #{item.close}, #{item.low},
#{item.high}, #{item.volume}, #{item.period}::quote_period)
</foreach>
</insert>
And pass a List to this statement. It is worked extremely slowly for 2000-3000 records, but the 10000 records are inserted more than 4 minutes (I should increase the timeout interval)! The same 10000 records are inserted to the same DB via PgAdmin for less than 10 second. I tried to trace the processing of this operation and found a bottleneck
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.update(stmt);
} finally {
closeStatement(stmt);
}
}
The StatementHandler is calculated few minutes, and few minutes for prepareStatement.
I understand, why it happens: 10000 records with 9 field for each record. All these 100k fields should be inserted as parameters into statement.
And how can I accelerate this process?
UPDATE:
I implements a batch save with using of "BATCH" mode of sqlFactory and #Transactional.
This is configuration of mybatis-spring XML config:
<bean id="sqlBatchTemplate" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg index="0" ref="sqlSessionFactory"/>
<constructor-arg index="1" value="BATCH"/>
</bean>
<bean id="quoteBatchMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
<property name="mapperInterface" value="tafm.dataaccess.mybatis.mapper.QuoteMapper"/>
<property name="sqlSessionFactory" ref="sqlSessionFactory"/>
<property name="sqlSessionTemplate" ref="sqlBatchTemplate"/>
</bean>
<bean id="dataAccessBatch" class="tafm.dataaccess.DataAccess">
<property name="quoteMapper" ref="quoteBatchMapper"/>
</bean>
Then, I have implemented a "batch" method:
#Transactional
public void saveBulk(List<Quote> quotes) {
for(Quote q:quotes) {
mapper.save(q);
}
}
mapper - is XML mapper for entity Quote:
<insert id="saveBulk" parameterType="List">
INSERT INTO "quote" ("id", "mi_id", "timestamp", "open", "close", "low", "high", "volume", "period")
VALUES
<foreach collection="list" item="item" index="index" separator=",">
( #{item.key}, #{item.marketInstrumentKey}, #{item.timestamp}, #{item.open}, #{item.close}, #{item.low},
#{item.high}, #{item.volume}, #{item.period}::quote_period)
</foreach>
</insert>
It is working fast

you can do something like this.
Mapper.xml
<insert id="saveBulk" parameterType="List">
INSERT INTO "quote" ("id", "mi_id", "timestamp", "open", "close", "low", "high", "volume", "period")
VALUES
<foreach collection="list" item="item" separator=",">
( #{item.key}, #{item.marketInstrumentKey}, #{item.timestamp}, #{item.open}, #{item.close}, #{item.low},
#{item.high}, #{item.volume}, #{item.period}::quote_period)
</foreach>
</insert>
In Java File
public void insertBulkData(List<Item> list)
{
int total = list.size();
int interval = 1000;
int from = 0;
int to = 0;
while (to <= total)
{
from = to == 0 ? 0 : to;
to = (to + interval) <= total ? (to + interval) : total;
saveBulk(list.subList(from, to));
if (to == total)
break;
}
}
above code using interval at 1000.so 1000 records will insert at a time.you can modify this interval also.
you have to call insertBulkData(list).
I hope this will helpful to you.

Related

how to directly add ENUM in Mybatis IN query ,with no params

i directly have to check ENUM in where condition ,
<if test="params.fileTypes != null and !params.fileTypes.isEmpty()">
AND
RTRIM(INTH.IF_FILETYPE) IN
<foreach item="item" index="index"
collection="params.fileTypes" open="(" separator="," close=")">
#{item}
</foreach>
</if>
<if test="params.fileTypes.isEmpty()">
AND
RTRIM(IF_FILETYPE) IN (#{#com.xyz.wealth.appconfiguration.domain.FileType})
</if>
Assuming that com.xyz.wealth.appconfiguration.domain.FileType is the enum you are referring to and it has two values. e.g.
public enum FileType {
DOCUMENT,
IMAGE
}
The following MyBatis statement ...
RTRIM(IF_FILETYPE) IN (
<foreach item="x" separator=","
collection="#om.xyz.wealth.appconfiguration.domain.FileType#values()">
#{x}
</foreach>
)
... is translated into the SQL below and the enum values DOCUMENT and IMAGE are bound to the placeholders as string respectively.
RTRIM(IF_FILETYPE) IN (?, ?)

How to properly use the #Param annotation of Mybatis

I didn't use #Param annotation at first, this is my mapper.java
public void changeUserAuth(Integer userId,int identity);
, and this is my mapper.xml
<update id="changeUserAuth">
update user
<set>
<if test="identity != 0">identity = #{identity}</if>
</set>
<where>
<if test="userId != 0">userId = #{userId}</if>
</where>
</update>
then it works correctly!I continue to write like this, as follows:
//this's mapper.java
public void updateUserStatus(Integer userId);
<!--this is mapper.xml>
<update id="changeUserAuth">
update user
set deleteFlag= true
<where>
<if test="userId != 0">userId = #{userId}</if>
</where>
</update>
however,it gave an error,the message is
There is no getter for property named 'userId' in 'class.java.lang.Integer'
I can understand that mybatis cannot parse the Integer, but why it is not an error like my first use, just because I have an int type Parameter? In the second method, I have to use #Param annotation
Here is how you reference parameters in MyBatis statements.
I'll use this POJO in the following explanation.
public class User {
private Integer id;
private String name;
//...
}
When #Param is used
If you add #Param annotation on a parameter, you can use the specified name to reference the parameter. This is the simplest case.
A few examples:
List<USer> select(#Param("id") Integer userId, #Param("name") String userName);
void insert(#Param("record") User user);
<select id="select" resultType="User">
select * from users
<where>
<if test="id != null">and id = #{id}</if>
<if test="name != null">and name = #{name}</if>
</where>
</select>
<insert id="insert">
insert into users (id, name) values
(#{record.id}, #{record.name})
</insert>
Without #Param
If there is no #Param, it depends on several conditions.
When the mapper method takes only one parameter [1] and ...
... the sole parameter is assignable to java.util.List, you can reference the parameter as list.
List<User> selectByIds(List<Integer> ids);
<select id="select" resultType="User">
select * from users
where id in (
<foreach item="x" collection="list" separator=",">
#{x}
</foreach>
)
</select>
... the sole parameter is assignable to java.util.Collection, you can reference the parameter as collection.
List<User> selectByIds(Set<Integer> ids);
<select id="select" resultType="User">
select * from users
where id in (
<foreach item="x" collection="collection" separator=",">
#{x}
</foreach>
)
</select>
... there is a type handler mapped to the sole parameter (i.e. the parameter is String, Integer, etc.).
With MyBatis 3.5.2 and later, you can reference the parameter using any name (you should use sensible names for obvious reasons, though). e.g.
List<User> select(Integer id);
<select id="select" resultType="User">
select * from users
<where>
<if test="x != null">and id = #{y}</if>
</where>
</select>
With MyBatis 3.5.1
you can reference the parameter with any name in #{}.
you must reference the parameter as _parameter in ${}, test attribute of <if /> and <when /> and value attribute of <bind />. This is why your second example throws exception.
List<User> select(Integer id);
<select id="select" resultType="User">
select * from users
<where>
<if test="_parameter != null">and id = #{z}</if>
</where>
</select>
... there is no type handler mapped to the sole parameter (i.e. the parameter is POJO or Map<String, ?>), you can reference the parameter properties directly with their names (or the keys if the parameter is a Map).
void insert(User user);
<insert id="insert">
insert into users (id, name) values
(#{id}, #{name})
</insert>
When the mapper method takes multiple parameters
If the project is compiled with '-parameters' compiler option, you can reference the parameters using their names declared in the method signature. This is your first example.
List<USer> select(Integer userId, String userName);
<select id="select" resultType="User">
select * from users
<where>
<if test="userId != null">and id = #{userId}</if>
<if test="userName != null">and name = #{userName}</if>
</where>
</select>
Otherwise, you can reference the parameters using the names implicitly assigned by MyBatis i.e. arg0, arg1, ... (I would not recommend this as it's fragile and error-prone).
List<USer> select(Integer userId, String userName);
<select id="select" resultType="User">
select * from users
<where>
<if test="arg0 != null">and id = #{arg0}</if>
<if test="arg1 != null">and name = #{arg1}</if>
</where>
</select>
[1] RowBounds and ResultHandler does not count.

How to iterate List in MyBatis

iBatis to MyBatis Migration:
Need Help for MyBatis foreach logic, because the Map contains Value as ArrayList.
The below java code is the logic:
employeeRequest.put("ID", employeeId);
Map <String,ArrayList<String> employeeRequest = new HashMap<String, ArrayList<String>>();
Set<String> employeeSet = new HashSet<String>();
for(Employee employee: employeeList) {
String name = employee.getName();
String value = employee.getValue();
if("EMPLOYEE".equalsIgnoreCase(name) {
employeeSet.add(value)
}
}
if(!employeeSet.isEmpty()) {
employeeRequest.put("EMPLOYEE", new ArrayList<String>(employeeSet))
}
iBatis SQL:
My Previous code I am using iBatis which has the following query
<select id="getEmployeeName" resultclass="java.util.HashMap" parameterClass="java.util.Map">
SELECT EMP.EMPNAM NAME FROM EMPLOYEE EMP
WHERE EMP.ID = #ID#
<isNotEmpty property="EMPLOYEE" prepend="AND">
<iterate property="EMPLOYEE" conjunction="AND">
EMP.EMPNAM != #EMPLOYEE[]#
<iterate>
</isNotEmpty>
</select>
MyBatis SQL:
Now I am migrating to MyBatis, so formatted the query as below
<select id="getEmployeeName" resultclass="java.util.HashMap" parameterClass="java.util.Map">
SELECT EMP.EMPNAM NAME FROM EMPLOYEE EMP
WHERE EMP.ID = #{ID}#
<if test="EMPLOYEE !=null and EMPLOYEE>0">
<foreach collection="EMPLOYEE" index="index" item="item" separator="AND">
EMP.EMP_ID != ${item}
</foreach>
</if>
</select>
Could any one of you please help me with the correct query for the above java code logic.
Missing spaces around separator value: AND instead of just AND.
For parameters use #{param} to bind parameter instead of ${param} that just concatenates values to the SQL string. That does not prevent from working but that is very bad.
!= is not standard SQL and will not work for every DB vendor (although it might do with the one you are using), unlike NOT column = value,
<foreach collection="EMPLOYEE" index="index" item="item" separator=" AND ">
NOT EMP.EMP_ID = #{item}
</foreach>
furthermore better use IN:
EMP.EMP_ID NOT IN (<foreach collection="EMPLOYEE" index="index" item="item" separator=", ">
#{item}
</foreach>)

how to parameterize tablename in mybatis

I want to parameterize table_name:t_user_address_book(uid/500000).
for example: when uid = 1000, table_name = t_user_address_book0;
when uid = 500001, table_name = t_user_address_book1;
How to write?
public interface UserAddressBookMapper {
#Insert("insert into t_user_address_book? values(...)")
int upsert(Long uid, UserAddressBookMsg userAddressBookMsg);
}
You can choose the table with Mybatis XML code:
<choose>
<when test="uid gt 1000000">
<bind name="tableName" value="t_user_address_book2" />
</when>
<when test="uid gt 500000">
<bind name="tableName" value="t_user_address_book1" />
</when>
<otherwise>
<bind name="tableName" value="t_user_address_book0" />
</otherwise>
</choose>
Or you can compute the table name in the java and pass it as parameter.
Whatever your choice, the table name parameter in the query must be referenced with the $ notation instead of # since the value must replace the place holder as is to be part of the query and not being interpreted/bound/escaped as parameters are:
INSERT INTO ${tableName} ...
Despite use of XML, you can stick with annotations surrounding the query with <script> tags:
#Insert({"<script>",
"<choose> ...",
"INSERT ..."
"</script>"
})
Also when using Mapper interface with annotations, you need to name the parameters with there are more than 1:
#Insert("INSERT INTO table VALUES(#{uid}, #{userAddressBookMsg.propertyName1})")
int upsert(upsert(#Param("uid")Long uid, #Param("userAddressBookMsg") UserAddressBookMsg userAddressBookMsg);
However, it seems you want to split into multiple tables for volume issues, this is much complexity to handle while it would better be to keep a single table and look around on DB side about indexing and partitioning.
Quick response will be "no". It is not possible to give table name as parameter, because mybatis uses prepared statements.
I would suggest using table name as variable, and giving it to the statement string.
For example:
public interface UserAddressBookMapper {
static String tableName;
static void setTableName(String name) {
tableName = name;
}
#Insert({"insert into", tableName, "values(...)"})
int upsert(UserAddressBookMsg userAddressBookMsg);
You will have to set tableName before calling the method.

How set foreign key to null when entity keys are of type bigint (ulong)?

In my model I'm using bigint (ulong) as the type for entity keys. I want the database to enforce referential integrity, so I have set persistenceEnforce to true. Columns for foreign keys are nullable. With referential integrity an entity can only be deleted if no foreign key is referring to the entity, so before deleting an entity I must first set each foreign key for this associated entity to null. However, I don't know how to clear the foreign key.
Here is my model:
<cf:entity name="Order" cfom:bindingList="false">
<cf:property name="Id" typeName="ulong" key="true" persistenceIdentity="true" cfps:hint="CLUSTERED" />
<cf:property name="Invoice" typeName="Invoice" persistenceEnforce="true" />
</cf:entity>
<cf:entity name="Invoice" cfom:bindingList="false">
<cf:property name="Id" typeName="ulong" key="true" persistenceIdentity="true" cfps:hint="CLUSTERED" />
<cf:property name="Name" typeName="string" />
</cf:entity>
Here is my code:
Invoice invoice = new Invoice();
invoice.Save();
Order order = new Order();
order.Invoice = invoice;
order.Save();
// We must clear the reference to the invoice before deleting the invoice,
// because the database enforces referential integrity.
order.InvoiceId = 0;
order.Save();
invoice.Delete();
The above code throws the following exception when saving the order for the second time:
The UPDATE statement conflicted with the FOREIGN KEY constraint \"FK_Ord_Ore_Inv_Inv\".
This is because the code produced by CodeFluent inserts the value 0 instead of null into the "Order_Invoice_Id" column. The following line in the Order.BaseSave method seems to be wrong:
persistence.AddParameter("#Order_Invoice_Id", this.InvoiceId, ((ulong)(0ul)));
I tried the settings persistenceDefaultValue="null" and usePersistenceDefaultValue="true" on the Invoice propery, but that did not solve the problem.
Note: A property of type UInt64 (unsigned) is translated to a column of type bigint (signed). So be careful with the conversions... FYI CodeFluent Entities uses CodeFluent.Runtime.Utilities.ConvertUtilities to convert values.
The overload AddParameter(string name, ulong value, ulong defaultValue) doesn't use the default value, so it does not translate the default value to NULL. As a workaround, you can create a PersistenceHook that changes the value of the parameter to match the expected behavior:
public class CustomPersistenceHook : BasePersistenceHook
{
public override void AfterAddParameter(string name, object value, IDbDataParameter parameter)
{
if (value is ulong && name == "#Order_Invoice_Id")
{
var defaultValue = (ulong)ContextData["defaultValue"];
if (defaultValue == (ulong)value)
{
parameter.Value = DBNull.Value;
}
}
base.AfterAddParameter(name, value, parameter);
}
}
Then, you need to register the persistence hook:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="Sample" type="CodeFluent.Runtime.CodeFluentConfigurationSectionHandler, CodeFluent.Runtime" />
</configSections>
<Sample persistenceHookTypeName="Sample.CustomPersistenceHook, Sample" />
</configuration>