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?
For part 1, "field that is NOT NULL with a DEFAULT", you should not be sending the field in the first place. It should not be mapped. There is no need to change that field to accept NULLs just for this.
For part 2, "field that is NULL with a DEFAULT", that will work to get the default value when passing in DbNull.Value, as long as you don't have the SqlBulkCopyOptions set to
KeepNulls, else it will insert an actual databaseNULL.Since there is some confusion about the SqlBulkCopyOption of
KeepNulls, let's look at its definition:This means that a DataColumn set to
DbNull.Valuewill be inserted as a databaseNULL, even if the column has a DEFAULT CONSTRAINT, if theKeepNullsoption is specified. It is not specified in your code. Which leads to the second part that saysDbNull.Valuevalues are replaced by "default values" where applicable. Here "applicable" means that the column has a DEFAULT CONSTRAINT defined on it. Hence, when a DEFAULT CONSTRAINT exists, a non-DbNull.Valuevalue will be sent in as is whileDbNull.Valueshould translate to the SQL keywordDEFAULT. This keyword is interpreted in an INSERT statement as taking the value of the DEFAULT constraint. Of course, it is also possible thatSqlBulkCopy, if issuing individual INSERT statements, could simply leave that field out of the column list if set to NULL for that row, which would pick up the default value. In either case, the end result is that it works as you expected. And my testing shows that it does indeed work in this manner.To be clear about the distinction:
If a field in the database is set to
NOT NULLand has a DEFAULT CONSTRAINT defined on it, your options are:Pass in the field (i.e. it will not pick up the DEFAULT value), in which case it can never be set to
DbNull.ValueDo not pass in the field at all (i.e. it will pick up the DEFAULT value), which can be accomplished by either:
Do not have it in the DataTable or query or DataReader or whatever is being sent in as the source, in which case you might not need to specify the
ColumnMappingscollection at allIf the field is in the source, then you must specify the
ColumnMappingscollection so that you can leave that field out of the mappings.Setting, or not setting,
KeepNullsdoes not change the above noted behavior.If a field in the database is set to
NULLand has a DEFAULT CONSTRAINT defined on it, your options are:Do not pass in the field at all (i.e. it will pick up the DEFAULT value), which can be accomplished by either:
Do not have it in the DataTable or query or DataReader or whatever is being sent in as the source, in which case you might not need to specify the
ColumnMappingscollection at allIf the field is in the source, then you must specify the
ColumnMappingscollection so that you can leave that field out of the mappings.Pass in the field set to a value that is not
DbNull.Value, in which case it will be set to this value and not pick up the DEFAULT valuePass in the field as
DbNull.Value, in which case the effect is determined by whether or notSqlBulkCopyOptionsis being passed in and has been set toKeepNulls:KeepNullsis not set will pick up the DEFAULT valueKeepNullsis set will leave the field set toNULLHere is a simple test to see how the
DEFAULTkeyword works:Results: