7

Update: Here is my solution

I have a table defined as:

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]

I have a DataTable with columns that match the table column names and data types. The DataTable is filled out with DBNull.Value in CreatedOn, LastUpdatedOn and LastUpdatedUser. ReferenceID is already generated. When I call the following code I get the error below.

Code:

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

Error:

Error trying to BulkCopy table csvrf_References
System.InvalidOperationException: Column 'CreatedOn' does not allow DBNull.Value.
at System.Data.SqlClient.SqlBulkCopy.ConvertValue(Object value, _SqlMetaData metadata, Boolean isNull, Boolean& isSqlType, Boolean& coercedToDataFeed)

I have looked all over and I can't seem to find an answer for this. The SqlBulkCopy class seems not to honor default values even though it says it does. What am I doing wrong here?

4

3 回答 3

15

对于第 1 部分,“具有默认值的非 NULL 字段”,您不应该首先发送该字段。它不应该被映射。无需为此更改该字段以接受 NULL。

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

由于对 的 SqlBulkCopyOption 存在一些混淆KeepNulls,我们来看看它的定义:

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

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

要清楚区别:

  • 如果数据库中的字段设置为NOT NULL并定义了默认约束,则您的选项是:

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

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

      • 不要将它放在 DataTable 或 query 或 DataReader 或任何作为源发送的东西中,在这种情况下,您可能根本不需要指定ColumnMappings集合

      • 如果该字段在源中,那么您必须指定ColumnMappings集合,以便您可以将该字段排除在映射之外。

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

  • 如果数据库中的字段设置为NULL并定义了默认约束,则您的选项是:

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

      • 不要将它放在 DataTable 或 query 或 DataReader 或任何作为源发送的东西中,在这种情况下,您可能根本不需要指定ColumnMappings集合

      • 如果该字段在源中,那么您必须指定ColumnMappings集合,以便您可以将该字段排除在映射之外。

    • 将字段设置为 not 的值DbNull.Value,在这种情况下,它将设置为该值并且不获取 DEFAULT 值

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

      • KeepNulls未设置将获取默认

      • 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
于 2014-11-20T16:39:44.353 回答
2

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

于 2016-08-30T07:09:52.517 回答
0

阅读有关 的文档SqlBulkCopy,特别是SqlBulkCopyOptions,我会得出与您所做的相同的结论:SQL Server 应该足够“智能”以在适用的情况下使用默认约束,尤其是因为您没有使用该SqlBulkCopyOptions.KeepNulls属性。

但是,在这种情况下,我怀疑文档有点不正确;如果不正确,那肯定是误导。

正如您所观察到的,对于具有默认约束的不可空字段(在这种情况下GetDate()),SqlBulkCopy 会因上述错误而失败。

作为测试,尝试创建第二个表来模仿第一个表,但这一次使CreatedOnandLastUpdatedOn字段可以为空。在我的SqlBulkCopyOptions.Default测试 CreatedOn,使用默认选项(LastUpdatedOnDBNull.Value

作为另一个测试,使用相同的(可为空字段)表,执行 SqlBulkCopy 仅这次使用该SqlBulkCopyOptions.KeepNulls属性。我怀疑您会看到与我所做的相同的结果,也就是说,CreatedOn并且LastUpdatedOn在表中都为空。

此行为类似于执行“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 进程使用“暂存”表(其中字段可为空并且具有类似的默认约束) )。一旦数据在临时表中,执行第二条语句将数据移动到实际的最终目标表中。

于 2014-11-19T21:25:03.200 回答