<update id="updateForBatch" parameterType="cn.net.susan.entity.sys.UserEntity"> <foreach collection="list" item="entity" separator=";"> UPDATE sys_user SET password = #{entity.password},update_user_id=#{entity.updateUserId},update_user_name=#{entity.updateUserName} <where> id = #{entity.id} </where> </foreach> </update>有小伙伴说,第一次见到这种写法,涨知识了。
for(UserEntity userEntity: list) { userMapper.update(userEntity); }
直接for循环需要多次请求数据库,网络有一定的开销,很显然没有批量一次请求数据库的好。
<update id="updateForBatch" parameterType="cn.net.susan.entity.sys.UserEntity"> update sys_user <trim prefix="set" suffixOverrides=","> <trim prefix="password = case id" suffix="end,"> <foreach collection="list" item="item"> when #{item.id} then #{item.password} </foreach> </trim> <trim prefix="update_user_id = case id" suffix="end,"> <foreach collection="list" item="item"> when #{item.id} then #{item.updateUserId} </foreach> </trim> <trim prefix="update_user_name = case id" suffix="end"> <foreach collection="list" item="item"> when #{item.id} then #{item.updateUserName} </foreach> </trim> </trim> <where> id in ( <foreach collection="list" separator="," item="item"> #{item.id} </foreach> ) </where> </update>但这种写法显然需要拼接很多条件,有点复杂,而且性能也不太好。还有些文章中介绍,可以使用在insert的时候,可以在语句最后加上ON DUPLICATE KEY UPDATE关键字。
<update id="updateForBatch" parameterType="cn.net.susan.entity.sys.UserEntity"> insert into sys_user (id,username,password) values <foreach collection="list" index="index" item="item" separator=","> (#{item.id}, #{item.username}, #{item.password}) </foreach> ON DUPLICATE KEY UPDATE password=values(password) </update>
在插入数据时,数据库会先判断数据是否存在,如果不存在,则执行插入操作。如果存在,则执行更新操作。这种方式我之前也用过,一般需要创建唯一索引。因为很多时候主键id,是自动增长的或者根据雪花算法生成的,每次都不一样,没法区分多次相同业务参数请求的唯一性。因此,建议创建一个唯一索引,来保证业务数据的唯一性。
比如:给username创建唯一索引,在insert的时候,发现username已存在,则执行update操作,更新password。这种方式批量更新数据,性能比较好,但一般的大公司很少会用,因为非常容易出现死锁的问题。因此,目前批量更新数据最好的选择,还是我在文章开头介绍的第一种方法。
sql injection violation, multi-statement not allow
这个异常是阿里巴巴druid包的WallFilter中报出来了。它里面有个checkInternal方法,会对sql语句做一些校验,如果不满足条件,就会抛异常:而druid默认不支持一条sql语句中包含多个statement语句,例如:我们的批量update数据的场景。
datasource: type: com.alibaba.druid.pool.DruidDataSource druid: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/console?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true username: root password: root这个改动非常简单。但WallFilter中的校验问题如何解决呢?于是,我上网查了一下,可以通过参数调整druid中的filter的判断逻辑,比如:
spring: datasource: url: jdbc:xxx&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true&allowMultiQueries=true username: xxx password: xxx driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource druid: filter: wall: config: multi-statement-allow: true none-base-statement-allow: true通过设置filter中的multi-statement-allow和none-base-statement-allow为true,这样就能开启批量更新的功能。
sql injection violation, multi-statement not allow这是怎么回事呢?
<dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <version>3.1.1</version> </dependency>我们是使用了baomidou包下的数据源配置,这个配置在DynamicDataSourceProperties类中:
/** * Copyright © 2018 organization baomidou * <pre> * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * <pre/> */ package com.baomidou.dynamic.datasource.spring.boot.autoconfigure; import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.druid.DruidConfig; import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.hikari.HikariCpConfig; import com.baomidou.dynamic.datasource.strategy.DynamicDataSourceStrategy; import com.baomidou.dynamic.datasource.strategy.LoadBalanceDynamicDataSourceStrategy; import com.baomidou.dynamic.datasource.toolkit.CryptoUtils; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.NestedConfigurationProperty; import org.springframework.core.Ordered; import java.util.LinkedHashMap; import java.util.Map; /** * DynamicDataSourceProperties * 堆代码 duidaima.com * @see DataSourceProperties * @since 1.0.0 */ @Slf4j @Getter @Setter @ConfigurationProperties(prefix = DynamicDataSourceProperties.PREFIX) public class DynamicDataSourceProperties { public static final String PREFIX = "spring.datasource.dynamic"; public static final String HEALTH = PREFIX + ".health"; /** * 必须设置默认的库,默认master */ private String primary = "master"; /** * 是否启用严格模式,默认不启动. 严格模式下未匹配到数据源直接报错, 非严格模式下则使用默认数据源primary所设置的数据源 */ private Boolean strict = false; /** * 是否使用p6spy输出,默认不输出 */ private Boolean p6spy = false; /** * 是否使用seata,默认不使用 */ private Boolean seata = false; /** * 是否使用 spring actuator 监控检查,默认不检查 */ private boolean health = false; /** * 每一个数据源 */ private Map<String, DataSourceProperty> datasource = new LinkedHashMap<>(); /** * 多数据源选择算法clazz,默认负载均衡算法 */ private Class<? extends DynamicDataSourceStrategy> strategy = LoadBalanceDynamicDataSourceStrategy.class; /** * aop切面顺序,默认优先级最高 */ private Integer order = Ordered.HIGHEST_PRECEDENCE; /** * Druid全局参数配置 */ @NestedConfigurationProperty private DruidConfig druid = new DruidConfig(); /** * HikariCp全局参数配置 */ @NestedConfigurationProperty private HikariCpConfig hikari = new HikariCpConfig(); /** * 全局默认publicKey */ private String publicKey = CryptoUtils.DEFAULT_PUBLIC_KEY_STRING; }这个类是数据库的配置类,我们可以看到master和druid的配置是在同一层级的,于是,将application.yml文件中的配置改成下面这样的:
spring: application: name: mall-job datasource: dynamic: primary: master datasource: master: username: root password: 123456 url: jdbc:mysql://localhost:3306/susan_mall?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false&zeroDateTimeBehavior=convertToNull driver-class-name: com.mysql.cj.jdbc.Driver druid: wall: multiStatementAllow: true noneBaseStatementAllow: true这样改动之后,商城项目中使用foreach这种批量更新数据的功能OK了。
sql injection violation, multi-statement not allow首先要在数据库连接的url后面增加&allowMultiQueries=true参数,开启数据的批量更新操作。
spring: datasource: druid: filter: wall: config: multi-statement-allow: true none-base-statement-allow: true主要是multi-statement-allow设置成true。如果你还使用了其他第三方的数据库中间件,比如我使用了baomidou实现多个数据源动态切换的功能。这时候,需要查看它的源码,确认它multi-statement-allow的配置参数是怎么配置的,有可能跟druid不一样。