大家好,我是伟伟。
本期给大家分享一下读者给我分享的一个关于MyBatis的“编程技巧”。说实话,这个炫酷的操作让我愣了一下。
我更喜欢称之为:埋雷骗你不商量的方法。
为了让大家顺利进入乐趣,我先给大家演示一下。
因为我们需要使用MyBatis,所以我们首先创建两个表。
表称为产品表。表格结构非常简单:
还有一个表叫order_info表,表结构也很简单:
当你看到这两张桌子出现的时候,你就知道我的场景是什么了,肯定是卖货的。
库存减一,订单加一。
大家都再熟悉不过的场景了。
您可以在几分钟内编写如下伪代码:
public void saleProduct(){ //更新库存,库存减一 ProductMapper.updateProductCount(); //保存订单信息 OrderInfoMapper.saveOr德信息( );}
当然,你一眼就能看出这段伪代码的问题:减少库存和节省订单应该是一个事务操作,所以这两个动作应该包装在一个事务中。
所以我们的伪代码变成这样:
public void saleProduct() { //开启交易 begin; //更新库存,库存减一 Boolean updateSuccess = ProductMapper.updateProductCount() ; //保存订单信息 orderInfoMapper.saveOrderInfo(); //提交交易 其他 { 回滚 当时读者给我举个例子的时候,那是完全不同的场景,与卖货无关。 读者给出的例子大概是几个表之间存在关系。如果一张表中的某条数据被删除,其他几张表中对应的数据也会被删除,并且一张表需要更新其状态。 为了更好地展示这种“编程技巧”,我把场景简化为上面提到的卖货的方式。 我上面提到的是伪代码。 现在我将向您展示使用“编程技巧”编写的真实代码。 第一个是控制器接口:@GetMapping("/sale")public void sale() { productMapper.selaProduct(); } 然后就是这个productMapper的selaProduct接口: 是的,你没有看错,这是一个MyBatis的mapper接口,然后直接进入mapper.xml文件: 我什至不会问你这个写作方法或这个小技巧是否性感。我只想问你,你以前见过吗? 可以使用吗? 伟师傅还太年轻,经验不够。我以前从未见过在mapper.xml 中这样写SQL。 别说我以前见过,在我小小的头脑里,我从来没想过要这样写。所以我看到这篇文章的第一反应是:这可能吗?这不行吗? 于是,秉着大胆假设、仔细验证的态度,我写了上面的Demo。 项目启动后,发起调用,控制台直接报错: 当我看到这个错误报告时,我下意识地觉得MyBatis不支持这种写法,直接报错。这也符合我之前的认知。 不过,在读者的指导下,他提醒我将这个配置添加到数据库连接配置中: allowMultiQueries=true 我的demo启动时,确实没有添加这个配置。但当我看到这个配置的那一刻,我就开始觉得有点意思了。 因为我知道这个配置是用来做什么的。 看名字就知道意思:allow Multi Queries,允许多个查询。 最常用的场景是使用foreach标签进行批量插入或更新时使用此配置。 有了这个参数的支持,mapper.xml中写的sql很可能能够正常执行。 因为添加这个配置后,一个数据库连接中可以执行多条sql语句,但是对于MyBatis或MySQL驱动来说,它并不区分“多条sql”是insert语句还是update语句,还是混合体的句子。 我还去MySQL官网查了一下这个配置的含义: https://www.sychzs.cn/doc/connector-j/8.1/en/connector-j-connp-props-security.html#cj-conn-prop_allowMultiQueries 这个参数,官网上只有一句话: 允许使用“;”在一个语句中分隔多个查询。此选项不会影响“addBatch()”和“executeBatch()”方法,它们依赖于“rewriteBatchStatements”。 允许使用“;”在一个语句中分隔多个查询。此选项不会影响“addBatch()”和“executeBatch()”方法,因为它们依赖于“rewriteBatchStatements”。 在引入allowMultiQueries的时候,还提到了一个rewriteBatchStatements参数。 至于这个参数的作用,这里就不多说了。我只能说这两件事是一套组合拳,里面的文章也很多。如果你不知道,我建议你去了解一下。 就当做课后练习吧。 我们先来说主线吧。 当我将配置allowMultiQueries=true添加到数据库连接时,我重新启动了服务。 再次发起呼叫。 为了表达我的震惊,我给你们做了一个动画: 库存减少一,订单增加一,方法执行成功。 确实有效。你做到了,真是令人大开眼界。 这波知识增加了,是一条从未想象过的路。 我的 不要这样写! 听韦大师的劝告,千万不要这样写! 首先,这种写法不符合大多数程序员的理解。 谁能想到最终的mapper.xml不只是简单的sql,里面还埋藏着一堆业务逻辑呢? 关键是这样写也有误导性。 举个简单的例子,这种写法没有考虑库存是否充足: 比如当前库存已经没有了,按照这种写法,仍然会向order_info表中插入一条数据。 超卖了,我的朋友。 只考虑commit,不考虑rollback。 而且这样写根本无法考虑超卖的情况,因为你无法得知减库存操作是否成功,从而无法判断是否需要commit或者rollback。 什么,你问我能不能写一个存储过程来判断? 是的,MyBatis 确实可以调用存储过程。 首先,存储过程还是要写在MySQL中,MyBatis只是发起调用。 其次,赶紧放弃你越走越远的顽皮想法,老老实实写Java代码来解决这个问题。是不是很好吃? 什么,你又问我,如果不需要判断前面的sql是否执行成功怎么办? 比如我前面提到的读者举的例子,几个表之间是有关系的。如果一个表中的某条数据被删除,那么其他表中相应的数据也必须被删除,而且还有一张表。需要更新状态。 大概是这样的: 开始;从表1中删除where user_id=xxx;从表2中删除whereuser_id=xxx; 从表3中删除其中 user_id=xxx;更新表4 设置 user_status=1 其中 user_id=xxx;提交; 与卖货的场景不同的是,在这个场景中,如果每条sql执行成功,就说明业务执行成功。 看起来没有什么问题。 但是我问你一个问题:这组SQL一定会被commit吗? 你想好了吗? 绝对不一定是这样。执行担保期间出现问题。 举个最简单的例子,表格写错了: 在这种情况下,再次发起呼叫: 程序报告错误,指出找不到表。 那么请问:此时订单表中是否应该有数据插入呢? 发生异常。绝对不应该插入任何数据。我检查了数据库,确实没有插入新数据。 看起来确实不错。 那我再问一下:这样写的话,当前事务是回滚了还是提交了? 。 。 。 。 。 。 。 。 。 正确答案暂停。 通过执行以下SQL,我们可以得到当前的事务列表: 从INFORMATION_SCHEMA.INNODB_TRX中选择*; 从查询结果中我们可以发现,我们的程序抛出异常后,当前事务仍然处于RUNNING状态: 而且,此事务将保持 RUNNING 状态,即挂起状态,直到服务重新启动。 但是仅仅从程序的角度来看,抛出了异常,没有数据,这也是符合预期的,没有什么问题。 是我的。 所以,听韦大师的建议,千万不要这样写! 老老实实写出大家都能理解的Java代码,不要乱搞mapper.xml。 延长 其实我觉得以上都是没什么用的知识点,因为人们一般不会这样写。 不过既然都写到这里了,场景也在那里,我也给大家拓展一些稍微有用的知识。 还是在卖货的背景下。 订单中加一,库存中减一。这就是它的工作原理。 开始;插入 order_info(`buy_name`, `buy_goods`) VALUES ('歪大师', 'ipad pro 顶配版'); 更新产品设置product_count=product_count-1其中id=1且product_count>0;提交; 库存减一,订单增加一: 开始;更新产品设置product_count=product_count-1其中id=1且product_count>0;插入order_info(`购买名称`, `购买商品`) VALUES ('歪大师', 'ipad pro 顶配版');提交; 都包含在交易中。为了简化代码,我们假设库存非常充足,不考虑回滚场景。 “订单加一、库存减一”的性能好还是“库存减一、订单加一”的性能好,还是两者没有区别? 首先从执行结果来看,两者确实没有什么区别,都能够保证业务场景的正确性。 但考虑性能的话,执行“订单加一,库存减一”肯定是更好的。 如果你还没弄清楚,我给你一个简单的提示:只要业务正确,锁定的代码与解锁的代码越接近,性能就越好? 如果你还没弄清楚,我再给你一个提示:当库存减一时,会被锁定吗?你并不关心它是否添加了表锁、间隙锁或记录锁。我就问你加不加锁? 如果你还没有反应过来,说明你对MySQL的锁机制的掌握有点薄弱,可以加强一下。 我直接公布答案: 更新产品集product_count=product_count-1,其中id=1且product_count>0; 因为where条件是id=1,加锁的是唯一索引,并且该记录存在于表中,所以只会锁定id=1的记录。 对于商品id=1,如果是热门商品,我们采用“订单加一,库存减一”的写法,性能会更高。 因为锁定频率相同时,解锁越快,性能越高。 看最后一张图你就明白了: 只要改一条SQL,性能就上去了。我问你,你觉得舒服吗? 最后说点不相关的: 我在文章开头给出了这样一张图: 你不觉得尴尬吗? sela是什么鬼? 显然,这个地方是一个简单的拼写错误,你要输入的单词是sale: 请问,当你在程序中看到这样的拼写时,你会怎么做? 如果是我的话,我会主动把selaProduct改成saleProduct,其他的就不会动了。 这是我在上一篇文章《童子军规则》中提到的编码规则: 纠正拼写错误的方法名或变量名也是代码中非常重要的事情。 这不是代码痴迷,这是基本的职业道德。 因为你不想让下一个接手你代码的人看到一堆“succeess、createTiem、lastUpdataBy、businessDate、producectName”之类的变量名,而血压上升,非常生气并损害他们的健康。
当时读者给我举个例子的时候,那是完全不同的场景,与卖货无关。
读者给出的例子大概是几个表之间存在关系。如果一张表中的某条数据被删除,其他几张表中对应的数据也会被删除,并且一张表需要更新其状态。
为了更好地展示这种“编程技巧”,我把场景简化为上面提到的卖货的方式。
我上面提到的是伪代码。
现在我将向您展示使用“编程技巧”编写的真实代码。
第一个是控制器接口:
@GetMapping("/sale")public void sale() { productMapper.selaProduct(); }
然后就是这个productMapper的selaProduct接口:
是的,你没有看错,这是一个MyBatis的mapper接口,然后直接进入mapper.xml文件:
我什至不会问你这个写作方法或这个小技巧是否性感。我只想问你,你以前见过吗?
伟师傅还太年轻,经验不够。我以前从未见过在mapper.xml 中这样写SQL。
别说我以前见过,在我小小的头脑里,我从来没想过要这样写。所以我看到这篇文章的第一反应是:这可能吗?这不行吗?
于是,秉着大胆假设、仔细验证的态度,我写了上面的Demo。
项目启动后,发起调用,控制台直接报错:
当我看到这个错误报告时,我下意识地觉得MyBatis不支持这种写法,直接报错。这也符合我之前的认知。
不过,在读者的指导下,他提醒我将这个配置添加到数据库连接配置中:
allowMultiQueries=true
我的demo启动时,确实没有添加这个配置。但当我看到这个配置的那一刻,我就开始觉得有点意思了。
因为我知道这个配置是用来做什么的。
看名字就知道意思:allow Multi Queries,允许多个查询。
最常用的场景是使用foreach标签进行批量插入或更新时使用此配置。
有了这个参数的支持,mapper.xml中写的sql很可能能够正常执行。
因为添加这个配置后,一个数据库连接中可以执行多条sql语句,但是对于MyBatis或MySQL驱动来说,它并不区分“多条sql”是insert语句还是update语句,还是混合体的句子。
我还去MySQL官网查了一下这个配置的含义:
https://www.sychzs.cn/doc/connector-j/8.1/en/connector-j-connp-props-security.html#cj-conn-prop_allowMultiQueries
这个参数,官网上只有一句话:
允许使用“;”在一个语句中分隔多个查询。此选项不会影响“addBatch()”和“executeBatch()”方法,它们依赖于“rewriteBatchStatements”。 允许使用“;”在一个语句中分隔多个查询。此选项不会影响“addBatch()”和“executeBatch()”方法,因为它们依赖于“rewriteBatchStatements”。
在引入allowMultiQueries的时候,还提到了一个rewriteBatchStatements参数。
至于这个参数的作用,这里就不多说了。我只能说这两件事是一套组合拳,里面的文章也很多。如果你不知道,我建议你去了解一下。
就当做课后练习吧。
我们先来说主线吧。
当我将配置allowMultiQueries=true添加到数据库连接时,我重新启动了服务。
再次发起呼叫。
为了表达我的震惊,我给你们做了一个动画:
库存减少一,订单增加一,方法执行成功。
确实有效。你做到了,真是令人大开眼界。
这波知识增加了,是一条从未想象过的路。
不要这样写!
听韦大师的劝告,千万不要这样写!
首先,这种写法不符合大多数程序员的理解。
谁能想到最终的mapper.xml不只是简单的sql,里面还埋藏着一堆业务逻辑呢?
关键是这样写也有误导性。
举个简单的例子,这种写法没有考虑库存是否充足:
比如当前库存已经没有了,按照这种写法,仍然会向order_info表中插入一条数据。
超卖了,我的朋友。
只考虑commit,不考虑rollback。
而且这样写根本无法考虑超卖的情况,因为你无法得知减库存操作是否成功,从而无法判断是否需要commit或者rollback。
什么,你问我能不能写一个存储过程来判断?
是的,MyBatis 确实可以调用存储过程。
首先,存储过程还是要写在MySQL中,MyBatis只是发起调用。
其次,赶紧放弃你越走越远的顽皮想法,老老实实写Java代码来解决这个问题。是不是很好吃?
什么,你又问我,如果不需要判断前面的sql是否执行成功怎么办?
比如我前面提到的读者举的例子,几个表之间是有关系的。如果一个表中的某条数据被删除,那么其他表中相应的数据也必须被删除,而且还有一张表。需要更新状态。
大概是这样的:
开始;从表1中删除where user_id=xxx;从表2中删除whereuser_id=xxx; 从表3中删除其中 user_id=xxx;更新表4 设置 user_status=1 其中 user_id=xxx;提交;
与卖货的场景不同的是,在这个场景中,如果每条sql执行成功,就说明业务执行成功。
看起来没有什么问题。
但是我问你一个问题:这组SQL一定会被commit吗?
你想好了吗?
绝对不一定是这样。执行担保期间出现问题。
举个最简单的例子,表格写错了:
在这种情况下,再次发起呼叫:
程序报告错误,指出找不到表。
那么请问:此时订单表中是否应该有数据插入呢?
发生异常。绝对不应该插入任何数据。我检查了数据库,确实没有插入新数据。
看起来确实不错。
那我再问一下:这样写的话,当前事务是回滚了还是提交了?
。 。 。
正确答案暂停。
通过执行以下SQL,我们可以得到当前的事务列表:
从INFORMATION_SCHEMA.INNODB_TRX中选择*;
从查询结果中我们可以发现,我们的程序抛出异常后,当前事务仍然处于RUNNING状态:
而且,此事务将保持 RUNNING 状态,即挂起状态,直到服务重新启动。
但是仅仅从程序的角度来看,抛出了异常,没有数据,这也是符合预期的,没有什么问题。
是我的。
所以,听韦大师的建议,千万不要这样写!
老老实实写出大家都能理解的Java代码,不要乱搞mapper.xml。
其实我觉得以上都是没什么用的知识点,因为人们一般不会这样写。
不过既然都写到这里了,场景也在那里,我也给大家拓展一些稍微有用的知识。
还是在卖货的背景下。
订单中加一,库存中减一。这就是它的工作原理。
开始;插入 order_info(`buy_name`, `buy_goods`) VALUES ('歪大师', 'ipad pro 顶配版'); 更新产品设置product_count=product_count-1其中id=1且product_count>0;提交;
库存减一,订单增加一:
开始;更新产品设置product_count=product_count-1其中id=1且product_count>0;插入order_info(`购买名称`, `购买商品`) VALUES ('歪大师', 'ipad pro 顶配版');提交;
都包含在交易中。为了简化代码,我们假设库存非常充足,不考虑回滚场景。
“订单加一、库存减一”的性能好还是“库存减一、订单加一”的性能好,还是两者没有区别?
首先从执行结果来看,两者确实没有什么区别,都能够保证业务场景的正确性。
但考虑性能的话,执行“订单加一,库存减一”肯定是更好的。
如果你还没弄清楚,我给你一个简单的提示:只要业务正确,锁定的代码与解锁的代码越接近,性能就越好?
如果你还没弄清楚,我再给你一个提示:当库存减一时,会被锁定吗?你并不关心它是否添加了表锁、间隙锁或记录锁。我就问你加不加锁?
如果你还没有反应过来,说明你对MySQL的锁机制的掌握有点薄弱,可以加强一下。
我直接公布答案:
更新产品集product_count=product_count-1,其中id=1且product_count>0;
因为where条件是id=1,加锁的是唯一索引,并且该记录存在于表中,所以只会锁定id=1的记录。
对于商品id=1,如果是热门商品,我们采用“订单加一,库存减一”的写法,性能会更高。
因为锁定频率相同时,解锁越快,性能越高。
看最后一张图你就明白了:
只要改一条SQL,性能就上去了。我问你,你觉得舒服吗?
最后说点不相关的:
我在文章开头给出了这样一张图:
你不觉得尴尬吗?
sela是什么鬼?
显然,这个地方是一个简单的拼写错误,你要输入的单词是sale:
请问,当你在程序中看到这样的拼写时,你会怎么做?
如果是我的话,我会主动把selaProduct改成saleProduct,其他的就不会动了。
这是我在上一篇文章《童子军规则》中提到的编码规则:
纠正拼写错误的方法名或变量名也是代码中非常重要的事情。
这不是代码痴迷,这是基本的职业道德。
因为你不想让下一个接手你代码的人看到一堆“succeess、createTiem、lastUpdataBy、businessDate、producectName”之类的变量名,而血压上升,非常生气并损害他们的健康。