1

I'm trying to use SqlBulkCopy as a way of doing multiple INSERTs at once but for some reason, I'm getting a unique constraint violation when running WriteToServer(DataTable). The odd thing about this SqlException is it's saying that .

My table schema:

CREATE TABLE Product (
  ID   INT IDENTITY (1, 1) PRIMARY KEY,
  Name NVARCHAR(450) UNIQUE NOT NULL, -- Unique constraint being called
  BulkInsertID NCHAR(6) -- Column the constraint is being called on
);

The only reason I can think of as to why this is happening is because I mixed up the column names when assigning them inside the DataColumns but I checked them multiple times and I cannot find any issues with them.

Minimal, Complete and Verifiable Example:

class Program
{
    private static SqlConnection connection;
    private static string connectionURL = "Server=ASUS-X750JA\\DIRECTORY;Database=directory;Integrated Security=True;";
    private static Random _random = new Random();

    public static SqlConnection openConnection()
    {
        connection = new SqlConnection(connectionURL);
        connection.Open();
        Console.WriteLine("Opened connection to DB");
        return connection;
    }

    public static void closeConnection()
    {
        connection.Close();
        Console.WriteLine("Closed connection to DB");
    }

    static void Main(string[] args)
    {
        List<string> productNames = new List<string>();
        productNames.Add("Diamond");
        productNames.Add("Gold");
        productNames.Add("Silver");
        productNames.Add("Platinum");
        productNames.Add("Pearl");
        addProducts(productNames);
    }

    private static void addProducts(List<string> productNames)
    {
        const string tableName = "Product";
        DataTable table = new DataTable(tableName);

        string bulkInsertID;
        do
        {
            bulkInsertID = generateID();
        } while (isDuplicateBulkInsertID(tableName, bulkInsertID));

        DataColumn nameColumn = new DataColumn("Name");
        nameColumn.Unique = true;
        nameColumn.AllowDBNull = false;

        DataColumn bulkInsertIDColumn = new DataColumn("BulkInsertID");
        bulkInsertIDColumn.Unique = false;
        bulkInsertIDColumn.AllowDBNull = true;

        table.Columns.Add(nameColumn);
        table.Columns.Add(bulkInsertIDColumn);

        foreach (string productName in productNames)
        {
            DataRow row = table.NewRow();
            row[nameColumn] = productName;
            row[bulkInsertIDColumn] = bulkInsertID;
            table.Rows.Add(row);
        }

        using (SqlConnection connection = openConnection())
        {
            using (SqlBulkCopy bulkCopy = new SqlBulkCopy(connection))
            {
                bulkCopy.DestinationTableName = table.TableName;
                bulkCopy.WriteToServer(table);
            }
        }
    }

    /// <summary>
    /// Generates random 6-character string but it's not like GUID so may need to check for duplicates
    /// </summary>
    /// <returns></returns>
    public static string generateID()
    {
        char[] _base62chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".ToCharArray();
        int length = 6;

        var sb = new StringBuilder(length);

        for (int i = 0; i < length; i++)
            sb.Append(_base62chars[_random.Next(62)]);

        return sb.ToString();
    }

    public static bool isDuplicateBulkInsertID(string tableName, string bulkInsertID)
    {
        string query = string.Format("SELECT BulkInsertID FROM {0} WHERE BulkInsertID = @bulkinsertid", tableName);
        SqlCommand command = new SqlCommand(query, openConnection());
        SqlParameter bulkInsertIDParam = new SqlParameter("@bulkinsertid", SqlDbType.NChar, bulkInsertID.Length);
        bulkInsertIDParam.Value = bulkInsertID;

        command.Parameters.Add(bulkInsertIDParam);
        command.Prepare();
        Task<SqlDataReader> asyncTask = command.ExecuteReaderAsync();
        SqlDataReader reader = asyncTask.Result;
        bool isDuplicate = reader.HasRows;

        closeConnection();
        return isDuplicate;
    }
}

enter image description here

The unique constraint shown in the screenshot belongs to the Name column but the duplicate key value is being sent to the BulkInsertID column and I don't know why the error is being thrown.

EDIT: I just changed my schema to use uniqueidentifier as the bulkInsertID column and changed row[bulkInsertIDColumn] = bulkInsertID to row[bulkInsertIDColumn] = Guid.NewGuid().ToString(). When I reran my code, I found that the generated GUID ran but when I looked at the table, the GUID was in the name column. So I can conclude it's not a server issue but a problem in the program.

Community
  • 1
  • 1
silverAndroid
  • 960
  • 3
  • 11
  • 29
  • looks to me as if the error message is telling you exactly what the issue is ..have you tried debugging the code and seeing where you're adding the duplicate key value? – MethodMan Aug 12 '16 at 19:08
  • Yes, it's the bulkInsertID string variable which I already knew but what I don't understand why the `BulkInsertID` column would be saying the UNIQUE constraint is being violated even though that column doesn't have a UNIQUE constraint on it. – silverAndroid Aug 12 '16 at 19:10
  • "Every time you do new Random() it is initialized using the clock. This means that in a tight loop you get the same value lots of times. You should keep a single Random instance and keep using Next on the same instance." http://stackoverflow.com/a/768001/43846 – stuartd Aug 12 '16 at 19:22
  • @stuartd Not a duplicate because the `Random` object isn't causing the problem. – silverAndroid Aug 12 '16 at 19:39

2 Answers2

2

Because you have a identity column bulk insert is trying to insert nameColumn in to ID (and ignoring it because the column is a identity column) and bulkInsertIDColumn in to Name. Just add the following to your insert to tell it to go to the correct columns.

using (SqlBulkCopy bulkCopy = new SqlBulkCopy(connection))
{
    bulkCopy.ColumnMappings.Add("Name", "Name"); //NEW
    bulkCopy.ColumnMappings.Add("BulkInsertID", "BulkInsertID"); //NEW
    bulkCopy.DestinationTableName = table.TableName;
    bulkCopy.WriteToServer(table);
}

The other option is add a ID column to table and just don't put any values in it.

DataColumn idColumn = new DataColumn("ID");

DataColumn nameColumn = new DataColumn("Name");
//nameColumn.Unique = true; //SqlBulkCopy does not care about these settings.
//nameColumn.AllowDBNull = false;

DataColumn bulkInsertIDColumn = new DataColumn("BulkInsertID");
//bulkInsertIDColumn.Unique = false;
//bulkInsertIDColumn.AllowDBNull = true;

table.Columns.Add(ID);
table.Columns.Add(nameColumn);
table.Columns.Add(bulkInsertIDColumn);

foreach (string productName in productNames)
{
    DataRow row = table.NewRow();
    //We don't do anything with row[idColumn]
    row[nameColumn] = productName;
    row[bulkInsertIDColumn] = bulkInsertID;
    table.Rows.Add(row);
}
Scott Chamberlain
  • 124,994
  • 33
  • 282
  • 431
  • Thank you for the column mappings! I had seen them in some tutorials but I figured they weren't so important because I would be using the column name inside `DataColumn` – silverAndroid Aug 12 '16 at 20:08
  • Nah, if you don't specify anything SqlBulkInsert goes by column order. – Scott Chamberlain Aug 12 '16 at 20:09
  • 1
    @silverAndroid however, be aware like i mentioned in my deleted answer. If to copies of your program are running it is possible that two of them will generate the same `bulkInsertID` at the same time. If that is a issue for you, you may want to address it. – Scott Chamberlain Aug 12 '16 at 20:11
0

Looks like it's throwing UNIQUE constraint violation for column BulkInsertID though from posted table schema don't see it's marked with that constraint and in your code I see you have bulkInsertIDColumn.Unique = false;. Are you sure you are not setting it to true anywhere else.

BTW, to me looks it's throwing that exception cause you are trying to create a new instance of Random() in loop as seen below pointed code block

    do
    {
        bulkInsertID = generateID(); //calling method generateID
    } while (isDuplicateBulkInsertID(tableName, bulkInsertID));

Where as in generateID() you are creating new instance of Random class

public static string generateID()
{
   ........
    Random _random = new Random(); // creating new instance every time
Rahul
  • 76,197
  • 13
  • 71
  • 125
  • Moved the new Random object outside of the method (as seen in my edited question) but it's still throwing the exception and the only place `Unique` is set to true is when I set `nameColumn.Unique = true` – silverAndroid Aug 12 '16 at 19:27
  • @silverAndroid, consider using `GUID` instead for that `BulkInsertID` column. – Rahul Aug 12 '16 at 19:32
  • Why would I need to? `generateID()` is not the one causing issues. The reason I chose to generate an ID instead of using GUID is because of the performance issues that come along with using it. – silverAndroid Aug 12 '16 at 19:34
  • @silverAndroid *"The reason I chose to generate an ID instead of using GUID is because of the performance issues that come along with using it."*, what performance issues are you talking about? Your current method of generating a ID is going to have very poor performance due to the fact it has to hit the server over and over tried various id's to see if they are free. – Scott Chamberlain Aug 12 '16 at 19:50