RODBC::sqlSave - problems creating/appending to a table

橙三吉。 提交于 2019-12-13 19:16:10

问题


Related to several other questions on the RODBC package, I'm having problems using RODBC::sqlSave to write to a table on a SQL Server database. I'm using MS SQL Server 2008 and 64-bit R on a Windows RDP.

The solution in the 3rd link (questions) does work [sqlSave(ch, df)]. But in this case, it writes to the wrong data base. That is, my default DB is "C2G" but I want to write to "BI_Sandbox". And it doesn't allow for options such as rownames, etc. So there still seems to be a problem in the package.

Obviously, a possible solution would be to change my ODBC solution to the specified database, but it seems there should be a better method. And this wouldn't solve the problem of unusable parameters in the sqlSave command--such as rownames, varTypes, etc.

I have the following ODBC- System DSN connnection:

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 code:

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))'

In the past, I've gotten around this by running the supremely inefficient sqlQuery with insert into row-by-row to get around this. But I tried this time and no data was written. Although the sqlQuery statement did not have an error or warning message.

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

Any help would be greatly appreciated.
Also, if there is any missing detail, please let me know and I'll edit the question.


回答1:


My apologies up front. This is not exactly a "simple example." It's pretty trivial, but there are a lot of parts. And by the end, you'll probably think I'm crazy for doing it this way.

Starting in SQL Server Management Studio

First, I've created a database on SQL Server called mtcars with default schema dbo. I've also added myself as a user. Under my own user name, I am the database owner, so I can do anything I want to the database, but from R, I will connect using a generic account that only has EXECUTE privileges.

The predefined table in the database that we are going to write to is called mtcars. (So the full path to the table is mtcars.dbo.mtcars; it's lazy, I know). The code to define the table is

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

Stored Procedures

I'm going to use two stored procedures. The first is an "UPSERT" procedure, that will first try to update a row in a table. If that fails, it will insert the row into the table.

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

Another stored procedure I will use is just the equivalent of RODBC::sqlFetch. As far as I can tell, sqlFetch depends on SQL injection, and I'm not allowed to use it. Just to be on the safe side of our data security policies, I write little procedures like this (Data security is pretty tight here, you may or may not need this)

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

Now, from R

I have a utility function I use to help me manage inputting data into the stored procedures. sqlSave would do a lot of this automatically, so I'm kind of reinventing the wheel. The gist of the utility function is to determine if the value I'm pushing to the database needs to be nested in quotes or not.

#* 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, "'"))
}

This next step isn't strictly necessary, but I'm going to do it just so that my R table is similar to my SQL table. This is organizational strategy on my part.

mtcars$OID <- NA

Now let's establish our connection:

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))

Now this next part is pure laziness. I'm going to use a for loop to push each row of the data frame the to SQL table one at a time. As noted in the original question, this is kind of inefficient. I'm sure I could write a stored procedure to accept several vectors of data, compile them into a temporary table, and do the UPSERT in SQL, but I don't work with large data sets when I'm doing this, and so it hasn't yet been worth it to me to write such a procedure. Instead, I prefer to stick with the code that is a little easier for me to reason with on my limited SQL skills.

Here, we're just going to push the first 5 rows of mtcars

#* 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]))
}

And now we'll take a look at the table from SQL

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

This next line is just to match up the OIDs in R and SQL for illustration purposes. Normally, I would do this manually.

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

This next for loop will UPSERT all 32 rows. We already have 5, we're UPSERTing 32, and the SQL table at the end should have 32 if we've done it correctly. (That is, SQL will recognize the 5 rows that already exist)

#* 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")

Recap

The stored procedure approach has a major disadvantage in that it is blatantly reinventing the wheel. It also requires that you learn SQL. SQL is pretty easy to learn for simple tasks, but some of the code I've written for more complex tasks is pretty difficult to interpret. Some of my procedures have taken me the better part of a day to get right. (once they are done, however, they work incredibly well)

The other big disadvantage to the stored procedure is, I've noticed, it does require a little bit more code work and organization. I'd say it's probably been about 10% more code work and documentation than if I were just using SQL Injection.

The chief advantages of the stored procedures approach are

  1. you have massive flexibility for what you want to do
  2. You can store your SQL code into the database and not pollute your R code with potentially huge strings of SQL code
  3. Avoiding SQL injection (again, this is a data security thing, and may not be an issue depending on your employer's policies. I'm strictly forbidden from using SQL injection, so stored procedures are my only option)

It should also be noted that I've not yet explored using Table-Valued parameters in my stored procedures, which might simplify things for me a bit.



来源:https://stackoverflow.com/questions/35512259/rodbcsqlsave-problems-creating-appending-to-a-table

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!