乐趣区

关于后端:角度新奇第一次看到这样使用MyBatis的看得我一愣一愣的

你好呀,我是歪歪。

这期给大家分享一个读者给我分享的一个对于 MyBatis 的“编程小技巧”,说真的,这骚操作,间接把我看得一愣一愣的。

我更愿意叫它:坑你没商量之埋雷大法。

Demo

为了让你丝滑入戏,我还是先给你搞个 Demo。

因为要应用到 MyBatis 嘛,所以咱们先搞两个表。

一个表叫做 product 表,表构造非常简单:

另一个表叫做 order_info 表,表构造也非常简单:

看到这两个表呈现的时候,你就晓得我的场景是啥了,必定是卖货嘛。

库存减一,订单加一。

大家再相熟不过的场景了。

分分钟能写出这样的伪代码:

public void saleProduct(){
    // 更新库存,库存减一
    productMapper.updateProductCount();
    // 保留订单信息
    orderInfoMapper.saveOrderInfo();}

当然了,这个伪代码你一眼就能看出问题:减库存和保留订单应该是一个事务操作,所以应该把这两个动作包裹在事务外面。

于是咱们的伪代码变成了这样:

public void saleProduct() {
    // 开启事务
    begin;
    // 更新库存,库存减一
    Boolean updateSuccess = productMapper.updateProductCount();
    // 保留订单信息
    orderInfoMapper.saveOrderInfo();
    if (updateSuccess) {
        // 提交事务
        commit;
    } else {
        // 回滚事务
        rollback
    }
}

过后读者给我举例的时候,齐全是另外一个场景,和卖货齐全没有任何关系。

读者举的例子大略是几个表之间有关联关系,如果一个表的某条数据被删除了,另外几个表外面对应的数据也要删除,还有一个表须要更新状态。

为了更好的展现这个“编程小技巧”,我才把场景简化到了后面提到的卖货的样子。

后面说的是伪代码。

当初我给你展现一下用“编程小技巧”写进去的实在的代码。

首先是 controller 接口:

@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://dev.mysql.com/doc/connector-j/8.1/en/connector-j-conn…

对于这个参数,官网上就一句话:

Allow the use of “;” to delimit multiple queries during one statement. This option does not affect the ‘addBatch()’ and ‘executeBatch()’ methods, which rely on ‘rewriteBatchStatements’ instead.
容许在一条语句中应用 ”; “ 分隔多个查问。该选项不会影响 “addBatch() “ 和 “executeBatch() “ 办法,因为它们依赖于 “rewriteBatchStatements”。

在介绍 allowMultiQueries 的时候,还提到了一个 rewriteBatchStatements 参数。

对于这个参数是干啥的,我这里就不开展形容了,我只能说这两个玩意是一套组合拳,外面也大有文章,如果你不晓得,倡议你去理解一下。

就当是课后习题了。

咱们还是先跟着骨干走。

当我在数据库连贯上追加配置 allowMultiQueries=true 之后,重启了服务。

再次发动调用。

为了示意我的震惊,我给你搞个动图:

库存减一,订单加一,办法执行胜利了。

还真 TM 能用,你说这事搞的,实属是开了眼了。

这波涨常识了,属于未曾构想过的路线。

埋雷

千万别这样写!

听歪徒弟一句劝,千万别这样写!

首先这样的写法就不合乎绝大部分程序员的认知。

试问谁能想到最初的 mapper.xml 外面,并不只是简简单单的 sql,外面竟然还埋在一坨业务逻辑呢?

要害是这样写也埋雷啊。

举个简略的例子,这样的写法,齐全没有思考库存是否足够的状况:

比方,以后库存没有了,依照这样的写法,还是会在 order_info 表外面插入一条数据。

超卖了,敌人。

只有 commit,没有思考回滚的状况。

而且这样写基本就齐全不可能思考超卖的状况,因为你拿不到扣减库存的操作是否执行胜利,从而无奈判断是须要 commit 还是 rollback。

什么,你问我能不能写存储过程来判断?

能,MyBatis 的确能够调用存储过程。

首先,存储过程还是得在 MySQL 外面写好,MyBatis 只是发动调用。

其次,连忙打消你这个越走越远的骚想法,老老实实的写 Java 代码来解决这个问题,它不香吗?

什么,你又问我如果是不须要判断前一条 sql 是否执行胜利的场景呢?

比方我后面提到的读者举的例子,几个表之间有关联关系,如果一个表的某条数据被删除了,另外几个表外面对应的数据也要删除,还有一个表须要更新状态。

大略是这样的:

begin;
delete from table1 where user_id=xxx;
delete from table2 where user_id=xxx;
delete from table3 where user_id=xxx;
update table4 set user_status=1 where user_id=xxx;
commit;

和卖货的场景不一样的是,在这个场景下如果每个 sql 执行胜利,则代表业务执行胜利。

看起来,仿佛没什么问题。

然而我问你一个问题:这一组 SQL 肯定会走都 commit 吗?

你好好想想?

必定不肯定嘛,保不齐执行的过程中出什么幺蛾子。

举个最简略的例子,表写错了:

在这个场景下,再次发动调用:

程序报错说找不到这个表。

那么请问:此时,订单表是否应该有数据被插入?

出异样了,必定不应该有数据插入。我看了数据库,的确也没有新数据插入。

看起来的确没问题。

那么再请问:在这种写法的状况下,以后这个事务是被回滚了还是被提交了?

。。。

。。。

。。。

正确答案是被挂起了。

通过执行上面这个 SQL,咱们能够获取到以后事务列表:

SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;

通过查问后果能够发现,在咱们程序抛出异样之后,以后事务还在 RUNNING 状态:

而且,这个事务在服务重启之前,将始终在 RUNNING 状态,即被挂起了。

但仅从程序的角度看,抛出异样,没有数据,合乎预期,没有任何故障。

埋雷了。

所以,听歪徒弟一句劝,千万别这样写!

老老实实的写大家都看得懂的 Java 代码,不要在 mapper.xml 外面搞事件。

扩大

其实我感觉吧,后面都属于卵用不大的知识点,因为大家个别都不会这样去写。

然而既然都写到这里了,场景也有了,我也给大家扩大一个略微有点用的常识。

还是在卖货的场景下。

订单加一,库存减一是这样的。

begin;
INSERT INTO order_info(`buy_name`, `buy_goods`) VALUES ('歪徒弟', 'ipad pro 顶配版');
update product set product_count=product_count-1 where id=1 and product_count>0;
commit;

而库存减一,订单加一是这样的:

begin;
update product set product_count=product_count-1 where id=1 and product_count>0;
INSERT INTO order_info(`buy_name`, `buy_goods`) VALUES ('歪徒弟', 'ipad pro 顶配版');
commit;

都是包裹在事务外面,为了简化代码,咱们假如库存十分够用,先不思考 rollback 的场景。

请问是“订单加一,库存减一”的性能好,还是“库存减一,订单加一”的性能好,还是说这二者没有什么区别?

首先,从执行后果上看,这二者的确是没有什么区别的,都能保障业务场景的正确性。

然而当你思考性能的时候,必定是“订单加一,库存减一”的性能更好。

如果你没想明确的话,我给你一个简略的提醒:在业务正确的前提下,加锁的代码越凑近解锁的代码,是不是性能越好?

如果你还没想明确的话,我再给你一个提醒:库存减一,它会加锁吗?你不论它是加表锁、间隙锁还是记录锁,我就问你它加不加锁?

如果你还没反馈过去的话,阐明你对于 MySQL 的加锁机制把握的有点单薄,能够去加固一下。

我间接颁布答案了:

update product set product_count=product_count-1 where id=1 and product_count>0;

因为 where 条件中是 id=1,所以锁是加在惟一索引上的,而且表中存在该记录,所以只会对 id=1 这行记录加锁。

针对 id=1 这一个产品来说,如果它是一个热点商品,咱们采取“订单加一,库存减一”的写法,性能会更高一点。

因为在加锁频率雷同的状况下,解锁越快的,性能越高。

上个图你就明确了:

调换一个 SQL 的事儿,性能就下来了,我就问你舒不难受?

最初,再说个不相干的:

我在文章最开始的中央给了这样的一个图片:

你不感觉顺当吗?

sela 是什么鬼?

很显著,这个中央是一个单纯的拼写错误,想要打出的单词是 sale:

请问,当你在程序外面看到这样的拼写的时候,你会怎么办?

如果是我,我会被动把 selaProduct 批改为 saleProduct,其余什么都不会动。

这就是我在之前的文章中提到的一个编码规定,童子军军规:

批改一个拼写错误的办法名、变量名,在代码外面也是一件很重要的小事。

这不是代码洁癖,这是根本的职业道德。

因为你也不想下一个接手你代码的人,因为看到一堆叫做“succeess、createTiem、lastUpdataBy、bussinessDate、proudectName”等等这些变量名而血压回升,气大伤身。

退出移动版