upsert是update和insert的合体,这里暂时不对其具体的语义进行探讨,简单对其做一个定义,基本功能为:存在时更新,不存在时插入,简单的解释就是,当某种条件成立时使用update,条件不成立时使用insert。
在很多场景下,我们少不了使用upsert功能,比如数据增量处理,商品状态更新等,而在mysql中并没有这样的一种语句,如果我们先查询是否存在,再选择使用update或者insert,这样不仅比较复杂而且还容易造成事务上的冲突,不过后来发现有方式实现这种需求,那就是ON DUPLICATE KEY UPDATE
。
举个栗子:
INSERT INTO table (a,b,c) VALUES (1,2,3) ON DUPLICATE KEY UPDATE c=c+1;
ON DUPLICATE KEY UPDATE
需要配合UNIQUE索引
或PRIMARY KEY
使用。
这句sql的基本解释为,当UNIQUE索引
或PRIMARY KEY
的字段在表中不存在时,行作为新记录被插入,如果字段存在,则执行ON DUPLICATE KEY UPDATE
后面的逻辑,这里就是c=c+1。
这里我们可以将字段a设置为唯一索引或者主键,还可以将a,b设置为联合唯一索引,就能实现upsert的需求,具体如何应用还是要看具体的场景和需求。
一般存入mysql中的数据都是结构化的数据,这里我推荐先将RDD转为dataframe,再进行数据库方面的操作,因为同为结构化的数据转换起来比较简单,而且只需要很小的改动就能得实现我们的需求。
而一般的dataframe写入mysql中一般是使用的
dataFrame.write().mode("append").jdbc(url, "table", mysqlProperties)
下面过一遍源码,首先调用的dataframe的write()生成DataFrameWriter,然后再配置mode,我们这里选用的是append,接下来来到了jdbc()中创建createConnectionFactory()以及检查表是否存在,如果不存在则从dataframe中取出schema来创建表,然后调用的是saveTable(),最后使用的是savePartition()来进行数据类型映射以及数据的commit,这里执行语句是在insertStatement()中处理的。
而在上面我们的mysql upsert实现中,主要还是通过对执行语句的处理来实现,也就是说我们只需要在调用链的函数中加上一个参数,并且改动一下insertStatement(),就能实现我们想要的需求,在这里我是这样操作的:
def insertStatement(conn: Connection, table: String, rddSchema: StructType, accumulateColumns: Array[String]): PreparedStatement = {
val columns = rddSchema.fields.map(_.name).mkString(",")
val placeholders = rddSchema.fields.map(_ => "?").mkString(",")
var sql = s"INSERT INTO $table ($columns) VALUES ($placeholders)"
if (!accumulateColumns.equals(null)) {
val finalMap = accumulateColumns.map(x=>x+ "=" + "values(" + x + ")+" + x).mkString(",")
sql = s"INSERT INTO $table ($columns) VALUES ($placeholders) ON DUPLICATE KEY UPDATE $finalMap"
}
conn.prepareStatement(sql)
}
累加的列为accumulateColumns,如果我们要累加的列为c,最后的字符串会变成
INSERT INTO table (a,b,c) VALUES (?,?,?) ON DUPLICATE KEY UPDATE c=VALUES(c)+c
最后我们编译完之后打包,最后调用写入mysql的代码为:
dataFrame.write().mode("append").jdbc(url, "table", mysqlProperties,accumulateColumns)
我们这里的业务逻辑比较简单,只是简单增量的处理,更加复杂的逻辑还是需要个性化的定制,在这里提供一个简单的思路,希望对大家有所帮助。