有没有一种有效的方法可以从 R 中的数据框中更新 MariaDB 行?

Is there an efficient way to update MariaDB rows from a data frame in R?

我正在从一个网站收集实时数据,该网站在 R 中填充了一个数据框。 这些行可以具有相同的唯一 ID,或者可以引入新行。 我想将动态数据帧发送到 MariaDB 数据库 table,其中具有现有唯一 ID 的行更新我指定的列,没有现有唯一 ID 的行作为新行插入 table . 我可以让它与 MariaDB INSERT ON DUPLICATE KEY UPDATE 语句一起使用,以及一个从动态数据帧生成所需值的函数。

MWE:

install.packages("odbc")
insall.packages("RMariaDB")
library(odbc)
library(RMariaDB)

con <- dbConnect(RMariaDB::MariaDB(), host = Sys.getenv('MARIADB_DBHOST'), 
port = Sys.getenv('MARIADB_DBPORT'), user = Sys.getenv('MARIADB_DBUSER'), 
password = Sys.getenv('MARIADB_DBPW'), db = Sys.getenv('MARIADB_DBNAME'), 
timeout = 5)

# Database table for mwe to work.
db_live <- data.frame(id = c(12, 22, 32), car_name = c("rolls royce","nissan","mercedes benz"), km = c(123,100,150), temp = c(78,60,80))


# Get table from database, id column is unique index.
db_live <- dbReadTable(con, "db_live")
print(db_live)
  id      car_name  km temp
1 12    rols royce 123   78
2 22        nissan 100   60
3 32 mercedes benz 150   80

# Build dynamic dataframe
df_live <- data.frame(id = c(12, 22, 32, 42), 
car_name = c("rolls royce","nissan","mercedes benz", "aston martin"),
km = c(140,120,200,40), temp = c(81,65,85,50))

print(df_live)
  id      car_name  km temp
1 12    rols royce 140   81
2 22        nissan 120   65
3 32 mercedes benz 200   85
4 42  aston martin  40   50

# create function that generates a string with values for dbSendQuery.
gen_insert_values <- function(df) {
for(i in 1:nrow(df)) {
row_string <- paste(df[i,1], paste0("'",df[i,2],"'"), df[i,3], df[i,4], 
collapse = ", ")

if(exists("df_string")) {
  df_string <- paste0(df_string,", ",paste0("(",row_string,")"))
} else {
  df_string <- paste0("(",row_string,")")
}
}
df_string
}

values <- gen_insert_values(df_live)

print(values)
"(12 'rolls royce' 140 81), (22 'nissan' 120 65), (32 'mercedes benz' 200 85), (42 'aston martin' 40 50)"

# Send query.
res <- dbSendQuery(con, paste0("INSERT INTO db_live (id,car_name,km,temp) VALUES ", values," ON DUPLICATE KEY UPDATE km = VALUES(km), temp = VALUES(temp);"))
dbClearResult(res)

#Check db table after sent query.

new_db_live <- dbReadTable(con, "db_live")

print(new_db_live)
  id      car_name  km temp
1 12   rolls royce 140   81
2 22        nissan 120   65
3 32 mercedes benz 200   85
4 42  aston martin  40   50

这似乎不是很有效,因为我必须更改查询和函数以防万一我想更新更多列,并且我在我的函数中包含一个 for 循环,这可能会导致脚本变慢。

有没有更有效的方法来解决这个问题?

这里有一个可能更有效的方法:使用临时 table 而不是手动将数据编码为 (a,b,c),(a,b,c) 数据集的字符串。

为了完整演示,我稍微修改了 df_live 数据,以便我们有一行没有变化,一行有更新的数据,还有一行是新的。这个过程同样适用于您原来的 df_live,我只是想强调这三种模式。

虽然从技术上讲,“无变化”行确实更新了数据库,但并不明显。如果 table 有一个“lastmodified”字段,当行中的某些内容更新时,该字段会使用当前时间戳进行更新,那么您可以看到更多正在发生的事情。

事实上,我将添加(仅用于演示)两个字段:createdmodified,它们显示首次创建行的时间和最后一次更新发生的时间。正常的 UPSERT 不需要这些。

设置

这部分应该不是必需的,除非您在 table 上没有主键(在这种情况下,添加一个)。

我将命名主 table "mydata",并将 db_live 数据集上传到其中。我相信(无需大量测试)MariaDB 需要 UPSERT 来根据预先存在的 keys 查找重复或冲突的行。这意味着我们需要设置一个(主)键;我假设您的 table 已经有了这个(并展示了我如何使用手动上传的数据)。

db <- DBI::dbConnect(RMariaDB::MariaDB(), ...)
db_live <- data.frame(id = c(12, 22, 32),
                      car_name = c("rolls royce","nissan","mercedes benz"),
                      km = c(123,100,150), temp = c(78,60,80))
df_live <- data.frame(id = c(12, 22, 42), 
                      car_name = c("rolls royce","nissan","aston martin"),
                      km = c(140,120,40), temp = c(81,65,50))
df_live
#   id     car_name  km temp
# 1 12  rolls royce 140   81   # updated
# 2 22       nissan 100   60   # no change
# 3 42 aston martin  40   50   # new data

DBI::dbWriteTable(db, "mydata", db_live)
DBI::dbExecute(db, "alter table mydata add primary key (id)")
# [1] 0
DBI::dbExecute(db, "
  alter table mydata
    add column created timestamp not null default CURRENT_TIMESTAMP,
    add column modified timestamp null default null")
# [1] 0
DBI::dbExecute(db, "
  create trigger updatemodified_mydata
  before update on mydata
  for each row set NEW.modified = CURRENT_TIMESTAMP")
# [1] 0

DBI::dbGetQuery(db, "select * from mydata")
#   id      car_name  km temp             created modified
# 1 12   rolls royce 123   78 2021-06-29 19:11:00     <NA>
# 2 22        nissan 100   60 2021-06-29 19:11:00     <NA>
# 3 32 mercedes benz 150   80 2021-06-29 19:11:00     <NA>

如果您在主 table mydata 上没有 primary key,则“UPSERT”操作将简单地插入(添加)所有行而不更新。我不知道是否有办法诱使 mariadb 伪造密钥以正确触发您预期的“如果存在则更新”逻辑。

更新

我们将使用一个临时的 table 这样要更新的数据就不会持久存在;这有几个好处,如果你做得正确,那么你的 DBA 会感谢你:-)

(如果你不熟悉 temp tables ...它们对数据库上的其他用户不可见,通常对同一用户的不同连接不可见,并且会在连接已关闭。)

DBI::dbCreateTable(db, "mytemp", df_live, temporary = TRUE)
DBI::dbWriteTable(db, "mytemp", df_live, append = TRUE, create = FALSE)
DBI::dbGetQuery(db, "select * from mytemp")
#   id     car_name  km temp
# 1 12  rolls royce 140   81
# 2 22       nissan 100   60
# 3 42 aston martin  40   50

DBI::dbExecute(db, "
  insert into mydata (id,car_name,km,temp)
  select id,car_name,km,temp from mytemp
  on duplicate key update km=VALUES(km), temp=VALUES(temp);")
# [1] 5

DBI::dbGetQuery(db, "select * from mydata")
#   id      car_name  km temp             created            modified
# 1 12   rolls royce 140   81 2021-06-29 19:11:00 2021-06-29 19:11:27
# 2 22        nissan 100   60 2021-06-29 19:11:00 2021-06-29 19:11:27
# 3 32 mercedes benz 150   80 2021-06-29 19:11:00                <NA>
# 4 42  aston martin  40   50 2021-06-29 19:11:27                <NA>

如果您注意到,即使 "nissan" 的值没有不同,据称该行仍已更新,如 modified 时间戳所证明。我们的“更改”行 "rolls royce" 显示了适当的 modified 时间。 mercedes benz是第一次上传没更新,aston martin是第二次更新,所以created时间跟其他的不一样


复制

我用 mariadb:latest docker 图片做了这个。下面的这些步骤纯粹是为了演示,并不是作为管理数据库(为了安全或性能)的规范方法提供的。是的,我正在连接到 "mysql" 数据库,这不是用户数据应该去的地方,我相信......很仓促,请原谅我。

$ docker pull mariadb:latest
Using default tag: latest
latest: Pulling from library/mariadb
c549ccf8d472: Pull complete
26ea6552a462: Pull complete
329b1f41043f: Pull complete
9f8d09317d80: Pull complete
2bc055a5511d: Pull complete
e989e430508e: Pull complete
cdba2af19f87: Pull complete
04fe4f90eab8: Pull complete
389c6b423e31: Pull complete
bef640655d86: Pull complete
Digest: sha256:0c72b63198ac53df4e84db821876c73794b00509b2d8a77100d186a13e49ac31
Status: Downloaded newer image for mariadb:latest
docker.io/library/mariadb:latest

$ docker run -p 127.0.0.1:3306:3306  --name some-mariadb \
    -e MARIADB_ROOT_PASSWORD=mysecretpw -e MARIADB_DATABASE=mydb -d mariadb:latest

在 R 中,连接很简单:

db <- DBI::dbConnect(RMariaDB::MariaDB(), host="127.0.0.1", port=3306, 
                     username="root", password="mysecretpw", dbname="mydb")
DBI::dbGetQuery(db, "select version() as dbver")
#                                   dbver
# 1 10.5.11-MariaDB-1:10.5.11+maria~focal