Skip to content

Commit ca9acb1

Browse files
committed
Merge fix/notification-batchinsert-typehandler2 into main: fix POST /admin/notifications500 due to MyBatis batchInsert missing JacksonTypeHandler for Map metadata2
2 parents d517e6c + 63c6138 commit ca9acb1

3 files changed

Lines changed: 348 additions & 47 deletions

File tree

backend-spring/src/main/java/com/ulticode/modules/notification/mapper/NotificationMapper.java

Lines changed: 61 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
44
import com.baomidou.mybatisplus.core.metadata.IPage;
5+
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
56
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
67
import com.ulticode.modules.notification.entity.Notification;
78
import org.apache.ibatis.annotations.Insert;
@@ -18,55 +19,68 @@
1819
@Mapper
1920
public interface NotificationMapper extends BaseMapper<Notification> {
2021

21-
@Select("SELECT COUNT(*) FROM notifications WHERE user_id = #{userId} AND is_read = 0")
22-
long countUnreadByUserId(@Param("userId") String userId);
22+
@Select("SELECT COUNT(*) FROM notifications WHERE user_id = #{userId} AND is_read =0")
23+
long countUnreadByUserId(@Param("userId") String userId);
2324

24-
@Update("UPDATE notifications SET is_read = 1, read_at = NOW() WHERE user_id = #{userId} AND is_read = 0")
25-
int markAllAsRead(@Param("userId") String userId);
25+
@Update("UPDATE notifications SET is_read =1, read_at = NOW() WHERE user_id = #{userId} AND is_read =0")
26+
int markAllAsRead(@Param("userId") String userId);
2627

27-
@Update("UPDATE notifications SET is_read = 1, read_at = NOW() WHERE id = #{id}")
28-
int markAsRead(@Param("id") String id);
28+
@Update("UPDATE notifications SET is_read =1, read_at = NOW() WHERE id = #{id}")
29+
int markAsRead(@Param("id") String id);
2930

30-
@Insert("<script>INSERT INTO notifications " +
31-
"(id, user_id, type, category, title, body, link, metadata, announcement_id, is_read, read_at, created_at, updated_at) VALUES " +
32-
"<foreach collection='list' item='item' separator=','>" +
33-
"(#{item.id}, #{item.userId}, #{item.type}, #{item.category}, #{item.title}, #{item.body}, #{item.link}, #{item.metadata}, #{item.announcementId}, #{item.isRead}, #{item.readAt}, #{item.createdAt}, #{item.updatedAt})" +
34-
"</foreach></script>")
35-
int batchInsert(@Param("list") List<Notification> list);
31+
/**
32+
* Batch insert notifications.
33+
*
34+
* <p>The {@code #{item.metadata}} parameter explicitly declares
35+
* {@link JacksonTypeHandler} because MyBatis does not inherit the
36+
* {@code @TableField(typeHandler=…)} metadata inside custom {@code @Insert}
37+
* SQL fragments. Without this, a {@code Map<String,Object>} field has no
38+
* JDBC type handler and MyBatis throws {@code IllegalStateException: Type
39+
* handler was null on parameter mapping for property '__frch_item_0.metadata'}.
40+
* Regression covered by {@code NotificationMapperBatchInsertTest}.
41+
*/
42+
@Insert("<script>INSERT INTO notifications "
43+
+ "(id, user_id, type, category, title, body, link, metadata, announcement_id, is_read, read_at, created_at, updated_at) VALUES "
44+
+ "<foreach collection='list' item='item' separator=','>"
45+
+ "(#{item.id}, #{item.userId}, #{item.type}, #{item.category}, #{item.title}, #{item.body}, #{item.link}, "
46+
+ "#{item.metadata, typeHandler=com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler}, "
47+
+ "#{item.announcementId}, #{item.isRead}, #{item.readAt}, #{item.createdAt}, #{item.updatedAt})"
48+
+ "</foreach></script>")
49+
int batchInsert(@Param("list") List<Notification> list);
3650

37-
/**
38-
* Paginated query for deduplicated system announcements by announcement_id.
39-
* Returns one representative notification per announcement group.
40-
*/
41-
@Select("<script>" +
42-
"SELECT n.* FROM notifications n " +
43-
"INNER JOIN (" +
44-
" SELECT announcement_id, MIN(id) AS representative_id " +
45-
" FROM notifications " +
46-
" WHERE category = #{category} " +
47-
" <if test='keyword != null and keyword != \"\"'> AND title LIKE CONCAT('%', #{keyword}, '%') </if>" +
48-
" <if test='type != null and type != \"\"'> AND type = #{type} </if>" +
49-
" <if test='announcementId != null and announcementId != \"\"'> AND announcement_id = #{announcementId} </if>" +
50-
" GROUP BY announcement_id" +
51-
") dedup ON n.id = dedup.representative_id " +
52-
"ORDER BY " +
53-
"<choose>" +
54-
" <when test='sortBy == \"title\"'>n.title</when>" +
55-
" <when test='sortBy == \"type\"'>n.type</when>" +
56-
" <when test='sortBy == \"category\"'>n.category</when>" +
57-
" <when test='sortBy == \"announcementId\"'>n.announcement_id</when>" +
58-
" <otherwise>n.created_at</otherwise>" +
59-
"</choose> " +
60-
"<choose>" +
61-
" <when test='sortOrder == \"asc\"'>ASC</when>" +
62-
" <otherwise>DESC</otherwise>" +
63-
"</choose>" +
64-
"</script>")
65-
IPage<Notification> selectDedupedAnnouncements(Page<Notification> page,
66-
@Param("category") String category,
67-
@Param("keyword") String keyword,
68-
@Param("type") String type,
69-
@Param("announcementId") String announcementId,
70-
@Param("sortBy") String sortBy,
71-
@Param("sortOrder") String sortOrder);
51+
/**
52+
* Paginated query for deduplicated system announcements by announcement_id.
53+
* Returns one representative notification per announcement group.
54+
*/
55+
@Select("<script>"
56+
+ "SELECT n.* FROM notifications n "
57+
+ "INNER JOIN ("
58+
+ " SELECT announcement_id, MIN(id) AS representative_id "
59+
+ " FROM notifications "
60+
+ " WHERE category = #{category} "
61+
+ " <if test='keyword != null and keyword != \"\"'> AND title LIKE CONCAT('%', #{keyword}, '%') </if>"
62+
+ " <if test='type != null and type != \"\"'> AND type = #{type} </if>"
63+
+ " <if test='announcementId != null and announcementId != \"\"'> AND announcement_id = #{announcementId} </if>"
64+
+ " GROUP BY announcement_id"
65+
+ ") dedup ON n.id = dedup.representative_id "
66+
+ "ORDER BY "
67+
+ "<choose>"
68+
+ " <when test='sortBy == \"title\"'>n.title</when>"
69+
+ " <when test='sortBy == \"type\"'>n.type</when>"
70+
+ " <when test='sortBy == \"category\"'>n.category</when>"
71+
+ " <when test='sortBy == \"announcementId\"'>n.announcement_id</when>"
72+
+ " <otherwise>n.created_at</otherwise>"
73+
+ "</choose> "
74+
+ "<choose>"
75+
+ " <when test='sortOrder == \"asc\"'>ASC</when>"
76+
+ " <otherwise>DESC</otherwise>"
77+
+ "</choose>"
78+
+ "</script>")
79+
IPage<Notification> selectDedupedAnnouncements(Page<Notification> page,
80+
@Param("category") String category,
81+
@Param("keyword") String keyword,
82+
@Param("type") String type,
83+
@Param("announcementId") String announcementId,
84+
@Param("sortBy") String sortBy,
85+
@Param("sortOrder") String sortOrder);
7286
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package com.ulticode.modules.notification.mapper;
2+
3+
import com.ulticode.modules.notification.entity.Notification;
4+
import org.apache.ibatis.annotations.Insert;
5+
import org.junit.jupiter.api.DisplayName;
6+
import org.junit.jupiter.api.Test;
7+
8+
import java.lang.reflect.Method;
9+
import java.util.List;
10+
11+
import static org.assertj.core.api.Assertions.assertThat;
12+
13+
/**
14+
* Compile-time + reflection regression guard for
15+
* {@link NotificationMapper#batchInsert(List)}.
16+
*
17+
* <p>Background: {@code Notification.metadata} is a {@code Map<String,Object>}
18+
* mapped to a MySQL {@code JSON} column via
19+
* {@code @TableField(typeHandler=JacksonTypeHandler.class)}. MyBatis does not
20+
* inherit that metadata inside custom {@code @Insert} SQL fragments, so the
21+
* {@code #{item.metadata}} parameter must explicitly declare a typeHandler
22+
* string. Without it, MyBatis throws
23+
* {@code IllegalStateException: Type handler was null on parameter mapping
24+
* for property '__frch_item_0.metadata'} at runtime when
25+
* {@code AdminNotificationServiceImpl.createSystemNotification} is called.
26+
*
27+
* <p>This test prevents the regression by asserting the {@code @Insert} SQL
28+
* body references {@code JacksonTypeHandler}. A full runtime IT
29+
* (Testcontainers + real MySQL JSON column) is the next level of coverage
30+
* and should be added alongside this guard.
31+
*/
32+
@DisplayName("NotificationMapper#batchInsert typeHandler guard")
33+
class NotificationMapperBatchInsertTest {
34+
35+
@Test
36+
@DisplayName("batchInsert declares JacksonTypeHandler for #{item.metadata}")
37+
void batchInsert_declaresJacksonTypeHandlerForMetadata() throws NoSuchMethodException {
38+
Method method = NotificationMapper.class.getDeclaredMethod("batchInsert", List.class);
39+
Insert insert = method.getAnnotation(Insert.class);
40+
41+
assertThat(insert)
42+
.as("@Insert annotation must be present on batchInsert")
43+
.isNotNull();
44+
45+
String[] value = insert.value();
46+
assertThat(value)
47+
.as("@Insert SQL must be present")
48+
.isNotEmpty();
49+
50+
String sql = String.join("\n", value);
51+
assertThat(sql)
52+
.as("batchInsert SQL must reference JacksonTypeHandler for the metadata column")
53+
.contains("JacksonTypeHandler");
54+
assertThat(sql)
55+
.as("batchInsert SQL must bind typeHandler on the metadata parameter")
56+
.containsPattern("metadata\\s*,\\s*typeHandler\\s*=\\s*[\\w.$]+JacksonTypeHandler");
57+
}
58+
59+
@Test
60+
@DisplayName("Notification entity retains @TableField JacksonTypeHandler on metadata")
61+
void entity_metadataStillHasJacksonTypeHandler() throws NoSuchFieldException {
62+
java.lang.reflect.Field field = Notification.class.getDeclaredField("metadata");
63+
com.baomidou.mybatisplus.annotation.TableField tableField =
64+
field.getAnnotation(com.baomidou.mybatisplus.annotation.TableField.class);
65+
66+
assertThat(tableField)
67+
.as("@TableField must remain on Notification.metadata to keep BaseMapper CRUD consistent")
68+
.isNotNull();
69+
assertThat(tableField.typeHandler())
70+
.as("@TableField(typeHandler=...) must remain JacksonTypeHandler.class")
71+
.isEqualTo(com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler.class);
72+
}
73+
}

0 commit comments

Comments
 (0)