当源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
使用CreatedOn
, LastUpdatedOn
和LastUpdatedUser
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会因上述错误而失败。
作为测试,尝试创建第二个模仿第一个表的表,但这次使CreatedOn
和LastUpdatedOn
字段可以为空。 在我的测试中,使用默认选项( SqlBulkCopyOptions.Default
),该过程无错误地工作, 并且 CreatedOn
和LastUpdatedOn
都在表中填充了正确的DateTime值,尽管DataTable的值为DBNull.Value
。
作为另一个测试,使用相同(可空字段)表,仅执行SqlBulkCopy,这次使用SqlBulkCopyOptions.KeepNulls
属性。 我怀疑你会看到我所做的相同结果,即CreatedOn
和LastUpdatedOn
在表中都是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”错误是由于源表和目标表具有不同的列顺序。