RODBC :: sqlSave - 创建/附加到表的问题

时间:2016-02-19 18:23:35

标签: sql-server r rodbc

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

第3个链接中的解决方案(问题)确实有效[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

非常感谢任何帮助。
此外,如果有任何遗漏的细节,请告诉我,我会修改问题。

2 个答案:

答案 0 :(得分:2)

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

从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注入,因此存储过程是我唯一的选择)
  4. 还应该注意的是,我还没有在我的存储过程中探索使用Table-Valued参数,这可能会为我简化一些事情。

答案 1 :(得分:0)

过去,我通过运行效率最低的sqlQuery并逐行插入来解决此问题,从而解决了这个问题。但是我这次尝试了,没有数据被写入。尽管sqlQuery语句没有错误或警告消息。

昨天遇到了:在我的情况下,问题出在方案上。该表实际上是创建的,但使用我自己的方案。

第一次创建它,然后出现此错误(该对象已存在) 在调查之后,我发现某些软件包不能与方案一起正常工作。

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