我在 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映射器或映射器类型处理程序中的问题出在哪里? 你能修改一下来修复吗?
类型处理程序处理单个列,因此不适合您的使用。
我可以想到一些解决方案,但使用
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