2.3 多参数、主键回填与批量插入


文档摘要

2.3 多参数、主键回填与批量插入 本节摘要:本节收拢参数入端三个高频进阶需求:多参数的两种组织方式(注解命名与参数对象)、插入后拿回自增主键的两种机制(useGeneratedKeys 与 selectKey)、批量写入的两条路线(foreach 拼值与 BATCH 执行器)及各自的上限。 上一节把命名规则讲清了,这一节把它们用到三个真实场景里。三个场景看似独立,其实都是"参数组织"这一个主题的变奏。 场景一:多参数怎么组织 后台筛选常常一攒就是五六个条件。两种组织方式对应两种演化阶段。 起步阶段:参数不多,逐个注解: 参数继续膨胀时,逐个注解的签名会失控。成熟阶段把散参收拢成查询对象: (WHERE 1=1 这种写法只是过渡形态,4.1 节的 where 标签会把它替换掉。

2.3 多参数、主键回填与批量插入

本节摘要:本节收拢参数入端三个高频进阶需求:多参数的两种组织方式(注解命名与参数对象)、插入后拿回自增主键的两种机制(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>

02-03-fig01-3

变式: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 混用没有收益,选定一条路线走到底。

💡 关键直觉:批量优化的本质是减少"网络往返次数"而不是减少"写入行数"。先数往返,再谈语句,优化方向就不会错。

本节要点回顾

  • 多参数三四个是分界:少于用注解、多于收对象;查询对象顺带安置分页排序字段
  • 自增主键回填:useGeneratedKeys 加 keyProperty,主键写回传入对象而非返回值
  • 外部号主键:selectKey 先取号后插入,order=BEFORE 是序列场景的标志
  • 批量两条路线:foreach 拼值一次往返但有语句长度上限;BATCH 执行器不改语句、分批稳当
  • 批量必修课:分批写入,量级以千为界;优化先数往返次数

参数的组织方式齐了,下一节把重复的 SQL 收进片段库——列清单与查询条件的复用术。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U