MyBatis 查询怎么返回 Map?@MapKey、List<Map> 和插入返回主键怎么用?
简化版
MyBatis 除了返回实体对象/List,还能返回 Map 形式的结果,常见三种:① 单行返回 Map<String, Object>——查一行、但不想定义实体类时,返回 Map(key 是列名、value 是列值),适合动态列、临时查询;② 多行返回 List<Map<String,Object>>——每行一个 Map,适合结果结构不固定的多行查询;③ 多行返回 Map<K, V>(用 @MapKey)——把「List 结果」转成「以某个字段为 key 的 Map」,比如查一批用户返回 Map<Long, User>(key 是 userId),方便按 id 快速查找,用 @MapKey("id") 指定用哪个字段做 key。另外「插入后拿自增主键」也很常用:用 @Options(useGeneratedKeys=true, keyProperty="id")(或 XML 的 useGeneratedKeys),插入后 MyBatis 会把数据库生成的自增主键回填到实体的 id 字段,不用再查一次。核心:Map 返回适合「不想定义实体/列不固定」的场景,@MapKey 方便按字段建索引,useGeneratedKeys 拿回自增主键。
详细版
几种 Map 返回方式:
| 返回类型 | 说明 | 适合 |
|---|---|---|
Map<String,Object> | 单行 → 列名:列值 | 单行、不想定义实体 |
List<Map<String,Object>> | 多行,每行一个 Map | 多行、列不固定 |
@MapKey("id") Map<Long,User> | 多行 → 以字段为 key 的 Map | 需要按字段快速查找 |
// ① 单行返回 Map(key=列名)
@Select("SELECT id, name, age FROM user WHERE id=#{id}")
Map<String, Object> selectAsMap(Long id); // {id=1, name=Tom, age=18}
// ② 多行返回 List<Map>
@Select("SELECT * FROM user")
List<Map<String, Object>> selectAllAsMapList();
// ③ 多行返回 Map<key, entity>,@MapKey 指定 key 字段
@MapKey("id")
@Select("SELECT * FROM user")
Map<Long, User> selectAsUserMap(); // {1=User(1,...), 2=User(2,...)}
// → 之后 map.get(1L) 直接拿到 id=1 的用户
// ④ 插入回填自增主键
@Insert("INSERT INTO user(name, age) VALUES(#{name}, #{age})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(User user);
// 调用后:user.getId() 就是数据库生成的自增主键
⚠️ 返回 Map 虽然方便,但要节制使用——它牺牲了「类型安全」和「可读性」:
Map<String, Object>拿值要map.get("name")(字符串 key 容易写错列名,且 value 是Object要强转),IDE 无法补全和检查,重构时也不会自动改。所以能定义实体就定义实体(resultType="User"),返回 Map 主要用于:① 临时/一次性查询,不值得建实体;② 列不固定的动态查询(如报表、动态字段);③ 只查一两个聚合值。而@MapKey的价值场景是「查出一批数据后要频繁按某字段查找」——比如查出 1000 个用户,后面要反复用 userId 查对应用户,用Map<Long,User>就是 O(1) 查找,比在List里遍历找(O(n))高效;本质是「查询时顺便建了个索引」。
完整版教学
一、单行返回 Map
先看最简单的「单行返回 Map」:
返回类型写成 Map<String, Object>:
@Select("SELECT id, name, age FROM user WHERE id=#{id}")
Map<String, Object> selectAsMap(Long id);
结果:一个 Map,key 是列名、value 是列值
{id=1, name="Tom", age=18}
什么时候用:
① 不想为这个查询定义一个实体类(临时、一次性)
② 查询的列不固定(动态 SQL 查出来的列可能变)
③ 只是拿几个值,建实体不划算
取值:
map.get("name") → 拿 name 列的值(是 Object,可能要强转)
缺点:
- 字符串 key 容易写错列名("nane" 不报错,返回 null)
- value 是 Object,用时要强转,没有类型安全
- IDE 不能补全/检查
单行返回 Map<String, Object>——key 是列名、value 是列值。适合「不想定义实体(临时/一次性)、列不固定、只拿几个值」。缺点是字符串 key 易写错、value 是 Object 要强转、无类型安全、IDE 不能补全。理解「单行返回 Map(key=列名 value=列值)、适合不想定义实体/列不固定、缺点是字符串 key 易错和无类型安全」,就掌握了单行返回 Map。
二、多行返回 List
多行查询也能返回 Map,每行一个:
返回类型 List<Map<String, Object>>:
@Select("SELECT * FROM user")
List<Map<String, Object>> selectAllAsMapList();
结果:List 里每个元素是一行的 Map
[{id=1, name=Tom}, {id=2, name=Jerry}, ...]
什么时候用:
① 多行、且列结构不固定(如动态报表:SELECT 的列是动态拼的)
② 通用查询工具(不知道具体查什么表、什么列)
③ 导出、展示原始数据
对比返回 List<实体>:
List<User>:类型安全、有实体、推荐(列固定时)
List<Map>:灵活、无需实体,但无类型安全(列不固定时用)
所以:列固定用 List<实体>,列不固定用 List<Map>
多行返回 List<Map<String,Object>>——每行一个 Map。适合「多行且列结构不固定」(动态报表、通用查询工具、导出原始数据)。对比 List<实体>:列固定用 List<实体>(类型安全、推荐),列不固定用 List<Map>(灵活但无类型安全)。理解「多行返回 List
三、@MapKey:把 List 转成索引 Map
@MapKey 是更有价值的用法——把结果转成「以某字段为 key 的 Map」:
需求:查出一批用户后,要频繁按 userId 查找对应用户
普通做法:返回 List<User>,然后在 List 里遍历找(O(n),慢)
@MapKey 做法:返回 Map<key, User>,以某字段为 key
@MapKey("id") ← 指定用 User 的 id 字段做 Map 的 key
@Select("SELECT * FROM user")
Map<Long, User> selectAsUserMap();
结果:{1=User(1,Tom), 2=User(2,Jerry), ...}
key 是每个 User 的 id,value 是 User 对象
之后:map.get(1L) → O(1) 拿到 id=1 的用户(快)
本质:查询时"顺便建了个索引"
把 List 结构变成"字段→对象"的 Map,方便按字段快速查找
适合:查出数据后要反复按某字段(通常是主键/唯一键)查找
比在 List 里遍历找高效(O(1) vs O(n))
注意:@MapKey 指定的字段应该是唯一的(否则重复 key 会覆盖)
@MapKey 是更有价值的用法——把查询结果转成「以某字段为 key 的 Map」(@MapKey("id") 指定用 id 做 key,返回 Map<Long, User>)。本质是「查询时顺便建了个索引」——之后 map.get(id) 是 O(1) 查找,比在 List 里遍历找(O(n))高效。适合「查出数据后要反复按某字段(主键/唯一键)查找」。注意 @MapKey 指定的字段应唯一(否则重复 key 覆盖)。理解「@MapKey 把 List 结果转成以字段为 key 的 Map(相当于建索引)、map.get(id)O(1)查找比 List 遍历 O(n)快、字段应唯一」,就掌握了 @MapKey 的价值。
四、插入回填自增主键
另一个高频需求「插入后拿自增主键」,用 useGeneratedKeys:
需求:插入一条记录后,要拿到数据库生成的自增主键
(比如插入订单后,要用订单 id 做后续操作)
普通问题:insert 返回的是"影响行数",不是主键
再查一次拿 id?多余、还可能不准(并发)
useGeneratedKeys 方案:
@Insert("INSERT INTO user(name) VALUES(#{name})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(User user);
含义:
useGeneratedKeys=true:使用数据库生成的主键
keyProperty="id":把生成的主键回填到实体的 id 属性
→ 插入后,user.getId() 就是数据库生成的自增主键(自动回填)
XML 写法:
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user(name) VALUES(#{name})
</insert>
原理:MyBatis 通过 JDBC 的 getGeneratedKeys() 拿回生成的主键
再 set 到实体的 keyProperty 字段
不支持自增的数据库(如 Oracle 序列):
用 <selectKey> 指定获取主键的 SQL(如 SELECT seq.nextval)
插入回填自增主键用 @Options(useGeneratedKeys=true, keyProperty="id")(或 XML 的 useGeneratedKeys="true" keyProperty="id")——插入后 MyBatis 把数据库生成的自增主键回填到实体的 id 字段(user.getId() 就是主键),不用再查一次。原理是通过 JDBC 的 getGeneratedKeys() 拿回主键再 set 到实体。不支持自增的数据库(Oracle 序列)用 <selectKey> 指定获取主键的 SQL。理解「useGeneratedKeys+keyProperty 插入后回填自增主键到实体(不用再查)、原理是 JDBC getGeneratedKeys、Oracle 序列用 selectKey」,就掌握了返回主键。
五、Map 返回的取舍
理解「什么时候该用 Map、什么时候该用实体」:
返回 Map 的优点:
✓ 灵活——不用定义实体类
✓ 适应列不固定的动态查询
返回 Map 的缺点:
✗ 无类型安全——value 是 Object,要强转
✗ 字符串 key 易写错列名(写错返回 null,静默)
✗ IDE 不能补全/检查/重构
✗ 可读性差——不知道这个 Map 里有啥
取舍原则:
能定义实体就定义实体(resultType="User"):
- 类型安全、IDE 友好、可读、可维护
- 列固定的业务查询都应该用实体
用 Map 的合理场景(少数):
- 临时/一次性查询,不值得建实体
- 列不固定的动态查询(报表、动态字段)
- 通用查询工具、数据导出
- 只查一两个聚合值(COUNT、SUM)
所以:Map 是"灵活但不安全"的选择,节制使用
业务代码优先实体,Map 用于特殊场景
Map 返回的取舍:优点是灵活(不用定义实体、适应列不固定),缺点是无类型安全(value 是 Object 要强转)、字符串 key 易写错、IDE 不友好、可读性差。原则「能定义实体就定义实体」(类型安全、IDE 友好、可维护,业务查询都应用实体);Map 只用于少数场景(临时查询、列不固定的动态查询/报表、通用工具、只查聚合值)。理解「Map 灵活但不安全(无类型安全/字符串 key 易错/IDE 不友好)、能定义实体就定义、Map 用于临时/列不固定/聚合值等特殊场景」,就掌握了 Map 返回的取舍。
六、实践小结
把这几个用法的适用场景整理清楚:
返回类型速查:
单个实体 → resultType="User"(列固定,最常用)
多个实体 → List<User>(列固定)
单行 Map → Map<String,Object>(不想建实体/列不固定)
多行 List<Map> → List<Map<...>>(多行列不固定,如报表)
索引 Map → @MapKey("id") Map<Long,User>(要按字段快速查找)
插入拿主键 → useGeneratedKeys + keyProperty(回填实体 id)
选择逻辑:
① 列固定的业务查询 → 用实体(类型安全,首选)
② 列不固定/临时 → 用 Map
③ 要按字段快速查找一批数据 → @MapKey 建索引 Map
④ 插入后要用生成的主键 → useGeneratedKeys 回填
记忆:实体优先,Map 用于灵活场景,@MapKey 用于查找,
useGeneratedKeys 用于拿主键
实践小结:列固定的业务查询用实体(首选、类型安全);列不固定/临时用 Map;要按字段快速查找一批数据用 @MapKey 建索引 Map;插入后要用主键用 useGeneratedKeys 回填。核心记忆「实体优先,Map 用于灵活场景,@MapKey 用于查找,useGeneratedKeys 用于拿主键」。理解「返回类型选择:列固定用实体/列不固定用 Map/按字段查找用 @MapKey/插入拿主键用 useGeneratedKeys」,就掌握了这几个用法的整体选择。
记忆钩子:「MyBatis 返回 Map 三种:①单行 Map<String,Object>(key=列名 value=列值,适合不想建实体/列不固定)②多行 List
(每行一个 Map,适合动态报表)③@MapKey(‘id’) Map<Long,User>(把 List 转成以字段为 key 的 Map=建索引,map.get(id)O(1)查找比 List 遍历快,字段要唯一);插入拿自增主键用 @Options(useGeneratedKeys=true,keyProperty=‘id’)回填实体 id(原理 JDBC getGeneratedKeys,Oracle 序列用 。);★Map 灵活但无类型安全(value 是 Object/字符串 key 易错/IDE 不友好)→能定义实体就定义,Map 用于临时/列不固定/聚合值」
七、常见误区与追问
- 误区:查询只能返回实体或 List<实体>。 还能返回 Map<String,Object>(单行)、List
(多行)、@MapKey 指定的 Map<K,实体>(索引 Map);适合不想定义实体、列不固定、或要按字段快速查找的场景。 - 误区:返回 Map 比返回实体好(更灵活)。 Map 牺牲了类型安全和可读性——value 是 Object 要强转、字符串 key 易写错列名(写错静默返回 null)、IDE 不能补全/检查/重构;能定义实体就定义实体,Map 只用于临时、列不固定的特殊场景。
- 误区:@MapKey 和普通返回 Map 一样。 不同——普通 Map<String,Object> 是「单行的列名→列值」;@MapKey 是把「多行 List 结果」转成「以某字段为 key 的 Map」(如 Map<Long,User>),相当于建索引,方便按字段 O(1) 快速查找。
- 误区:插入后要再查一次才能拿到自增主键。 不用——用 useGeneratedKeys=true + keyProperty=“id”,MyBatis 插入后通过 JDBC 的 getGeneratedKeys() 把生成的主键自动回填到实体的 id 字段,直接 user.getId() 就能拿到,不用再查。
- 追问:@MapKey 有什么实用价值? 把查出的一批数据转成「字段→对象」的 Map(相当于建索引),后续要反复按某字段(通常是主键/唯一键)查找时是 O(1),比在 List 里遍历找(O(n))高效;适合「查出一批数据后频繁按某字段查找」的场景;注意 key 字段要唯一,否则重复 key 会覆盖。
- 追问:Oracle 这种用序列的数据库怎么拿插入主键? Oracle 不支持自增(用序列),不能用 useGeneratedKeys;要用
标签指定获取主键的 SQL(如 SELECT seq.nextval FROM dual),设 order=“BEFORE”(插入前取序列值)或 “AFTER”,把值设到 keyProperty 指定的字段。 - 追问:什么时候该用 Map 返回、什么时候用实体? 列固定的业务查询用实体(resultType=“User”,类型安全、IDE 友好、可维护,首选);列不固定的动态查询(报表、动态字段)、临时一次性查询、只查一两个聚合值时用 Map(灵活但无类型安全);原则是「能定义实体就定义实体,Map 节制使用」。
八、加强记忆
MyBatis 除了返回实体,还能返回 Map,常见三种:① 单行 Map<String,Object>(key=列名、value=列值,适合不想定义实体/列不固定);② 多行 List<Map<String,Object>>(每行一个 Map,适合动态报表/列不固定);③ @MapKey("id") Map<Long,User>(把「多行 List 结果」转成「以某字段为 key 的 Map」——相当于建索引,map.get(id) 是 O(1) 查找,比 List 遍历 O(n) 快,适合查出一批数据后频繁按字段查找,key 字段应唯一否则覆盖)。插入回填自增主键用 @Options(useGeneratedKeys=true, keyProperty="id")(或 XML 同名属性)——插入后 MyBatis 通过 JDBC 的 getGeneratedKeys() 把主键回填到实体 id 字段(不用再查),Oracle 序列用 <selectKey>。Map 返回灵活但牺牲类型安全(value 是 Object 要强转、字符串 key 易写错、IDE 不友好)——能定义实体就定义实体(业务查询首选),Map 只用于临时/列不固定/聚合值等特殊场景。一句话「MyBatis 返回 Map:单行 Map<String,Object>(列名→列值)/多行 List