本篇文章对MyBatis操作MySQL时自增主键返回情况进行详细的实验,给出不同情况下Mybatis返回自增主键的不同行为,仅基于实验结果,不做源码分析。

1. 实验对比维度
(1)单纯的insert和insert on duplicate key update
这也是本文最大的特点,查询网上各种阐述Mybatis返回主键的文章,基本只关注insert时Mybatis返回主键的情况,对于插入或更新的sql语句insert on duplicate key update时mybatis返回主键(此时还细分为仅insert,仅update和insert和update混合三种情况)的文章则比较少。

(2)selectKey和useGeneratedKeys
Mybatis用这两个方法都能返回主键,前者一般用于单个插入,后者一般用于批量插入。本文还会给出两者更加细致的实验区别。

(3)@Param和parameterType
Mybatis传参时可以用@Param注解或者在xml中用parmaterType引用java对象,本文会实验两者传参对返回主键的影响。

(4)单个和批量
单个插入、批量插入、单个插入或更新和批量插入或更新时Mybatis返回主键情况各不相同,本文针对此进行实验分析。

(5)keyProperty写法
这个维度和@Param、parameterType紧密结合。不管采用selectKey还是useGeneratedKeys,返回主键时都需要用keyProperty指定主键对应的Java对象属性名,以便将主键设置到Java对象上(达到返回主键的目的)。而@Param和parameterType影响入参名字,也就会影响keyProperty对应的属性名写法,进一步会影响到返回主键的行为。

2. 基本概念介绍
对本文感兴趣的人一般已经熟悉MySQL和Mybatis,在工程中也有大量应用,但是为了帮助读者更顺畅的阅读,笔者还是介绍下本文涉及的相关基本概念。

(1)插入或更新SQL(简称InsertOrUpdate)
当我们往数据库插入记录时,如果数据库原先不存在该记录,那么就正常插入(此时就是insert);如果数据库原先存在该记录,那么就更新此记录(此时就是update),用一条SQL语句完成上述要求就是所谓的InsertOrUpdate。

MySQL判断记录是否存在的依据是主键或者唯一索引,insert在主键或者唯一索引已经存在的情况下会插入失败,而InsertOrUpdate在主键或者唯一索引已经存在的情况下就变成了根据主键或唯一索引update的操作。

有人会问原先有多个记录同时与插入的记录相同,会发生什么?答案是MySQL会更新第一个匹配的记录,其余的则不更新(网上看到的,笔者没有实验过),本质上这是由于单表中有多个唯一索引并存,所以有人会推荐一张表最好只建一个唯一索引。

MySQL实现InsertOrUpdate的语句有两种:一种就是本文要实验的Insert into values on duplicate key update语句。另一种是replace into values语句。insert on duplicate key update在发现记录已经存在时就地更新,或者说和update行为一致。replace into在发现记录已经存在时,先把原先的记录删除,然后再插入新的记录,相当于delete+insert操作。两者具体的细节可以参考下其他文章。

这里说明下为什么选取insert on duplicate key on而非replace into做实验:目前后端开发的数据表设计,比较流行用自增主键而不是自己选定字段做主键(这样做有诸多好处,可以参考MySQL的底层数据结构),并且也不依赖数据库提供的外键功能,而是在程序逻辑中保证数据一致性关系。这样一来replace into的功能就非常坑,它在记录已经存在时会改变主键,带来数据不一致的风险。实战中笔者建议除非特殊需求否则不要用replace into,即便当下没问题,将来扩展起来也容易埋坑。即便insert on duplicate key能真正做到Insert和Update一体,笔者基本也只用它来做批量更新用,不会使用其不存在插入、存在则更新的特性——宁愿在程序逻辑里先查一遍,区分需要insert和需要update的记录,该insert的insert,该update的update——因为MySQL的语言和Java等命令型语言不一样,它是有解释器的,开发者输入的SQL语句具体怎么执行不是由开发者决定的而是由MySQL决定的,所以功能越多的、语句越复杂的SQL执行的结果越不可控,也很难调试和维护。比如InsertOrUpdate在遇到多个相同的记录时只会更新第一个相同的记录就是不可控性的表现,非常坑,但是如果在应用程序中处理这些逻辑就很方便、很确定。

无论是insert on duplicate key update还是replace into,在插入时MySQL返回的影响行数就是插入的记录数,但是在更新时返回的影响行数时更新行数*2。所以笔者只会在批量更新时放心的使用insert on duplicate key update,这样根据返回值是否是参数大小的2倍就能判断是否只进行了更新操作。

笔者推崇开发中把数据库当作单纯的数据存储服务,减少其业务逻辑的负担,返璞归真。数据库语句应该尽量单表、单crud操作,不要动则几十上百行SQL,这样也符合当前微服务的趋势。笔者刚入行时经常听闻某某大神把几分钟的SQL优化到几秒,非常佩服,后来才发现本质上这是个比烂大赛,开发人员偷懒用重SQL语句弥补开发效率,一个联表查询就能从地球联到火星,留下了一大堆低效、无扩展性、难阅读、无从调试的代码,只能借助于SQL大神擦屁股,这种屁股擦起来其实对人也没啥长进(求职时看到职责描述有“能书写复杂SQL或者优化复杂SQL”的,亲们最好留个心眼...)。还是要做好系统架构、引导好团队的开发习惯、合理设计表和索引、细致负责的规划梳理业务,避免出现SQL大神才好。

题外话说的太多了,既然本文是Mybatis返回主键的实验,replace into这种会改变主键而且实战中也不建议使用的语句当然不予考虑。

(2)selectKey和useGeneratedKeys的异同
关于两者介绍的文章也很多。概括来说selectKey用于单个记录返回主键,useGeneratedKeys单记录多记录都能返回主键。所以单记录插入时实验会对比selectKey和useGeneratedKeys,而批量插入时只会采用useGeneratedKeys。

另外useGeneratedKeys=true需要数据库的支持,mysql可以使用,但是oracle不支持。selectKey实用的last_insert_id()的适应范围更广。

这里根据后面的实验先提前下个结论:在数据库兼容的情况下,返回主键的方式用useGeneratedKeys是最佳实践,selectKey在某些情况下(单记录)不会返回主键。

(3)@Param和parameterType的异同
基本异同也请参考其他文章。简单来说有个数据库映射实体Boy,其属性id映射数据库自增主键。如果dao中入参用@Param("entity")标注,那么在Mybatis的映射xml文件里,引用boy.id要写成#{entity.id}。如果在映射xml文件用parameterType="全路径名.Boy",那么引用boy.id只用写成#{id}。

@Param在替换dao的入参实体时可能免去修改映射文件工作,而且多参数情况下只能用@Param。parameterType会让映射文件中引用对象属性写法变得简单,但是只能适应单参数情况。根据后面的实验结果,这里提前给结论:在需要返回主键场景(插入、批量插入、插入或更新,批量插入或更新)里,parameterType比@Param适应性更好,@param在某些场景下不会返回主键。