问题
Working on a legacy solution that uses MyBatis and Oracle's stored procedures for bulk database updates. Current version of Mapper looks similar to this
@Mapper
public interface MyMapper {
void doUpdate(@Param("in") Map<String, List> in,
@Param("out") Map<String, List> out);
}
The idea is to provide a map of lists of the same length with fields values as "in" parameter to use those lists as an arguments to call a stored procedure like this
<select id="doUpdate"
statementType="CALLABLE">
<![CDATA[
{
CALL doUpdate(
#{in.field1, mode=IN, jdbcType=ARRAY, jdbcTypeName=MY_TYPE, typeHandler=NumberTypeHandler },
#{in.field2, mode=IN, jdbcType=ARRAY, jdbcTypeName=MY_TYPE, typeHandler=NumberTypeHandler},
#{in.field3, mode=IN, jdbcType=ARRAY, jdbcTypeName=MY_TYPE, typeHandler=NumberTypeHandler},
#{out.field1, mode=IN, jdbcType=ARRAY, jdbcTypeName=MY_TYPE, typeHandler=NumberTypeHandler })}]]>
</select>
And then iterate over these arrays in stored procedure to update entities one by one.
The issue is that I have to initialize all the Maps/Arrays and fill them manually before the call and also convert the results back to Java objects manually also. So right now it looks too complicated and verbose and I'm trying to find a more accurate solution.
So the question is: is there an easier way to provide the list of objects to stored procedure with MyBatis? I tried parameterMap but the actual parameter type should be List in my case and elements of that List should be custom Java objects so I did not managed to find a suitable solution using this approach.
回答1:
A procedure can take table type parameters and you can write a custom type handler that performs the conversion.
It may be easier to explain using concrete objects.
Instead of MY_TYPE
, I'll use S_USER_OBJ
...
create or replace type S_USER_OBJ as object (
id integer,
name varchar(20)
);
...a table...
create table users (
id integer,
name varchar(20)
);
...and a POJO.
public class User {
private Integer id;
private String name;
// setter/getter
}
Here is the new type which is a collection of S_USER_OBJ
.
create or replace type S_USER_OBJ_LIST as table of S_USER_OBJ;
The procedure can take the table type as parameters. e.g.
create or replace procedure doUpdate(
user_list in S_USER_OBJ_LIST,
user_out out S_USER_OBJ_LIST
) is
begin
-- process IN param
for i in user_list.first .. user_list.last loop
update users
set name = user_list(i).name)
where id = user_list(i).id;
end loop;
-- set OUT param
select * bulk collect into user_out
from (
select S_USER_OBJ(u.id, u.name) from users u
);
end;
Mapper would look as follows:
void doUpdate(
@Param("users") List<User> users,
@Param("outParam") Map<String, ?> outParam);
<update id="doUpdate" statementType="CALLABLE">
{call doUpdate(
#{users,typeHandler=pkg.UserListTypeHandler},
#{outParam.outUsers,jdbcType=ARRAY,jdbcTypeName=S_USER_OBJ_LIST,mode=OUT,typeHandler=pkg.UserListTypeHandler}
)}
</update>
UserListTypeHandler
is a custom type handler that converts List<User>
to/from an ARRAY
of STRUCT
.
import java.math.BigDecimal;
import java.sql.Array;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Struct;
import java.util.ArrayList;
import java.util.List;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import oracle.jdbc.driver.OracleConnection;
public class UserListTypeHandler extends
BaseTypeHandler<List<User>>{
@Override
public void setNonNullParameter(
PreparedStatement ps, int i, List<User> parameter,
JdbcType jdbcType) throws SQLException {
Connection conn = ps.getConnection();
List<Struct> structs = new ArrayList<Struct>();
for (int idx = 0; idx < parameter.size(); idx++) {
User user = parameter.get(idx);
Object[] result = { user.getId(), user.getName() };
structs.add(conn.createStruct("S_USER_OBJ", result));
}
Array array = ((OracleConnection) conn)
.createOracleArray("S_USER_OBJ_LIST",
structs.toArray());
ps.setArray(i, array);
array.free();
}
@Override
public List<User> getNullableResult(
CallableStatement cs,
int columnIndex) throws SQLException {
List<User> result = new ArrayList<>();
Array array = cs.getArray(columnIndex);
Object[] objs = (Object[]) array.getArray();
for (Object obj : objs) {
Object[] attrs = ((Struct) obj).getAttributes();
result.add(new User(
((BigDecimal) attrs[0]).intValue(),
(String) attrs[1]));
}
array.free();
return result;
}
...
}
The code using the method would look something like this.
Map<String, ?> outParam = new HashMap<>();
mapper.doUpdate(userList, outParam);
List<User> outUsers = outParam.get("outUsers");
For OUT
parameter, there also is another way using refcursor and result map.
In the mapper statement, specify the OUT parameter as follows.
#{outParam.outUsers,jdbcType=CURSOR,javaType=java.sql.ResultSet,mode=OUT,resultMap=userRM}
The result map is pretty straightforward.
<resultMap type="test.User" id="userRM">
<id property="id" column="id" />
<result property="name" column="name" />
</resultMap>
In the procedure, declare OUT parameter as SYS_REFCURSOR
create or replace procedure doUpdate(
user_list in S_USER_OBJ_LIST,
user_out out SYS_REFCURSOR
) is
begin
...
-- set OUT param
open user_out for select * from users;
end;
Here is an executable demo:
https://github.com/harawata/mybatis-issues/tree/master/so-56834806
来源:https://stackoverflow.com/questions/56834806/doing-bulk-updates-with-mybatis-and-oracle-stored-procedures