列不允许使用DBNull.Value-不允许KeepNulls-正确的列映射

时间:2019-01-09 17:55:38

标签: c# sql-server datatable

我将C#与.NET 4.5.2一起使用,并推送到SQL Server 2017 14.0.1000.169

在我的数据库中,我有一个带有DateAdded字段的表,类型为DateTimeOffset

我正在尝试使用以下代码批量复制:

private Maybe BulkCopy(SqlSchemaTable table, System.Data.DataTable dt, bool identityInsertOn)
{
    try
    {
        var options = SqlBulkCopyOptions.TableLock | SqlBulkCopyOptions.FireTriggers | SqlBulkCopyOptions.UseInternalTransaction; //  | SqlBulkCopyOptions.CheckConstraints; // Tried CheckConstraints, but it didn't change anything.
        if (identityInsertOn) options |= SqlBulkCopyOptions.KeepIdentity;
        using (var conn = new SqlConnection(_connString))
        using (var bulkCopy = new SqlBulkCopy(conn, options, null))
        {
            bulkCopy.DestinationTableName = table.TableName;
            dt.Columns.Cast<System.Data.DataColumn>().ToList()
                .ForEach(x => bulkCopy.ColumnMappings.Add(new SqlBulkCopyColumnMapping(x.ColumnName, x.ColumnName)));

            try
            {
                conn.Open();
                bulkCopy.WriteToServer(dt);
            }
            catch (Exception ex)
            {
                return Maybe.Failure(ex);
            }
        }
    }
    catch (Exception ex)
    {
        return Maybe.Failure(ex);
    }

    return Maybe.Success();
}

我知道的does not allow DBNull错误的两个可能原因是:

  1. 列的顺序错误,可以通过按与数据库序号相同的顺序或通过执行列映射来解决。
  2. 启用了KeepNulls,并在数据表中设置了DBNull.Value(或null?)。

但是我映射正确,从来没有设置KeepNulls。

但是我收到错误:

  

列DateAdded不允许使用DBNull.Value

编辑,我也只是尝试不设置任何内容,包括nullDBNull.ValueDefaultValue ...根本就没有设置该列。

此外,如果我从DataTable中删除DateAdded列,则它可以正常工作。但是我不想要那个。在100,000条记录中,可能有20条具有数据。因此,在我的500个批次中,有时“ DateAdded”字段中没有数据,有时一两个数据中的数据。

所以我想将该列保留在DataTable中,但让它使用DefaultValue。

最后一点:我在将DataColumn的值设置为DBNull.Valuedt.Columns[x.ColumnName].DefaultValue之间进行了交替。两种方式都会产生相同的错误。

编辑2

这是我用来填充数据表中数据的代码:

foreach (var column in table)
{
    System.Data.DataRow newRow = dt.NewRow();
    foreach (var field in column)
    {
        if (!IsNull(field.Value) && !IsEmptyDateOrNumber(field.ColumnType, field.Value))
        {
            // For DateAdded, this is not hit on the first batch, though there are columns Before and After DateAdded on the same row which do have value.
            // But it WILL be hit once or twice a few batches later.  So I don't want to completely remove the definition from the DataTable.
            newRow[field.ColumnName] = field.Value;
        }
        else
        {
            // newRow[field.ColumnName] = dt.Columns[field.ColumnName].DefaultValue;
            // newRow[field.ColumnName] = DBNull.Value;
            // dt.Columns[field.ColumnName].AllowDBNull = true;
        }
    }
    dt.Rows.Add(newRow);
}

IsNull()返回TRUE,如果值是null或字符串"null"(根据我的业务要求)。

如果该字段是数字或日期类型,并且值为空或空IsEmptyDateOrNumber(),则

TRUE将返回""。因为虽然empty对许多类似字符串的字段都有效,但它永远不是有效的数值。

为该字段分配值的条件恰好在该特定列的时间的 0%内达到。因此什么也没设置。

3 个答案:

答案 0 :(得分:2)

简单地说,您不能做自己想做的。关于BulkCopy如何使用默认值的最佳参考是Rutzky的This Answer

问题在于,BulkCopy包含一个查询目标数据库并确定表结构的步骤。如果它确定目标列可以NOT NULL,并且您要传递null或DBNull,它甚至在尝试传递数据之前都会引发异常。

如果使用SQL事件探查器,则将看到BCP调用,但看不到数据(无论如何都不会显示数据)。您将看到的是定义列列表和标志的调用。

当BulkCopy最终决定传递数据时。如果该列存在,并且该字段是NULL字段,并且值是DBNull.Value,并且该列具有默认值;批量复制本质上传递了该列的DEFAULT标志。但是做出了一些决定,使得那些条件(除非该字段是NOT NULL可以使用)不应该使用默认值,而应该引发异常。

据我所知这是Microsoft的错误或疏忽。

与其他一些答案一样,常见的解决方法是通过计算值应在代码中手动处理这些值。当然,如果您计算默认值,则DBA会更改字段的实际SQL默认值,您的系统将不匹配。下一步是向您的系统中添加一个子系统,该子系统可以查询和/或跟踪/缓存您要查找的SQL Server中当前指定的默认值,并为其分配这些默认值。这比需要的工作还要多。

TLDR:您无法做您想做的事。但是还有其他人指定的次优解决方法。

答案 1 :(得分:1)

此组合不适用于SqlBulkCopy:

  1. SQL表中的日期字段配置为不为空。
  2. 您的C#数据表中的记录具有未设置的日期值(或具有设置的值,但它们不是实际的日期/时间值)。

此组合确实有效:

  1. SQL表中的日期字段配置为允许为空,但使用默认值。
  2. 您的C#数据表中的记录的日期值未设置(当未在C#数据表中指定日期字段时,SQL将使用默认值)。

如果必须将表中的日期字段配置为不接受空值,那么您还有更多工作要做(创建一个接受空值的临时表,将其批量插入该临时表,调用您编写的存储过程,该插入过程从临时表到常规表)。

我认为您要寻找的最简单的路径是:

您的表中的日期字段应允许为NULL,但默认值为:

CREATE TABLE [dbo].[MyTable](
    [Field1] [varchar](50) NULL,
    [MyDateField] [datetime] NULL,
) ON [PRIMARY]
GO

ALTER TABLE [dbo].[MyTable] ADD  CONSTRAINT [DF_Table_1_MyDateField] DEFAULT (getdate()) FOR [MyDateField]
GO

然后用C#:

        DataTable dt = new DataTable("dbo.MyTable");

        dt.Columns.Add("Field1");
        dt.Columns.Add("MyDateField");

        DataRow row1 = dt.NewRow();
        row1["Field1"] = "test row 1";
        row1["MyDateField"] = DateTime.Now; //specify a value for the date field in C#
        dt.Rows.Add(row1);

        DataRow row2 = dt.NewRow();
        row2["Field1"] = "test row 2";
        //do not specify a value for the date field - SQL will use the default value
        dt.Rows.Add(row2);

        do the bulk copy

答案 2 :(得分:1)

  

在100,000条记录中,可能有20条具有数据。所以在我的批次中   的500,有时在DateAdded字段中没有数据,有时一个   一两个有数据。

我猜您的某些记录在DateAdded中具有空值,而DateAdded配置为不限制空值。

要解决此问题,您可以采用多种方法:

  1. (更简单的一个)只是将DateAdded列更改为接受null。
  2. 将默认值分配给DateAdded列(来自MSSQL)。
  3. 从您的代码中管理记录中的空值。

如果您没有权限,或者您的工作要求指定DateAdded不能接受空值或具有MSSQL的默认值,或者1和2不能解决您的问题。 然后,可以在将每个批次的DateAdded列上的空值复制到服务器之前对其进行管理。

在这种情况下,在您的代码中,当您填充数据时,应在此列中添加如下条件:

if(field.ColumnName == "DateAdded")
{
    // Do something to handle this column if it's null 

    // Set a default value 
    DateTimeOffset dtoffset = DateTimeOffset.Parse("2019-01-01 00:00:00.0000000 -00:00", CultureInfo.InvariantCulture); // change it to the required offset 
    string defaultDateAdded = dtoffset.ToString("yyyy-MM-dd HH:mm:ss.fffffff zzz", CultureInfo.InvariantCulture);               

    // Add the default value 
    newRow[field.ColumnName] = defaultDateAdded;
}
else
{
    // Do something to handle the rest of columns if they're null 

}   

然后,当您完成所有列的处理后,只需将新行添加到数据表中,然后将最终的数据表复制到服务器即可。