【问题标题】:RODBC::sqlSave - problems creating/appending to a tableRODBC::sqlSave - 创建/追加到表的问题
【发布时间】:2016-02-19 18:23:35
【问题描述】:

RODBC 包上的several other questions 相关,我在使用RODBC::sqlSave 写入SQL Server 数据库上的表时遇到问题。我在 Windows RDP 上使用 MS SQL Server 2008 和 64 位 R。

第三个链接(问题)中的解决方案确实有效 [sqlSave(ch, df)]。但在这种情况下,它写入了错误的数据库。也就是说,我的默认数据库是“C2G”,但我想写入“BI_Sandbox”。而且它不允许诸如rownames之类的选项。所以包中似乎仍然存在问题。

显然,一个可能的解决方案是将我的 ODBC 解决方案更改为指定的数据库,但似乎应该有更好的方法。而这并不能解决sqlSave命令中参数不可用的问题——比如rownamesvarTypes等。

我有以下 ODBC-系统 DSN 连接:

Microsoft SQL Server Native Client Version 11.00.3000

Data Source Name: c2g
Data Source Description: c2g
Server: DC01-WIN-SQLEDW\BISQL01,29537
Use Integrated Security: Yes
Database: C2G
Language: (Default)
Data Encryption: No
Trust Server Certificate: No
Multiple Active Result Sets(MARS): No
Mirror Server: 
Translate Character Data: Yes
Log Long Running Queries: No
Log Driver Statistics: No
Use Regional Settings: No
Use ANSI Quoted Identifiers: Yes
Use ANSI Null, Paddings and Warnings: Yes

R 代码:

R> ch <- odbcConnect("c2g")
R> sqlSave(ch, zinq_scores, tablename = "[bi_sandbox].[dbo].[table1]",
        append= FALSE, rownames= FALSE, colnames= FALSE)
Error in sqlColumns(channel, tablename) : 
  ‘[bi_sandbox].[dbo].[table1]’: table not found on channel

# after error, try again:
R> sqlDrop(ch, "[bi_sandbox].[dbo].[table1]", errors = FALSE)
R> sqlSave(ch, zinq_scores, tablename = "[bi_sandbox].[dbo].[table1]",
        append= FALSE, rownames= FALSE, colnames= FALSE)
Error in sqlSave(ch, zinq_scores, tablename = "[bi_sandbox].[dbo].[table1]",  : 
  42S01 2714 [Microsoft][SQL Server Native Client 11.0][SQL Server]There is already an object named 'table1' in the database.
[RODBC] ERROR: Could not SQLExecDirect 'CREATE TABLE [bi_sandbox].[dbo].[table1]  ("credibility_review" float, "creditbuilder" float, "no_product" float, "duns" varchar(255), "pos_credrev" varchar(5), "pos_credbuild" varchar(5))'

过去,我通过逐行运行效率极低的sqlQueryinsert into 来解决这个问题。但是我这次试了一下,没有写入数据。尽管sqlQuery 语句没有错误或警告消息。

temp <-"INSERT INTO [bi_sandbox].[dbo].[table1] 
+   (credibility_review, creditbuilder,  no_product, duns, pos_credrev, pos_credbuild) VALUES ("
> 
> for(i in 1:nrow(zinq_scores)) { 
+   sqlQuery(ch, paste(temp, "'", zinq_scores[i, 1], "'",",", " ", 
+                         "'", zinq_scores[i, 2], "'", ",",
+                         "'", zinq_scores[i, 3], "'", ",", 
+                         "'", zinq_scores[i, 4], "'", ",",
+                         "'", zinq_scores[i, 5], "'", ",", 
+                         "'", zinq_scores[i, 6], "'", ")"))
+ }
> str(sqlQuery(ch, "select * from [bi_sandbox].[dbo].[table1]"))
'data.frame':   0 obs. of  6 variables:
 $ credibility_review: chr 
 $ creditbuilder     : chr 
 $ no_product        : chr 
 $ duns              : chr 
 $ pos_credrev       : chr 
 $ pos_credbuild     : chr

任何帮助将不胜感激。
另外,如果有任何遗漏的细节,请告诉我,我会修改问题。 p>

【问题讨论】:

  • 我也遇到过类似的问题。我已经读到列映射必须完全正确,或者任何非 varchar 数据类型都需要输入数据框as.factor(x)。我刚刚通过 sqlQuery 从 CSV 批量插入,所以我也很想看到一个解决方案。
  • 就个人而言,我从不使用sqlSavesqlUpdate,而是在SQL Server Management Studio 中编写存储过程。然后我使用sqlQuery 执行存储过程。它似乎表现良好,减少了混乱的 R 代码,而且我的 IT 安全团队非常喜欢存储过程而不是 SQL 注入。
  • @Benjamin 您能否提供此过程的示例作为答案?我认为这对我自己和其他人来说都是一个有用的解决方案。
  • @Alex,小心你的要求——请看下面我的回答:)

标签: sql-server r rodbc


【解决方案1】:

我很抱歉。这不完全是一个“简单的例子”。这很简单,但是有很多部分。到最后,你可能会认为我这样做很疯狂。

从 SQL Server Management Studio 开始

首先,我在 SQL Server 上创建了一个名为 mtcars 的数据库,默认架构为 dbo。我也将自己添加为用户。在我自己的用户名下,我是数据库所有者,所以我可以对数据库做任何我想做的事情,但在 R 中,我将使用只有 EXECUTE 权限的通用帐户进行连接。

我们要写入的数据库中的预定义表称为mtcars。 (所以表格的完整路径是mtcars.dbo.mtcars;我知道它很懒)。定义表的代码是

USE [mtcars]
GO

/****** Object:  Table [dbo].[mtcars]    Script Date: 2/22/2016 11:56:53 AM ******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[mtcars](
    [OID] [int] IDENTITY(1,1) NOT NULL,
    [mpg] [numeric](18, 0) NULL,
    [cyl] [numeric](18, 0) NULL,
    [disp] [numeric](18, 0) NULL,
    [hp] [numeric](18, 0) NULL
) ON [PRIMARY]

GO

存储过程

我将使用两个存储过程。第一个是“UPSERT”过程,它将首先尝试更新表中的一行。如果失败,它将将该行插入到表中。

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

CREATE PROCEDURE dbo.sample_procedure
    @OID int = 0,
    @mpg numeric(18,0) = 0,
    @cyl numeric(18,0) = 0,
    @disp numeric(18,0) = 0,
    @hp numeric(18,0) = 0
AS
BEGIN
    -- SET NOCOUNT ON added to prevent extra result sets from
    -- interfering with SELECT statements.
    SET NOCOUNT ON;

    -- TRANSACTION code borrowed from
    -- http://stackoverflow.com/a/21209131/1017276

    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
    BEGIN TRANSACTION;

    UPDATE dbo.mtcars
        SET mpg = @mpg,
            cyl = @cyl,
            disp = @disp,
            hp = @hp
    WHERE OID = @OID;

    IF @@ROWCOUNT = 0
    BEGIN
    INSERT dbo.mtcars (mpg, cyl, disp, hp) 
        VALUES (@mpg, @cyl, @disp, @hp)
    END
    COMMIT TRANSACTION;

END
GO

我将使用的另一个存储过程与RODBC::sqlFetch 等效。据我所知,sqlFetch 依赖于 SQL 注入,我不允许使用它。为了保证我们的数据安全政策的安全,我写了这样的小程序(这里的数据安全性非常严格,您可能需要也可能不需要)

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

CREATE PROCEDURE dbo.get_mtcars
AS
BEGIN
    -- SET NOCOUNT ON added to prevent extra result sets from
    -- interfering with SELECT statements.
    SET NOCOUNT ON;

    SELECT * FROM dbo.mtcars    
END
GO

现在,从 R

我有一个实用函数来帮助我管理将数据输入到存储过程中。 sqlSave 会自动做很多这样的事情,所以我有点重新发明轮子。实用函数的要点是确定我推送到数据库的值是否需要嵌套在引号中。

#* Utility function.  This does a couple helpful things like
#*   Convert NA and NULL into a SQL NULL
#*   wrap character strings and dates in single quotes
sqlNullString <- function(value, numeric=FALSE)
{
  if (is.null(value)) value <- "NULL"
  if (is.na(value)) value <- "NULL"
  if (inherits(value, "Date")) value <- format(x = value, format = "%Y-%m-%d")
  if (value == "NULL") return(value) 
  else if (numeric) return(value)
  else return(paste0("'", value, "'"))
}

这一步并不是绝对必要的,但我将这样做只是为了让我的 R 表与我的 SQL 表相似。这是我的组织策略。

mtcars$OID <- NA

现在让我们建立我们的连接:

server <- "[server_name]"
uid <- "[generic_user_name]"
pwd <- "[password]"

library(RODBC)
channel <- odbcDriverConnect(paste0("driver=SQL Server;",
                              "server=", server, ";",
                              "database=mtcars;",
                              "uid=", uid, ";",
                              "pwd=", pwd))

现在下一部分是纯粹的懒惰。我将使用for 循环将数据帧的每一行一次推送到SQL 表中。正如原始问题中所述,这有点低效。我确信我可以编写一个存储过程来接受多个数据向量,将它们编译成一个临时表,并在 SQL 中执行 UPSERT,但是当我这样做时我不使用大型数据集,所以编写这样一个程序对我来说还不值得。相反,我更喜欢坚持使用我有限的 SQL 技能更容易推理的代码。

这里,我们只推送mtcars的前5行

#* Insert the first 5 rows into the SQL Table
for (i in 1:5)
{
  sqlQuery(channel = channel,
           query = paste0("EXECUTE dbo.sample_procedure ",
                          "@OID = ", sqlNullString(mtcars$OID[i]), ", ",
                          "@mpg = ", mtcars$mpg[i], ", ",
                          "@cyl = ", mtcars$cyl[i], ", ",
                          "@disp = ", mtcars$disp[i], ", ",
                          "@hp = ", mtcars$hp[i]))
}

现在我们来看看 SQL 中的表

sqlQuery(channel = channel,
         query = "EXECUTE dbo.get_mtcars")

下一行只是为了说明目的而匹配 R 和 SQL 中的 OID。通常,我会手动执行此操作。

mtcars$OID[1:5] <- 1:5

下一个 for 循环将 UPSERT 所有 32 行。我们已经有 5 个,我们正在 UPSERTing 32,如果我们正确完成,最后的 SQL 表应该有 32。 (即SQL会识别已经存在的5行)

#* Update/Insert (UPSERT) the entire table
for (i in 1:nrow(mtcars))
{
  sqlQuery(channel = channel,
           query = paste0("EXECUTE dbo.sample_procedure ",
                          "@OID = ", sqlNullString(mtcars$OID[i]), ", ",
                          "@mpg = ", mtcars$mpg[i], ", ",
                          "@cyl = ", mtcars$cyl[i], ", ",
                          "@disp = ", mtcars$disp[i], ", ",
                          "@hp = ", mtcars$hp[i]))
}


#* Notice that the first 5 rows were unchanged (though they would have changed 
#*  if we had changed the data...the point being that the stored procedure
#*  correctly identified that these records already existed)
sqlQuery(channel = channel,
         query = "EXECUTE dbo.get_mtcars")

回顾

存储过程方法的一个主要缺点是它公然重新发明轮子。它还要求您学习 SQL。对于简单的任务,SQL 很容易学习,但我为更复杂的任务编写的一些代码很难解释。我的一些程序花了我一天的大部分时间来做对。 (然而,一旦它们完成,它们就会工作得非常好)

存储过程的另一个大缺点是,我注意到,它确实需要更多的代码工作和组织。我想说,与仅使用 SQL 注入相比,代码工作和文档可能多出 10%。

存储过程方法的主要优点是

  1. 您可以灵活地做自己想做的事
  2. 您可以将您的 SQL 代码存储到数据库中,而不会用大量的 SQL 代码字符串污染您的 R 代码
  3. 避免 SQL 注入(同样,这是一个数据安全问题,根据您雇主的政策可能不是问题。我严格禁止使用 SQL 注入,因此存储过程是我唯一的选择)

还应该注意的是,我还没有探索在我的存储过程中使用表值参数,这对我来说可能会简化一些事情。

【讨论】:

  • 谢谢!我将与我们的数据工程师一起按照这些思路实施一些东西。
  • 这是一个我希望我在回答之前了解更多的答案案例。我建议查看 RODBCext 包,它是运行参数化查询的 sqlExecute 函数。更安全,并为您完成大量准备工作。
【解决方案2】:

过去,我通过运行效率极低的 sqlQuery 并逐行插入来解决这个问题。但是我这次试了一下,没有写入数据。虽然 sqlQuery 语句没有错误或警告消息。

昨天面对它:就我而言,问题在于方案。该表实际上是在我的用户自己的方案中创建的。

第一次你可以创建它并且你有这个错误(那个对象已经存在) 经过调查,我发现某些包无法与方案一起正常工作。

最后我使用了“逐行插入”解决方案。解决方案可用herehere

【讨论】:

猜你喜欢
  • 2014-07-17
  • 2011-10-18
  • 1970-01-01
  • 1970-01-01
  • 2016-07-12
  • 2017-06-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多