不存在时插入
不存在时插入
相信很多朋友写过这样的代码,如果查库发现不存在此条数据,那么就插入当前数据。
伪代码如下:
Object obj = this.mapper.selectById(id);
if(obj==null){
this.mapper.insert(entity);
}
这种方式是大家最常用的一种写法。
但是当出现高并发的情况下就会出现问题:
假设有两个线程触发当前程序,线程一执行到了第三行,但还未插入;
线程二执行到第一行刚好执行完查询语句,obj为空。
此时,线程一就会执行插入语句,而线程二仍然会走到第三行执行插入语句,这样就会导致库里出现两条数据,与我们期望的结果不一样。
解决方法
- 最简单一种解决方法:在此段代码块上加锁。
synchronized (this) {
Object obj = this.mapper.selectById(id);
if(obj==null){
this.mapper.insert(entity);
}
}
这种方法简单粗暴,但不建议。因为加锁会阻塞进程,降低并发性。
- 还有一种方法是更改sql语句,在插入时校验数据是否存在。
-- 当张三的用户信息不存在时,插入张三的个人信息
-- 如下
-- 插入的数据为select中的'123456','张三','beijing'部分,DUAL 为任意名的关联表用于查询用户是否存在
insert into t_user (id,name,address)
SELECT
'123456','张三','beijing'
FROM DUAL WHERE NOT EXISTS
(
SELECT * FROM t_user WHERE name = '张三'
)
这种方法借助数据库ACID特性解决了高并发下多次插入的问题。
编码实现动态sql语句完成不存在时插入
使用@InsertProvider注解
mapper如下:
import com.example.wechat.response.pojo.entity.User;
import com.example.wechat.response.service.impl.UserSqlProvider;
import org.apache.ibatis.annotations.InsertProvider;
import tk.mybatis.mapper.common.Mapper;
/**
* @author Zzwen
* @date 2020-12-16 15:08
*/
@org.apache.ibatis.annotations.Mapper
public interface UserMapper extends Mapper<User> {
/**
* 当existedUser不存在时,插入user
*
* @param user 要插入的用户信息
* @param existedUser 要查询的用户信息
* @return 影响行数
*/
@InsertProvider(
type = UserSqlProvider.class,
method = "insertOnNotExist"
)
int insertOnNotExist(User user, User existedUser);
}
借助UserSqlProvider的insertOnNotExist方法得到动态sql语句:
import com.example.wechat.response.pojo.entity.User;
import org.springframework.stereotype.Service;
import tk.mybatis.mapper.entity.EntityColumn;
import tk.mybatis.mapper.entity.EntityTable;
import tk.mybatis.mapper.mapperhelper.EntityHelper;
import tk.mybatis.mapper.mapperhelper.SqlHelper;
import java.sql.JDBCType;
import java.util.Date;
import java.util.Set;
/**
* @author Zzwen
* @date 2020-12-16 15:10
*/
@Service
public class UserSqlProvider {
/**
* 当existedUser不存在时,插入user
*
* @param user 要插入的用户信息
* @param existedUser 要查询的用户信息
* @return 影响行数
*/
public String insertOnNotExist(User user, User existedUser) {
Class entityClass = User.class;
EntityTable entityTable = EntityHelper.getEntityTable(entityClass);
StringBuilder sql = new StringBuilder();
sql.append("<script>");
//插入数据列名
sql.append("INSERT INTO ").append(entityTable.getName()).append(" ");
Set<EntityColumn> columnSet = EntityHelper.getColumns(entityClass);
sql.append("(");
for (EntityColumn column : columnSet) {
sql.append(column.getColumn()).append(",");
}
sql.deleteCharAt(sql.length() - 1);
sql.append(")");
//插入数据属性值
sql.append("SELECT ");
for (EntityColumn column : columnSet) {
sql.append("#{user.").append(column.getProperty())
.append(",jdbcType=");
JDBCType jdbcType = getJdbcType(column.getEntityField().getJavaType());
sql.append(jdbcType).append("},");
}
sql.deleteCharAt(sql.length() - 1);
sql.append(" FROM DUAL WHERE NOT EXISTS");
//数据存在条件
sql.append("(");
sql.append("SELECT * ");
sql.append(SqlHelper.fromTable(entityClass, entityTable.getName()));
SqlHelper.whereAllIfColumns(entityClass, true, false);
sql.append("<where>");
for (EntityColumn column : columnSet) {
sql.append(SqlHelper.getIfNotNull("existedUser", column, " AND " + column.getColumnEqualsHolder("existedUser"), true));
}
sql.append("</where>");
sql.append(")");
sql.append("</script>");
return sql.toString();
}
/**
* java类型转成Jdbc类型
*
* @param javaType Java类型
* @return jdbc类型
*/
private JDBCType getJdbcType(Class<?> javaType) {
if (javaType.equals(Date.class)) {
return JDBCType.DATE;
} else if (javaType.equals(Integer.class)) {
return JDBCType.INTEGER;
} else if (javaType.equals(Long.class)) {
return JDBCType.BIGINT;
} else if (javaType.equals(Boolean.class)) {
return JDBCType.TINYINT;
} else if (javaType.equals(String.class)) {
return JDBCType.VARCHAR;
}
return null;
}
}
上面的getJdbcType方法因为没有找到转换的工具类,所以是自己写了个方法转换的,转换类型并不全,需要自己补充。
可以参考 https://blog.csdn.net/xxjuanq_only_one/article/details/11936541,继续补充其他类型的转换,或者有工具类就更好了。
在拼接SQL语句的过程中,借助了tk包中的SqlHelper,里面有包装一些sql语句,可以直接应用。
最后生成的语句如下:
<script>
INSERT INTO user (id,name,address)
SELECT
#{user.id,jdbcType=VARCHAR},#{user.name,jdbcType=VARCHAR},#{user.address,jdbcType=VARCHAR}
FROM DUAL WHERE NOT EXISTS
(
SELECT * FROM user
<where>
<if test="existedUser.id != null and existedUser.id != '' "> AND id = #{existedUser.id}</if>
<if test="existedUser.name != null and existedUser.name != '' "> AND name = #{existedUser.name}</if>
<if test="existedUser.address != null and existedUser.address != '' "> AND address = #{existedUser.address}</if>
</where>
)
</script>
该语句通过mybatis解析成相应的sql语句,就可以达到sql语句完成不存在时插入了。
应用到的依赖:
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
参考资料
010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101