Doing bulk updates with MyBatis and Oracle stored procedures

左心房为你撑大大i 提交于 2020-06-17 14:21:04

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!