不存在时插入

不存在时插入

相信很多朋友写过这样的代码,如果查库发现不存在此条数据,那么就插入当前数据。

伪代码如下:

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>

参考资料

原文地址:https://www.cnblogs.com/Zzwena/p/14144744.html