2.1 四种映射语句:一台完整的 CRUD 手术 本节摘要:select、insert、update、delete 四种映射语句覆盖全部数据操作。本节用一套完整的用户模块把四类语句写齐,讲清语句级通用属性(parameterType、resultType、flushCache、timeout 等),弄清各语句的返回值约定,最后用"订单状态更新"病例演示返回值的实际用法。 进入第 2 章,站在手术的入口端。四种映射语句就是四种基础术式,先把它们一次写全,后续小节再往深里打。 病例背景:用户模块的完整 CRUD 沿用第 1 章的 users 表,这次把增、查、改、删四类操作全部做完。接口先行: 注意返回类型的约定:查询返回对象或对象集合;增删改返回 int,值是受影响行数。
本节摘要:select、insert、update、delete 四种映射语句覆盖全部数据操作。本节用一套完整的用户模块把四类语句写齐,讲清语句级通用属性(parameterType、resultType、flushCache、timeout 等),弄清各语句的返回值约定,最后用"订单状态更新"病例演示返回值的实际用法。
进入第 2 章,站在手术的入口端。四种映射语句就是四种基础术式,先把它们一次写全,后续小节再往深里打。
沿用第 1 章的 users 表,这次把增、查、改、删四类操作全部做完。接口先行:
public interface UserMapper { User findById(int id); // 按 id 查单条 List<User> findAll(); // 查全部 int insert(User user); // 插入,返回受影响行数 int updateEmail(User user); // 更新,返回受影响行数 int deleteById(int id); // 删除,返回受影响行数 }
注意返回类型的约定:查询返回对象或对象集合;增删改返回 int,值是受影响行数。这个 int 不是主键、不是成功标志,是数据库报告的行数——后面马上会看到它的正确用法。
<mapper namespace="com.example.mapper.UserMapper"> <!-- 查询单条:resultType 指定结果装进哪个类 --> <select id="findById" parameterType="int" resultType="com.example.entity.User"> SELECT id, username, email FROM users WHERE id = #{id} </select> <!-- 查询全部:同一 resultType,返回 List 由接口签名决定 --> <select id="findAll" resultType="com.example.entity.User"> SELECT id, username, email FROM users ORDER BY id </select> <!-- 插入:属性值从参数对象的 getter 取 --> <insert id="insert" parameterType="com.example.entity.User"> INSERT INTO users (username, email) VALUES (#{username}, #{email}) </insert> <!-- 更新:只改邮箱,条件用主键 --> <update id="updateEmail" parameterType="com.example.entity.User"> UPDATE users SET email = #{email} WHERE id = #{id} </update> <!-- 删除:单参数直接用 --> <delete id="deleteById" parameterType="int"> DELETE FROM users WHERE id = #{id} </delete> </mapper>
调用与输出:
try (SqlSession session = factory.openSession()) { UserMapper mapper = session.getMapper(UserMapper.class); User u = mapper.findById(1); // 输出:User{id=1, username='zhangsan', email='zhangsan@example.com'} System.out.println(u); int inserted = mapper.insert(new User("lisi", "lisi@example.com")); session.commit(); // 写操作记得提交(1.4 节的教训) // 输出:1,表示一行被写入 System.out.println(inserted); List<User> all = mapper.findAll(); // 输出:2 System.out.println(all.size()); }
四种语句共享一批可选属性,都不填也有合理默认:
| 属性 | 作用 | 典型用法 |
|---|---|---|
| parameterType | 入参全限定名或别名 | 单参数可省略,框架自动推断 |
| resultType / resultMap | 结果去向(二选一) | 查询必填其一,第 3 章展开 |
| flushCache | 执行前是否清缓存 | 查询默认 false,写语句默认 true |
| timeout | 语句超时秒数 | 个别慢语句单独限时的精手段 |
| statementType | 语句处理器类型 | PREPARED 默认; Callable 用于存储过程 |
| useGeneratedKeys | 回填自增主键 | 2.3 节展开 |
| databaseId | 多数据库方言 | 同一 id 写多版语句,按当前库选择 |
两个默认值值得多看一眼:写语句默认 flushCache 为 true,意味着任何增删改都会清掉本命名空间的一二级缓存(5.1 节把它和脏读问题连起来讲);parameterType 在多数场景可以省略,因为框架从方法签名就能推断入参类型——写了也不参与匹配,只是文档作用。

背景:运营要求"只允许把待支付订单改成已支付",接口需要拒绝已经支付过的重复请求。
错误做法是先查再改(两次数据库往返,且两步之间存在并发缝隙)。正确做法是让条件更新自己说话:
public interface OrderMapper { int payIfWaiting(Long orderId); }
<update id="payIfWaiting" parameterType="long"> UPDATE orders SET status = 'PAID', paid_at = NOW() WHERE id = #{orderId} AND status = 'WAITING' </update>
int rows = mapper.payIfWaiting(orderId); if (rows == 1) { // 更新成功:订单确实从待支付变成了已支付 System.out.println("支付成功"); } else { // 行数为 0:没有待支付订单被更新(已支付、已取消或不存在) System.out.println("状态不允许支付,拒绝请求"); }
解读:WHERE 条件带上状态前置校验,行数直接回答"这条规则有没有被满足"。一次往返、无并发缝隙。这就是受影响行数的正确打开方式——把业务断言压进 WHERE,而不是先查后改。
变式:乐观锁同理。给表加 version 列,UPDATE 的 WHERE 带上 version 条件,行数为 0 即说明期间有并发修改,重试或报冲突。4.2 节的 set 标签会把这类"动态字段加版本号"的更新写成完整案例。
⚠️ 常见坑:把 insert 的返回值当主键用。返回的 1 是行数,主键回填要用 useGeneratedKeys 或 selectKey(下一节展开)。另一个坑是查不到数据:selectOne 返回 null 而不是抛异常,多行时才会抛 TooManyResultsException。
💡 关键直觉:增删改是"带着 WHERE 断言的动作",行数是断言结果的回执。想让接口回答"做没做成",先问 WHERE 够不够硬。
四种术式的骨架立住了,下一节钻进参数装配的核心辨析:井号占位与美元拼接,一条分界线两侧是完全不同的执行通道。