具有几何点类型的dbWriteTable到MariaDB

问题描述

使用sf中的R对象,以及MariaDB(在本例中为geometry)列中的point中的表,我都在努力以便在两个数据之间有效地移动数据(sf对象到MariaDB表,反之亦然)。

请注意,我正在使用RMariaDB包连接到MariaDB,并在此处将我的连接定义为consdb

示例数据:

library(sf)
pnt <- data.frame( name = c("first","second"),lon = c(145,146),lat = c(-38,-39) )
pnt <- st_as_sf( pnt,coords = c("lon","lat") )

尝试直接写sf对象

理想情况下,我希望能够使用MariaDBdbWriteTable将类似sf的对象直接写入dbAppendTable。目前,这看起来像是兼容性错误

如果我尝试使用dbWriteTable

dbWriteTable(consdb,"temp",pnt,temporary=TRUE,overwrite=TRUE)

# Error in result_bind(res@ptr,params) : Cannot get geometry object from data you send to the GEOMETRY field [1416]

或首先创建表格:

dbExecute(consdb,"CREATE OR REPLACE TEMPORARY TABLE temp (name VARCHAR(10),geometry POINT)")
dbAppendTable(consdb,pnt)

# Error in result_bind(res@ptr,params) : Unsupported column type list

尝试在插入时转换为点类型

如果我要通过sql插入查询进行插入,则可以像这样使用PointFromText

INSERT INTO temp (name,geometry) VALUES ('new point',PointFromText('POINT(145 38)',4326));

所以我尝试使用它来将数据作为字符串发送。我编写了一些函数sf几何列转换为适当的字符串列:

# to convert 1 value
point_to_text <- function(x,srid = 4326) {
    sprintf("PointFromText('POINT(%f %f)',%i)",x[1],x[2],srid)
}

# to apply the above over a whole column
points_to_text <- function(x,srid = 4326) {
    vapply(x,point_to_text,srid = srid,NA_character_)
}

用于将sf对象变成data.frame

for_sql <- data.frame(pnt)
for_sql$geometry <- points_to_text(for_sql$geometry)

“几何”列现在是一个字符列,例如:PointFromText('POINT(145.000000 -38.000000)',4326)

使用dbWriteTable只会创建一个文本列,因此我尝试创建表,然后使用dbAppendTable

dbExecute(consdb,params) : Cannot get geometry object from data you send to the GEOMETRY field [1416]

有效的方法,但似乎很愚蠢

如果我创建一个临时sql表,将列更改为文本,从R中插入数据,在sql中转换列,然后将其附加到原始sql表中,则可以使此方法起作用。似乎令人费解,但是只是为了证明它有效:

# create temporary table
dbExecute(consdb,"CREATE OR REPLACE TEMPORARY TABLE temp_geom LIKE temp")

# change the geometry column to text
dbExecute(consdb,"ALTER TABLE temp_geom MODIFY COLUMN geometry TEXT")

# add the data to the temporary table
dbAppendTable(consdb,"temp_geom",for_sql)

# add a new point column
dbExecute(consdb,"ALTER TABLE temp_geom ADD COLUMN geom_conv POINT")

# convert strings to points
dbExecute(consdb,"UPDATE temp_geom SET geom_conv = PointFromText(geometry,4326)")

# drop the old column and replace it with the new one
dbExecute(consdb,"ALTER TABLE temp_geom DROP COLUMN geometry")
dbExecute(consdb,"ALTER TABLE temp_geom CHANGE COLUMN geom_conv geometry POINT")

# append the data from the temporary table to the main one
dbExecute(consdb,"INSERT INTO temp SELECT * FROM temp_geom")

是否有其他解决方案,或者任何可能解决sf对象和MariaDB表之间传递数据的问题的解决方案?

添加:根据@SymbolixAU的评论,我现在尝试了以下

st_write(
    obj=pnt,# the sf class object,as created above
    dsn=consdb,# the MariaDB connection
    layer="temp",# the table name on MariaDB
    append=TRUE,layer_options=c('OVERWRITE=false','APPEND=true')
)

# Error in result_bind(res@ptr,params) : 
  Cannot get geometry object from data you send to the GEOMETRY field [1416]

解决方法

我为此提出了一些棘手的解决方案。这不是理想的,但我认为可能已经足够了。

由于sf包可以使用st_as_text将几何列转换为WKT字符串,并且MariaDB可以对ST_GeomFromText做相反的处理,因此我可以使用它们来获得一切正常。

一个问题是我要传递给MariaDB的函数调用(类似ST_GeomFromText('POINT(1 2)')的东西不能传递给任何dbAppendTable这样的常用表写函数,因为它们会转换该函数将调用文本字符串(假定为带引号),因此我必须创建自己的插入查询并使用dbExecute进行调用。

这是我想出的功能,当要写入的对象是dbAppendTable对象时,其作用是扮演sf的角色。

sf_dbAppendTable <- function(conn,name,value,srid = 4326) {
    
    # convert the geometry columns to MariaDB function calls
    sfc_cols <- vapply(value,inherits,NA,"sfc")
    for(col in which(sfc_cols)) {
        value[[col]] <- sprintf(
            "ST_GeomFromText('%s',%i)",sf::st_as_text( value[[col]] ),srid
        )
    }
    
    
    # when inserting to sql,surround some values in quotes,except a few types
    # specifically exclude the geometry columns from this
    cols_to_enquote <- vapply(value,function(x) {
        if (inherits(x,"logical")) return( FALSE )
        if (inherits(x,"integer")) return( FALSE )
        if (inherits(x,"double")) return( FALSE )
        return( TRUE )
    },NA) & !sfc_cols
    
    # set aside column names
    col_names <- names(value)
    
    # convert to a matrix
    value <- as.matrix(value)
    
    # it should be character
    if (typeof(value) != "character") value <- as.character(value)
    
    # enquote the columns that need it,except for `NA` values,replace with `NULL`
    value[,which(cols_to_enquote) ] <- ifelse(
        is.na(value[,which(cols_to_enquote) ]),"NULL",paste0("'",value[,which(cols_to_enquote) ],"'")
    )
    
    # any `NA` values still remaining,also replace with `NULL`
    value[ is.na(value) ] <- "NULL"
    
    # create a single insert query
    sql_query <- sprintf(
        "INSERT INTO %s (%s) VALUES (%s);",paste(col_names,collapse = ","),paste(apply(value,1,paste,collapse = "),(")
    )
    
    # execute the query
    dbExecute(conn,sql_query)
    
}

这似乎对我有用,但是我敢肯定,它远没有dbAppendTable这样的健壮或高效。一方面,我使用的是单个查询字符串,不适用于大型查询,并且效率不如某些软件包设法利用的LOAD DATA INFILE方法有效。

如果有人有更好的解决方案,我还是很想听听。