当源DataTable行具有DBNull.Value时,SqlBulkCopy进入表,默认列值失败

更新: 这是我的解决方案

我有一个表定义为:

CREATE TABLE [dbo].[csvrf_References] ( [Ident] [int] IDENTITY(1,1) NOT NULL, [ReferenceID] [uniqueidentifier] NOT NULL DEFAULT (newsequentialid()), [Type] [nvarchar](255) NOT NULL, [Location] [nvarchar](1000) NULL, [Description] [nvarchar](2000) NULL, [CreatedOn] [datetime] NOT NULL DEFAULT (getdate()), [LastUpdatedOn] [datetime] NOT NULL DEFAULT (getdate()), [LastUpdatedUser] [nvarchar](100) NOT NULL DEFAULT (suser_sname()), CONSTRAINT [PK_References] PRIMARY KEY NONCLUSTERED ([ReferenceID] ASC) ) ON [PRIMARY] 

我有一个DataTable ,其列与表列名和数据类型相匹配。 DataTable使用CreatedOnLastUpdatedOnLastUpdatedUser DBNull.Value填充。 ReferenceID已生成。 当我调用以下代码时,我得到以下错误。

码:

 SqlBulkCopy bulkCopy = new SqlBulkCopy(conn, SqlBulkCopyOptions.Default, bulkCopyTran); bulkCopy.DestinationTableName = table.TableName; bulkCopy.ColumnMappings.Clear(); foreach (DataColumn col in table.Columns) bulkCopy.ColumnMappings.Add(col.ColumnName, col.ColumnName); bulkCopy.WriteToServer(table); 

错误:

尝试BulkCopy表csvrf_References时出错
System.InvalidOperationException:列’CreatedOn’不允许DBNull.Value。
at System.Data.SqlClient.SqlBulkCopy.ConvertValue(Object value,_SqlMetaData metadata,Boolean isNull,Boolean&isSqlType,Boolean&coercedToDataFeed)

我看了一遍,似乎无法找到答案。 SqlBulkCopy类似乎不尊重默认值,即使它说它也是如此。 我在这做错了什么?

对于第1部分,“带有DEFAULT的非NULL字段”,您不应该首先发送字段。 它不应该映射。 没有必要为此更改该字段以接受NULL。

对于第2部分,“带有DEFAULT为NULL的字段”,只要您没有将SqlBulkCopyOptions设置为KeepNulls ,它将在传递DbNull.Value时获取默认值,否则它将插入实际数据库NULL

由于对KeepNulls的SqlBulkCopyOption存在一些疑惑,让我们来看看它的定义:

无论默认值的设置如何,都保留目标表中的空值。 未指定时,null值将替换为适用的默认值。

这意味着设置为DbNull.Value的DataColumn将作为数据库NULL插入,即使该列具有DEFAULT CONSTRAINT, 如果指定了KeepNulls选项。 它没有在您的代码中指定。 这导致第二部分说DbNull.Value值被适用的“默认值”替换。 这里“适用”表示该列上定义了DEFAULT CONSTRAINT。 因此,当存在DEFAULT CONSTRAINT时, DbNull.Value发送非DbNull.Value值,而DbNull.Value 转换为SQL关键字DEFAULT 。 此关键字在INSERT语句中被解释为获取DEFAULT约束的值。 当然,如果发出单独的INSERT语句, SqlBulkCopy也可能只是将该字段留出列列表(如果为该行设置为NULL),这将获取默认值。 在任何一种情况下,最终结果是它按预期工作。 我的测试表明它确实以这种方式工作。

要清楚区别:

  • 如果数据库中的字段设置为NOT NULL并且在其上定义了DEFAULT CONSTRAINT,则您的选项为:

    • DbNull.Value字段(即它不会获取DEFAULT值),在这种情况下,它永远不能设置为DbNull.Value

    • 根本不要传入字段(即它将获取DEFAULT值),这可以通过以下任一方式完成:

      • 不要在DataTable或查询或DataReader中或者作为源发送的任何内容中使用它,在这种情况下,您可能根本不需要指定ColumnMappings集合

      • 如果该字段位于源中,则必须指定ColumnMappings集合,以便可以将该字段保留在映射之外。

    • 设置或不设置KeepNulls不会改变上述行为。

  • 如果数据库中的字段设置为NULL并且在其上定义了DEFAULT CONSTRAINT,则您的选项为:

    • 根本不要传入字段(即它将获取DEFAULT值),这可以通过以下任一方式完成:

      • 不要在DataTable或查询或DataReader中或者作为源发送的任何内容中使用它,在这种情况下,您可能根本不需要指定ColumnMappings集合

      • 如果该字段位于源中,则必须指定ColumnMappings集合,以便可以将该字段保留在映射之外。

    • DbNull.Value设置为不是DbNull.Value的值的字段,在这种情况下,它将被设置为此值而不会获取DEFAULT值

    • 在字段中传递为DbNull.Value ,在这种情况下,效果取决于是否传入SqlBulkCopyOptions并已设置为KeepNulls

      • 设置KeepNulls将获取DEFAULT值

      • 设置KeepNulls将字段设置为NULL


这是一个简单的测试,用于查看DEFAULT关键字的工作原理:

 --DROP TABLE ##DefaultTest; CREATE TABLE ##DefaultTest ( Col1 INT, [CreatedOn] [datetime] NOT NULL DEFAULT (GETDATE()), [LastUpdatedOn] [datetime] NULL DEFAULT (GETDATE()) ); INSERT INTO ##DefaultTest (Col1, CreatedOn) VALUES (1, DEFAULT); INSERT INTO ##DefaultTest (Col1, LastUpdatedOn) VALUES (2, DEFAULT); INSERT INTO ##DefaultTest (Col1, LastUpdatedOn) VALUES (3, NULL); INSERT INTO ##DefaultTest (Col1, LastUpdatedOn) VALUES (4, '3333-11-22'); SELECT * FROM ##DefaultTest ORDER BY Col1 ASC; 

结果:

 Col1 CreatedOn LastUpdatedOn 1 2014-11-20 12:34:31.610 2014-11-20 12:34:31.610 2 2014-11-20 12:34:31.610 2014-11-20 12:34:31.610 3 2014-11-20 12:34:31.610 NULL 4 2014-11-20 12:34:31.613 3333-11-22 00:00:00.000 

阅读有关SqlBulkCopy的文档,特别是SqlBulkCopyOptions ,我会得出你所做的相同结论:SQL Server应该“足够”以便在适用的地方使用默认约束,特别是因为你没有使用SqlBulkCopyOptions.KeepNulls属性。

但是,在这种情况下,我怀疑文档是微妙的错误; 如果不是错误的话肯定会产生误导。

正如您所观察到的,对于具有默认约束的非可空字段(在本例中为GetDate() ),SqlBulkCopy会因上述错误而失败。

作为测试,尝试创建第二个模仿第一个表的表,但这次使CreatedOnLastUpdatedOn字段可以为空。 在我的测试中,使用默认选项( SqlBulkCopyOptions.Default ),该过程无错误地工作, 并且 CreatedOnLastUpdatedOn都在表中填充了正确的DateTime值,尽管DataTable的值为DBNull.Value

作为另一个测试,使用相同(可空字段)表,仅执行SqlBulkCopy,这次使用SqlBulkCopyOptions.KeepNulls属性。 我怀疑你会看到我所做的相同结果,即CreatedOnLastUpdatedOn在表中都是null。

此行为类似于执行“vanilla”T-SQL语句以将数据插入表中。

如果执行,使用原始表(不可空字段)作为示例

 INSERT INTO csvrf_References ([Type], [Location], [Description], [CreatedOn], [LastUpdatedOn], [LastUpdatedUser]) VALUES ('test', 'test', 'test', null, null, null) 

您将收到有关表中不允许的空值的类似错误。

但是,如果从语句中省略不可为空的字段,SQL Server将对这些字段使用默认约束:

 INSERT INTO csvrf_References ([Type], [Location], [Description] VALUES ('test', 'test', 'still testing') 

基于此,我建议在表中使字段可以为空(在我看来并不是一个很好的选项)或者为SqlBulkCopy进程使用“staging”表(其中字段可以为空并且具有类似的默认约束) )。 一旦数据在临时表中,执行第二个语句将数据移动到实际的最终目标表中。

“SQLBulkCopy列不允许DbNull.value”错误是由于源表和目标表具有不同的列顺序。