2.3 多参数、主键回填与批量插入 本节摘要:本节收拢参数入端三个高频进阶需求:多参数的两种组织方式(注解命名与参数对象)、插入后拿回自增主键的两种机制(useGeneratedKeys 与 selectKey)、批量写入的两条路线(foreach 拼值与 BATCH 执行器)及各自的上限。 上一节把命名规则讲清了,这一节把它们用到三个真实场景里。三个场景看似独立,其实都是"参数组织"这一个主题的变奏。 场景一:多参数怎么组织 后台筛选常常一攒就是五六个条件。两种组织方式对应两种演化阶段。 起步阶段:参数不多,逐个注解: 参数继续膨胀时,逐个注解的签名会失控。成熟阶段把散参收拢成查询对象: (WHERE 1=1 这种写法只是过渡形态,4.1 节的 where 标签会把它替换掉。
本节摘要:本节收拢参数入端三个高频进阶需求:多参数的两种组织方式(注解命名与参数对象)、插入后拿回自增主键的两种机制(useGeneratedKeys 与 selectKey)、批量写入的两条路线(foreach 拼值与 BATCH 执行器)及各自的上限。
上一节把命名规则讲清了,这一节把它们用到三个真实场景里。三个场景看似独立,其实都是"参数组织"这一个主题的变奏。
后台筛选常常一攒就是五六个条件。两种组织方式对应两种演化阶段。
起步阶段:参数不多,逐个注解:
List<User> search(@Param("keyword") String keyword, @Param("status") Integer status, @Param("minAge") Integer minAge, @Param("maxAge") Integer maxAge);
参数继续膨胀时,逐个注解的签名会失控。成熟阶段把散参收拢成查询对象:
// 参数对象:把筛选条件收进一个类,签名永远只有一个参数 public class UserQuery { private String keyword; private Integer status; private Integer minAge; private Integer maxAge; private int offset; // 分页偏移 private int limit; // 每页条数 // 省略 getter 与 setter } List<User> search(UserQuery query);
<select id="search" parameterType="com.example.query.UserQuery" resultType="User"> SELECT id, username, email FROM users WHERE 1 = 1 AND username LIKE CONCAT('%', #{keyword}, '%') AND status = #{status} AND age BETWEEN #{minAge} AND #{maxAge} LIMIT #{offset}, #{limit} </select>
(WHERE 1=1 这种写法只是过渡形态,4.1 节的 where 标签会把它替换掉。)
两种方式的选择线画在三四个参数:少于三个用注解,多于三个收对象。查询对象还有额外红利——分页字段、排序字段都有归宿,接口签名稳定不随需求膨胀。
背景:注册接口创建用户后要立刻给用户发欢迎邮件,邮件里要带用户主页链接——链接需要数据库生成的自增 id。问题是 INSERT 执行完,这个 id 在数据库手里,Java 侧的对象里还是 null。
方案一:useGeneratedKeys,适用于自增主键:
<!-- keyProperty 指明回填到参数对象的哪个属性 --> <insert id="insert" parameterType="User" useGeneratedKeys="true" keyProperty="id"> INSERT INTO users (username, email) VALUES (#{username}, #{email}) </insert>
User u = new User("wangwu", "wangwu@example.com"); mapper.insert(u); session.commit(); // 回填已发生:对象里能直接拿到数据库生成的主键 // 输出:51(取决于表当前自增值) System.out.println(u.getId());
注意回填发生在语句执行后、方法返回前,写进的是你传入的那个对象——不需要从返回值里取(返回值还是行数)。
方案二:selectKey,适用于序列、雪花号等数据库不自动回填的主键策略:
<insert id="insertWithSeq" parameterType="User"> <!-- order=BEFORE:先取号再插入;resultType 是取回值的类型 --> <selectKey keyProperty="id" resultType="long" order="BEFORE"> SELECT nextval('user_id_seq') <!-- PostgreSQL 序列取号 --> </selectKey> INSERT INTO users (id, username, email) VALUES (#{id}, #{username}, #{email}) </insert>

变式:MySQL 里也可以用 selectKey 先查最大 id 加一(order=AFTER),但并发下有取重风险,仅作了解;生产上自增表用方案一,序列数据库用方案二,分布式系统多在应用层发号(雪花算法),插入时 id 已经在对象里,两种机制都不需要。
背景:Excel 导入一次带来两千行用户数据,逐条 insert 意味着两千次数据库往返,导入耗时以分钟计。
路线一:foreach 拼一条多值 INSERT(细节在 4.3 节展开,此处看全貌):
<insert id="insertBatch"> INSERT INTO users (username, email) VALUES <foreach collection="list" item="u" separator=","> (#{u.username}, #{u.email}) </foreach> </insert>
mapper.insertBatch(users); // 2000 行拼成一条语句 session.commit();
一条 SQL、一次往返,速度最快。风险也在这条 SQL 上:行数太多时语句超长,可能撞数据库的包大小限制或参数个数上限。
路线二:BATCH 执行器,语句不变,执行方式变:
// BATCH 执行器:同一条语句反复入参,驱动层攒批提交 try (SqlSession session = factory.openSession(ExecutorType.BATCH, false)) { UserMapper mapper = session.getMapper(UserMapper.class); for (User u : users) { mapper.insert(u); // 还是单条 insert 语句 } session.commit(); // 提交时驱动把批次真正发出 }
结果对比(两千行数据、本机 MySQL 实测量级,不同环境数字会变,量级关系稳定):
| 路线 | 数据库往返 | 语句形态 | 主要风险 |
|---|---|---|---|
| 逐条插入 | 2000 次 | 单值 INSERT | 最慢,网络往返占大头 |
| foreach 拼值 | 1 次 | 超长多值 INSERT | 语句超长、参数上限 |
| BATCH 执行器 | 分批若干次 | 单值 INSERT 重复 | 忘 commit 批次不生效 |
解读:几百行以内、字段不多,foreach 最省事;上千行或字段宽,BATCH 更稳——它不改变 SQL 文本,只是把"同一条预编译语句反复设值"攒成批发。两条路线的共同前提是必须分批:按五百或一千行切一刀,别让一次导入撑爆任何一层。
⚠️ 常见坑:BATCH 会话里调用 insert 不返回真实行数(返回的是批量占位值),别拿它做业务判断;另外 BATCH 与手写 foreach 混用没有收益,选定一条路线走到底。
💡 关键直觉:批量优化的本质是减少"网络往返次数"而不是减少"写入行数"。先数往返,再谈语句,优化方向就不会错。
参数的组织方式齐了,下一节把重复的 SQL 收进片段库——列清单与查询条件的复用术。