Map 对象的 Spring Boot 示例中的 MyBatis 处理程序问题

问题描述 投票:0回答:1

我在 Spring Boot 示例中通过 MyBatis 实现 Map 的自定义处理程序时遇到问题。

这是我的请求对象,如下所示

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class QueryRequest implements Serializable {

    private List<String> years;
    private List<String> months;
    private List<String> region;
    private List<String> office;

}

这是如下所示的响应对象

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class TaskCountResponse implements Serializable {

    private String region;
    private String office;
    private Map<String, Integer> monthlyCounts;

}

这是下面所示的 dao

@Repository
public interface QueryTaskDao {

    List<TaskCountResponse> getTaskStatusCounts(QueryRequest queryRequest);
}

这是 Map 的自定义处理程序如下所示

@Slf4j
public class MapTypeHandler extends BaseTypeHandler<Map<String, Integer>> {

    private static final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, Map<String, Integer> parameter, JdbcType jdbcType) throws SQLException {
        try {
            String json = objectMapper.writeValueAsString(parameter);
            log.info("MapTypeHandler | setNonNullParameter | json : " + json);
            ps.setString(i, json);
        } catch (IOException e) {
            throw new SQLException("Error converting Map to JSON", e);
        }
    }

    @Override
    public Map<String, Integer> getNullableResult(ResultSet rs, String columnName) throws SQLException {
        try {
            String json = rs.getString(columnName);
            log.info("MapTypeHandler | getNullableResult(ResultSet rs, String columnName) | json : " + json);
            if (json != null) {
                return objectMapper.readValue(json, new TypeReference<Map<String, Integer>>() {});
            }
            return null;
        } catch (IOException e) {
            throw new SQLException("Error converting JSON to Map", e);
        }
    }

    @Override
    public Map<String, Integer> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        try {
            String json = rs.getString(columnIndex);
            log.info("MapTypeHandler | getNullableResult(ResultSet rs, int columnIndex) | json : " + json);
            if (json != null) {
                return objectMapper.readValue(json, new TypeReference<Map<String, Integer>>() {});
            }
            return null;
        } catch (IOException e) {
            throw new SQLException("Error converting JSON to Map", e);
        }
    }

    @Override
    public Map<String, Integer> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        try {
            String json = cs.getString(columnIndex);
            log.info("MapTypeHandler | getNullableResult(CallableStatement cs, int columnIndex) | json : " + json);
            if (json != null) {
                return objectMapper.readValue(json, new TypeReference<Map<String, Integer>>() {});
            }
            return null;
        } catch (IOException e) {
            throw new SQLException("Error converting JSON to Map", e);
        }
    }
}

这里是mybatis的XML部分

<?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="com.example.dao.QueryTaskDao">
    <cache/>

    <!-- Result Map to map query results to TaskStatusCountResponse -->
    <resultMap id="taskStatusCountResultMap" type="com.example.model.TaskStatusCountResponse">
        <result property="region" column="region_name"/>
        <result property="office" column="office_name"/>
        <!-- Handling dynamic columns for monthlyCounts -->
        <result property="monthlyCounts" column="monthlyCounts" javaType="java.util.Map" typeHandler="com.example.utils.MapTypeHandler"/>
    </resultMap>

    <!-- SQL fragment for dynamic filtering -->
    <sql id="queryTaskStatusCondition">
        <where>
            <!-- Filter by years -->
            <if test="years != null and !years.isEmpty()">
                AND SUBSTRING(tt.task_finish_time, 1, 4) IN
                <foreach collection="years" item="year" open="(" close=")" separator=",">
                    #{year}
                </foreach>
            </if>

            <!-- Filter by months -->
            <if test="months != null and !months.isEmpty()">
                AND SUBSTRING(tt.task_finish_time, 6, 2) IN
                <foreach collection="months" item="month" open="(" close=")" separator=",">
                    #{month}
                </foreach>
            </if>

            <!-- Filter by region -->
            <if test="region != null and !region.isEmpty()">
                AND tt.region_name IN
                <foreach collection="region" item="region" open="(" close=")" separator=",">
                    #{region}
                </foreach>
            </if>

            <!-- Filter by office -->
            <if test="office != null and !office.isEmpty()">
                AND tt.office_name IN
                <foreach collection="office" item="office" open="(" close=")" separator=",">
                    #{office}
                </foreach>
            </if>
        </where>
    </sql>

    <!-- Main SQL query to get Task Status Counts -->
    <select id="getTaskStatusCounts" resultMap="taskStatusCountResultMap" parameterType="com.example.model.QueryRequest">
        SELECT
        tt.region_name,
        tt.office_name,

        <!-- Dynamically create counts for each year-month combination -->
        <trim prefix="" suffix="" suffixOverrides=",">
            <foreach collection="years" item="year" separator=",">
                <foreach collection="months" item="month" separator=",">
                    COUNT(CASE WHEN SUBSTRING(tt.task_finish_time, 1, 7) = CONCAT(#{year}, '-', #{month}) THEN 1 END) AS month_${year}_${month}
                </foreach>
            </foreach>
        </trim>

        FROM
        task_list tt
        <include refid="queryTaskStatusCondition"/>
        GROUP BY
        tt.region_name,
        tt.office_name
        ORDER BY
        tt.region_name_en ASC;
    </select>
</mapper>

当我向

http://localhost:8048/statistic/getTaskCountsbyTime

发送请求时
{
    "years": ["2022", "2023"],
    "months": ["01", "02", "03"],
    "region": ["A Region", "B Region"],
    "office": ["A Region Office", "B Region Office"]
}

我收到这个回复

[
    {
        "region": "A Region",
        "office": "A Region Office",
        "monthlyCounts": null
    },
    {
        "region": "B Region",
        "office": "B Region Office",
        "monthlyCounts": null
    }
]

响应应如下所示(用实际计数代替空值):

[
    {
        "region": "A Region",
        "office": "A Region Office",
        "monthlyCounts": {
            "month_2022_01": 10,
            "month_2022_02": 15,
            "month_2022_03": 12,
            "month_2023_01": 5,
            "month_2023_02": 7,
            "month_2023_03": 6
        }
    },
    {
        "region": "B Region",
        "office": "B Region Office",
        "monthlyCounts": {
            "month_2022_01": 8,
            "month_2022_02": 14,
            "month_2022_03": 13,
            "month_2023_01": 4,
            "month_2023_02": 6,
            "month_2023_03": 9
        }
    }
]

我得到的monthlyCounts为空。xml映射器或映射器类型处理程序中的问题出在哪里? 你能修改一下来修复吗?

java sql spring-boot mybatis
1个回答
0
投票

类型处理程序处理单个列,因此不适合您的使用。
我可以想到一些解决方案,但使用

ResultHandler
的解决方案可能是最干净的。

映射器方法现在如下所示。

void getTaskStatusCounts(QueryRequest queryRequest,
    ResultHandler<Map<String, String>> resultHandler);

这是更简单的新查询。

<select id="getTaskStatusCounts" resultType="map">
  SELECT
    region_name,
    office_name,
    CONCAT(SUBSTRING(task_finish_time, 1, 4), '_', SUBSTRING(task_finish_time, 6, 2)) year_month
  FROM task_list
  <include refid="queryTaskStatusCondition" />
</select>

一般来说,您应该避免具有动态列计数的查询,因为它更难处理。

这是自定义结果处理程序。

import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.Map.Entry;

import org.apache.ibatis.session.ResultContext;
import org.apache.ibatis.session.ResultHandler;

public class TaskCountResponseResultHandler implements ResultHandler<Map<String, String>> {
  private final Map<Entry<String, String>, TaskCountResponse> map = new TreeMap<>(
      (e1, e2) -> {
        int x = e1.getKey().compareTo(e2.getKey());
        return x != 0 ? x : e1.getValue().compareTo(e2.getValue());
      });
  private final List<String> years;
  private final List<String> months;

  public TaskCountResponseResultHandler(List<String> years, List<String> months) {
    super();
    this.years = years;
    this.months = months;
  }

  @Override
  public void handleResult(ResultContext<? extends Map<String, String>> resultContext) {
    // This method is called for each row and
    // this map contains [column label] vs. [column value]
    Map<String, String> row = resultContext.getResultObject();

    Entry<String, String> regionOfficeKey = Map.entry(row.get("REGION_NAME"), row.get("OFFICE_NAME"));
    String monthKey = "month_" + row.get("YEAR_MONTH");

    map.computeIfAbsent(regionOfficeKey, k -> {
      TaskCountResponse v = new TaskCountResponse();
      v.setRegion(k.getKey());
      v.setOffice(k.getValue());
      v.setMonthlyCounts(new TreeMap<>());
      return v;
    }).getMonthlyCounts().merge(monthKey, 1, Integer::sum);
  }

  public List<TaskCountResponse> getResult() {
    return map.values().stream().map(response -> {
      // Puts zero for months with no data
      Map<String, Integer> counts = response.getMonthlyCounts();
      for (String year : years) {
        for (String month : months) {
          counts.putIfAbsent("month_" + year + "_" + month, 0);
        }
      }
      return response;
    }).toList();
  }
}

要执行查询并获取列表,您的代码基本上如下所示。

List<String> years = List.of("2022", "2023");
List<String> months = List.of("01", "02");

// Prepare parameter
QueryRequest param = new QueryRequest();
param.setYears(years);
param.setMonths(months);

// Create a result handler.
TaskCountResponseResultHandler resultHandler = new TaskCountResponseResultHandler(years, months);
// Execute query.
mapper. getTaskStatusCounts(param, resultHandler);
// Get the list from the result handler.
List<TaskCountResponse> result = resultHandler.getResult();

这是一个您可以尝试的便携式演示。
https://github.com/harawata/mybatis-issues/tree/master/so-79260900

© www.soinside.com 2019 - 2024. All rights reserved.