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;
}
}
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.
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);
}
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
Related
I'm using C# and .NET Core 6. I want to bulk insert about 100 rows at once into database and get back their Ids from BigInt identity column.
I have tried lot of different variants, but still do not have the working solution. When I preview table variable, the Id column has DbNull value and not the newly inserted Id.
How to get Ids of newly inserted rows?
What I have:
SQL Server table:
CREATE TABLE dbo.ExamResult
(
Id bigint IDENTITY(1, 1) NOT NULL,
Caption nvarchar(1024) NULL,
SortOrder int NOT NULL,
CONSTRAINT PK_ExmRslt PRIMARY KEY CLUSTERED (Id)
) ON [PRIMARY]
GO
C# code:
private static void BulkCopy(IEnumerable<ExamResultDb> examResults, SqlConnection connection)
{
using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.Default, null))
{
var table = CreateDataTable(examResults);
bulkCopy.DestinationTableName = "ExamResult";
bulkCopy.EnableStreaming = true;
foreach (var item in table.Columns.Cast<DataColumn>())
{
if (item.ColumnName != "Id")
{
bulkCopy.ColumnMappings.Add(item.ColumnName, item.ColumnName);
}
}
using (var reader = new DataTableReader(table))
{
bulkCopy.WriteToServer(reader);
}
}
}
private static DataTable CreateDataTable(IEnumerable<ExamResultDb> examResults)
{
var table = new DataTable("ExamResult");
table.Columns.AddRange(new[]
{
new DataColumn("Id", typeof(long)),
new DataColumn("Caption", typeof(string)),
new DataColumn("SortOrder", typeof(int))
});
////table.Columns[0].AutoIncrement = true;
////table.PrimaryKey = new[] { table.Columns[0] };
foreach (var examResult in examResults)
{
var row = table.NewRow();
row["Caption"] = examResult.Caption;
row["SortOrder"] = examResult.SortOrder;
table.Rows.Add(row);
}
return table;
}
You need an OUTPUT clause on your insert, but Bulk Copy does not allow this kind of customization.
While nowhere near as neat, you could do this by using a Table Valued Parameter and a custom INSERT statement. TVPs use the bulk copy mechanism, so this should still be pretty fast, although there will be two inserts: one to the TVP and one to the real table.
First create a table type
CREATE TYPE dbo.Type_ExamResult AS TABLE
(
Caption nvarchar(1024) NULL,
SortOrder int NOT NULL
);
This function will iterate the rows as SqlDatRecord
private static IEnumerable<SqlDataRecord> AsExamResultTVP(this IEnumerable<ExamResultDb> examResults)
{
// fine to reuse object, see https://stackoverflow.com/a/47640131/14868997
var record = new SqlDataRecord(
new SqlMetaData("Caption", SqlDbType.NVarChar, 1024),
new SqlMetaData("SortOrder", SqlDbType.Int)
);
foreach (var examResult in examResults)
{
record.SetString(0, examResult.Caption);
record.SetInt32(0, examResult.SortOrder);
yield return record; // looks weird, see above link
}
}
Finally insert using OUTPUT
private static void BulkCopy(IEnumerable<ExamResultDb> examResults, SqlConnection connection)
{
const string query = #"
INSERT dbo.ExamResult (Caption, SortOrder)
OUTPUT Id, SortOrder
SELECT t.Caption, t.SortOrder
FROM #tvp t;
";
var dict = examResults.ToDictionary(er => er.SortOrder);
using (var comm = new SqlCommand(query, connection))
{
comm.Parameters.Add(new SqlParameter("#tmp", SqlDbType.Structured)
{
TypeName = "dbo.Type_ExamResult",
Value = examResults.AsExamResultTVP(),
});
using (var reader = comm.ExecuteReader())
{
while(reader.Read())
dict[(int)reader["SortOrder"]].Id = (int)reader["Id"];
}
}
}
Note that the above code assumes that SortOrder is a natural key within the dataset to be inserted. If it is not then you will need to add one, and if you are not inserting that column then you need a rather more complex MERGE statement to be able to access that column in OUTPUT, something like this:
MERGE dbo.ExamResult er
USING #tvp t
ON 1 = 0 -- never match
WHEN NOT MATCHED THEN
INSERT (Caption, SortOrder)
VALUES (t.Caption, t.SortOrder)
OUTPUT er.Id, t.NewIdColumn;
This application I'm developing will read in a bunch of records, validate / modify data, then send that data back to the DB.
I need to do this with an "All or None" approach; either update all of the rows, or don't modify any of the data in the DB.
I found the SqlBulkCopy class which has a method for writing all of the rows in a DataTable to a database but this is unacceptable for my purposes. I just wrote a quick app that'd write a few rows to the DB, populate a DataTable from that database, modify the data, then call WriteToServer(DataTable dt) again. Even though the DataTable has all of the Primary Key information, it simply added new rows with the data and new primary keys rather than updating the data for the rows that have matching primary keys.
So, how can I do bulk Update statements to an MSSQL database from a C# Winforms application?
I don't think this is really relevant, but I'll include my code anyways. I'm not great when it comes to interacting with databases, so it's very possible I'm just doing something dumb and wrong.
static void Main(string[] args)
{
using (SqlConnection conn = new SqlConnection("myConnectionInfo"))
{
//DataTable dt = MakeTable();
DataTable dt = FillDataTable(conn);
using (SqlBulkCopy sbc = new SqlBulkCopy(conn))
{
sbc.DestinationTableName = "TestTbl";
try
{
for (int i = 0; i < dt.Rows.Count; i++)
{
dt.Rows[i][1] = i;
dt.Rows[i][2] = i+1;
dt.Rows[i][3] = i+2;
}
sbc.WriteToServer(dt);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
finally
{
Console.WriteLine("Press Any Key To Continue");
Console.ReadKey();
}
}
}
}
private static DataTable MakeTable()
{
DataTable newProducts = new DataTable("TestTbl");
DataColumn TestPk = new DataColumn();
TestPk.DataType = System.Type.GetType("System.Int32");
TestPk.ColumnName = "TestPk";
TestPk.AllowDBNull = false;
TestPk.AutoIncrement = true;
TestPk.Unique = true;
newProducts.Columns.Add(TestPk);
DataColumn Col1 = new DataColumn();
Col1.DataType = System.Type.GetType("System.String");
Col1.ColumnName = "Col1";
Col1.AllowDBNull = false;
newProducts.Columns.Add(Col1);
// Add 2 more columns like the above block
DataRow row = newProducts.NewRow();
row["Col1"] = "CC-101-WH";
row["Col2"] = "Cyclocomputer - White";
row["Col3"] = DBNull.Value;
newProducts.Rows.Add(row);
// Add 2 more rows like the above block
newProducts.AcceptChanges();
return newProducts;
}
private static DataTable FillDataTable(SqlConnection conn)
{
string query = "SELECT * FROM NYMS.dbo.TestTbl";
using (SqlCommand cmd = new SqlCommand(query, conn))
{
conn.Open();
DataTable dt = new DataTable();
dt.Load(cmd.ExecuteReader());
return dt;
}
}
The call to MakeTable() is commented out because I used it when I first ran the application to insert some data, then while trying to update that test data I use FillDataTable simply to populate it
For inserting a huge amount of data in a database, I used to collect all the inserting information into a list and convert this list into a DataTable. I then insert that list to a database via SqlBulkCopy.
Where I send my generated list LiMyList which contain information of all bulk data which I want to insert to database and pass it to my bulk insertion operation
InsertData(LiMyList, "MyTable");
Where InsertData is
public static void InsertData<T>(List<T> list,string TableName)
{
DataTable dt = new DataTable("MyTable");
clsBulkOperation blk = new clsBulkOperation();
dt = ConvertToDataTable(list);
ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.PerUserRoamingAndLocal);
using (SqlBulkCopy bulkcopy = new SqlBulkCopy(ConfigurationManager.ConnectionStrings["SchoolSoulDataEntitiesForReport"].ConnectionString))
{
bulkcopy.BulkCopyTimeout = 660;
bulkcopy.DestinationTableName = TableName;
bulkcopy.WriteToServer(dt);
}
}
public static DataTable ConvertToDataTable<T>(IList<T> data)
{
PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(typeof(T));
DataTable table = new DataTable();
foreach (PropertyDescriptor prop in properties)
table.Columns.Add(prop.Name, Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType);
foreach (T item in data)
{
DataRow row = table.NewRow();
foreach (PropertyDescriptor prop in properties)
row[prop.Name] = prop.GetValue(item) ?? DBNull.Value;
table.Rows.Add(row);
}
return table;
}
Now I want to do an update operation, is there any way as for inserting data is done by SqlBulkCopy for Updating data to DataBase From C#.Net
What I've done before is perform a bulk insert from the data into a temp table, and then use a command or stored procedure to update the data relating the temp table with the destination table. The temp table is an extra step, but you can have a performance gain with the bulk insert and massive update if the amount of rows is big, compared to updating the data row by row.
Example:
public static void UpdateData<T>(List<T> list,string TableName)
{
DataTable dt = new DataTable("MyTable");
dt = ConvertToDataTable(list);
using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["SchoolSoulDataEntitiesForReport"].ConnectionString))
{
using (SqlCommand command = new SqlCommand("", conn))
{
try
{
conn.Open();
//Creating temp table on database
command.CommandText = "CREATE TABLE #TmpTable(...)";
command.ExecuteNonQuery();
//Bulk insert into temp table
using (SqlBulkCopy bulkcopy = new SqlBulkCopy(conn))
{
bulkcopy.BulkCopyTimeout = 660;
bulkcopy.DestinationTableName = "#TmpTable";
bulkcopy.WriteToServer(dt);
bulkcopy.Close();
}
// Updating destination table, and dropping temp table
command.CommandTimeout = 300;
command.CommandText = "UPDATE T SET ... FROM " + TableName + " T INNER JOIN #TmpTable Temp ON ...; DROP TABLE #TmpTable;";
command.ExecuteNonQuery();
}
catch (Exception ex)
{
// Handle exception properly
}
finally
{
conn.Close();
}
}
}
}
Notice that a single connection is used to perform the whole operation, in order to be able to use the temp table in each step, because the scope of the temp table is per connection.
In my personal experience, the best way to handled this situation is utilizing a Stored Procedure with a Table-Valued Parameter and a User-Defined Table Type. Just set up the type with the columns of the data table, and pass in said-data table as a parameter in the SQL command.
Within the Stored Procedure, you can either join directly on some unique key (if all rows you are updating exist), or - if you might run into a situation where you are having to do both updates and inserts - use the SQL Merge command within the stored procedure to handle both the updates and inserts as applicable.
Microsoft has both syntax reference and an article with examples for the Merge.
For the .NET piece, it's a simple matter of setting the parameter type as SqlDbType.Structured and setting the value of said-parameter to the Data Table that contains the records you want to update.
This method provides the benefit of both clarity and ease of maintenance. While there may be ways that offer performance improvements (such as dropping it into a temporary table then iterating over that table), I think they're outweighed by the simplicity of letting .NET and SQL handle transferring the table and updating the records itself. K.I.S.S.
Bulk Update:
Step 1: put the data which you want to update and primary key in a list.
Step 2: pass this list and ConnectionString to BulkUpdate Method As shown below
Example:
//Method for Bulk Update the Data
public static void BulkUpdateData<T>(List<T> list, string connetionString)
{
DataTable dt = new DataTable("MyTable");
dt = ConvertToDataTable(list);
using (SqlConnection conn = new SqlConnection(connetionString))
{
using (SqlCommand command = new SqlCommand("CREATE TABLE
#TmpTable([PrimaryKey],[ColumnToUpdate])", conn))
{
try
{
conn.Open();
command.ExecuteNonQuery();
using (SqlBulkCopy bulkcopy = new SqlBulkCopy(conn))
{
bulkcopy.BulkCopyTimeout = 6600;
bulkcopy.DestinationTableName = "#TmpTable";
bulkcopy.WriteToServer(dt);
bulkcopy.Close();
}
command.CommandTimeout = 3000;
command.CommandText = "UPDATE P SET P.[ColumnToUpdate]= T.[ColumnToUpdate] FROM [TableName Where you want to update ] AS P INNER JOIN #TmpTable AS T ON P.[PrimaryKey] = T.[PrimaryKey] ;DROP TABLE #TmpTable;";
command.ExecuteNonQuery();
}
catch (Exception ex)
{
// Handle exception properly
}
finally
{
conn.Close();
}
}
}
}
Step 3: put The ConvertToDataTable Method as shown Below.
Example:
public static DataTable ConvertToDataTable<T>(IList<T> data)
{
PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(typeof(T));
DataTable table = new DataTable();
foreach (PropertyDescriptor prop in properties)
table.Columns.Add(prop.Name, Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType);
foreach (T item in data)
{
DataRow row = table.NewRow();
foreach (PropertyDescriptor prop in properties)
row[prop.Name] = prop.GetValue(item) ?? DBNull.Value;
table.Rows.Add(row);
}
return table;
}
Notes: WhereEver SquareBracket[] is there, put your own value.
Try out SqlBulkTools available on Nuget.
Disclaimer: I'm the author of this library.
var bulk = new BulkOperations();
var records = GetRecordsToUpdate();
using (TransactionScope trans = new TransactionScope())
{
using (SqlConnection conn = new SqlConnection(ConfigurationManager
.ConnectionStrings["SqlBulkToolsTest"].ConnectionString))
{
bulk.Setup<MyTable>()
.ForCollection(records)
.WithTable("MyTable")
.AddColumn(x => x.SomeColumn1)
.AddColumn(x => x.SomeColumn2)
.BulkUpdate()
.MatchTargetOn(x => x.Identifier)
.Commit(conn);
}
trans.Complete();
}
Only 'SomeColumn1' and 'SomeColumn2' will be updated. More examples can be found here
I would insert new values in a temporary table and then do a merge against the destination table, something like this:
MERGE [DestTable] AS D
USING #SourceTable S
ON D.ID = S.ID
WHEN MATCHED THEN
UPDATE SET ...
WHEN NOT MATCHED
THEN INSERT (...)
VALUES (...);
You could try to build a query that contains all data. Use a case. It could look like this
update your_table
set some_column = case when id = 1 then 'value of 1'
when id = 5 then 'value of 5'
when id = 7 then 'value of 7'
when id = 9 then 'value of 9'
end
where id in (1,5,7,9)
I'd go for a TempTable approach because that way you aren't locking anything. But if your logic needs to be only in the front end and you need to use bulk copy, I'd try a Delete/Insert approach but in the same SqlTransaction to ensure integrity which would be something like this:
// ...
dt = ConvertToDataTable(list);
using (SqlConnection cnx = new SqlConnection(myConnectionString))
{
using (SqlTranscation tran = cnx.BeginTransaction())
{
DeleteData(cnx, tran, list);
using (SqlBulkCopy bulkcopy = new SqlBulkCopy(cnx, SqlBulkCopyOptions.Default, tran))
{
bulkcopy.BulkCopyTimeout = 660;
bulkcopy.DestinationTableName = TabelName;
bulkcopy.WriteToServer(dt);
}
tran.Commit();
}
}
Complete answer, disclaimer: arrow code; this is mine built from research; Published in SqlRapper. It uses custom attributes over properties to determine whether a key is primary. Yes, super complicated. Yes super reusable. Yes, needs to be refactored. Yes, it is a nuget package. No, the documentation isn't great on github, but it exists. Will it work for everything? Probably not. Will it work for simple stuff? Oh yeah.
How easy is it to use after setup?
public class Log
{
[PrimaryKey]
public int? LogId { get; set; }
public int ApplicationId { get; set; }
[DefaultKey]
public DateTime? Date { get; set; }
public string Message { get; set; }
}
var logs = new List<Log>() { log1, log2 };
success = db.BulkUpdateData(logs);
Here's how it works:
public class PrimaryKeyAttribute : Attribute
{
}
private static bool IsPrimaryKey(object[] attributes)
{
bool skip = false;
foreach (var attr in attributes)
{
if (attr.GetType() == typeof(PrimaryKeyAttribute))
{
skip = true;
}
}
return skip;
}
private string GetSqlDataType(Type type, bool isPrimary = false)
{
var sqlType = new StringBuilder();
var isNullable = false;
if (Nullable.GetUnderlyingType(type) != null)
{
isNullable = true;
type = Nullable.GetUnderlyingType(type);
}
switch (Type.GetTypeCode(type))
{
case TypeCode.String:
isNullable = true;
sqlType.Append("nvarchar(MAX)");
break;
case TypeCode.Int32:
case TypeCode.Int64:
case TypeCode.Int16:
sqlType.Append("int");
break;
case TypeCode.Boolean:
sqlType.Append("bit");
break;
case TypeCode.DateTime:
sqlType.Append("datetime");
break;
case TypeCode.Decimal:
case TypeCode.Double:
sqlType.Append("decimal");
break;
}
if (!isNullable || isPrimary)
{
sqlType.Append(" NOT NULL");
}
return sqlType.ToString();
}
/// <summary>
/// SqlBulkCopy is allegedly protected from Sql Injection.
/// Updates a list of simple sql objects that mock tables.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="rows">A list of rows to insert</param>
/// <param name="tableName">a Table name if your class isn't your table name minus s.</param>
/// <returns>bool success</returns>
public bool BulkUpdateData<T>(List<T> rows, string tableName = null)
{
var template = rows.FirstOrDefault();
string tn = tableName ?? template.GetType().Name + "s";
int updated = 0;
using (SqlConnection con = new SqlConnection(ConnectionString))
{
using (SqlCommand command = new SqlCommand("", con))
{
using (SqlBulkCopy sbc = new SqlBulkCopy(con))
{
var dt = new DataTable();
var columns = template.GetType().GetProperties();;
var colNames = new List<string>();
string keyName = "";
var setStatement = new StringBuilder();
int rowNum = 0;
foreach (var row in rows)
{
dt.Rows.Add();
int colNum = 0;
foreach (var col in columns)
{
var attributes = row.GetType().GetProperty(col.Name).GetCustomAttributes(false);
bool isPrimary = IsPrimaryKey(attributes);
var value = row.GetType().GetProperty(col.Name).GetValue(row);
if (rowNum == 0)
{
colNames.Add($"{col.Name} {GetSqlDataType(col.PropertyType, isPrimary)}");
dt.Columns.Add(new DataColumn(col.Name, Nullable.GetUnderlyingType(col.PropertyType) ?? col.PropertyType));
if (!isPrimary)
{
setStatement.Append($" ME.{col.Name} = T.{col.Name},");
}
}
if (isPrimary)
{
keyName = col.Name;
if (value == null)
{
throw new Exception("Trying to update a row whose primary key is null; use insert instead.");
}
}
dt.Rows[rowNum][colNum] = value ?? DBNull.Value;
colNum++;
}
rowNum++;
}
setStatement.Length--;
try
{
con.Open();
command.CommandText = $"CREATE TABLE [dbo].[#TmpTable]({String.Join(",", colNames)})";
//command.CommandTimeout = CmdTimeOut;
command.ExecuteNonQuery();
sbc.DestinationTableName = "[dbo].[#TmpTable]";
sbc.BulkCopyTimeout = CmdTimeOut * 3;
sbc.WriteToServer(dt);
sbc.Close();
command.CommandTimeout = CmdTimeOut * 3;
command.CommandText = $"UPDATE ME SET {setStatement} FROM {tn} as ME INNER JOIN #TmpTable AS T on ME.{keyName} = T.{keyName}; DROP TABLE #TmpTable;";
updated = command.ExecuteNonQuery();
}
catch (Exception ex)
{
if (con.State != ConnectionState.Closed)
{
sbc.Close();
con.Close();
}
//well logging to sql might not work... we could try... but no.
//So Lets write to a local file.
_logger.Log($"Failed to Bulk Update to Sql: {rows.ToCSV()}", ex);
throw ex;
}
}
}
}
return (updated > 0) ? true : false;
}
Here's the code I'm executing:
public static void Main(string[] args)
{
var connectionString = "Data Source=dbname;User Id=usrname;Password=pass;";
DataTable dt = new DataTable("BULK_INSERT_TEST");
dt.Columns.Add("N", typeof(double));
var row = dt.NewRow();
row["N"] = 1;
using (var connection = new OracleConnection(connectionString)){
connection.Open();
using(var bulkCopy = new OracleBulkCopy(connection, OracleBulkCopyOptions.UseInternalTransaction))
{
bulkCopy.DestinationTableName = dt.TableName;
bulkCopy.WriteToServer(dt);
}
}
using (var connection = new OracleConnection(connectionString)){
connection.Open();
var command = new OracleCommand("select count(*) from BULK_INSERT_TEST", connection);
var res = command.ExecuteScalar();
Console.WriteLine(res); // Here I'm getting 0
}
}
It uses OracleBulkCopy to insert 1 entry to table and then it counts rows in the table.
Why am I getting 0 rows? Here's the structure of table:
-- Create table
create table BULK_INSERT_TEST
(
n NUMBER
)
You haven't actually added the row to the table. You've used the table to create a new row with the right columns, but not actually added it to the table. You need:
dt.Rows.Add(row);
(before your first using statement, basically)
I have been struggling to get the right c# code for getting the values after a PRAGMA table_info query.
Since my edit with extra code was rejected in this post, I made this question for other people that would otherwise waste hours for a fast solution.
Assuming you want a DataTable with the list of field of your table:
using (var con = new SQLiteConnection(preparedConnectionString))
{
using (var cmd = new SQLiteCommand("PRAGMA table_info(" + tableName + ");"))
{
var table = new DataTable();
cmd.Connection = con;
cmd.Connection.Open();
SQLiteDataAdapter adp = null;
try
{
adp = new SQLiteDataAdapter(cmd);
adp.Fill(table);
con.Close();
return table;
}
catch (Exception ex)
{ }
}
}
Return result is:
cid: id of the column
name: the name of the column
type: the type of the column
notnull: 0 or 1 if the column can contains null values
dflt_value: the default value
pk: 0 or 1 if the column partecipate to the primary key
If you want only the column names into a List you can use (you have to include System.Data.DataSetExtension):
return table.AsEnumerable().Select(r=>r["name"].ToString()).ToList();
EDIT: Or you can avoid the DataSetExtension reference using this code:
using (var con = new SQLiteConnection(preparedConnectionString))
{
using (var cmd = new SQLiteCommand("PRAGMA table_info(" + tableName + ");"))
{
var table = new DataTable();
cmd.Connection = con;
cmd.Connection.Open();
SQLiteDataAdapter adp = null;
try
{
adp = new SQLiteDataAdapter(cmd);
adp.Fill(table);
con.Close();
var res = new List<string>();
for(int i = 0;i<table.Rows.Count;i++)
res.Add(table.Rows[i]["name"].ToString());
return res;
}
catch (Exception ex){ }
}
}
return new List<string>();
There are a lot of PRAGMA statements that you can use in SQLite, have a look at the link.
About the using statement: it's very simple, it is used to be sure that disposable objects will be disposed whatever can happen in your code: see this link or this reference
Code:
DB = new SQLiteConnection(#"Data Source="+DBFileName);
DB.Open();
SQLiteCommand command = new SQLiteCommand("PRAGMA table_info('tracks')", DB);
DataTable dataTable = new DataTable();
SQLiteDataAdapter dataAdapter = new SQLiteDataAdapter(command);
dataAdapter.Fill(dataTable);
DB.Close();
foreach (DataRow row in dataTable.Rows) {
DBColumnNames.Add((string)row[dataTable.Columns[1]]); }
//Out(String.Join(",",
DBColumnNames.ToArray()));//debug
All elements in the resulted rows:
int cid, string name, string type,int notnull, string dflt_value, int pk
More info on PRAGMA
Not sure if this exactly what you are after but this is how I have grabbed the data and subsequently used it. Hope it helps!
Obviously the switch is not covering all eventualities, just those I have needed to so far.
/// <summary>
/// Allows the programmer to easily update rows in the DB.
/// </summary>
/// <param name="tableName">The table to update.</param>
/// <param name="data">A dictionary containing Column names and their new values.</param>
/// <param name="where">The where clause for the update statement.</param>
/// <returns>A boolean true or false to signify success or failure.</returns>
public bool Update(String tableName, Dictionary<String, String> data, String where)
{
String vals = "";
Boolean returnCode = true;
//Need to determine the dataype of fields to update as this affects the way the sql needs to be formatted
String colQuery = "PRAGMA table_info(" + tableName + ")";
DataTable colDataTypes = GetDataTable(colQuery);
if (data.Count >= 1)
{
foreach (KeyValuePair<String, String> pair in data)
{
DataRow[] colDataTypeRow = colDataTypes.Select("name = '" + pair.Key.ToString() + "'");
String colDataType="";
if (pair.Key.ToString()== "rowid" || pair.Key.ToString()== "_rowid_" || pair.Key.ToString()=="oid")
{
colDataType = "INT";
}
else
{
colDataType = colDataTypeRow[0]["type"].ToString();
}
colDataType = colDataType.Split(' ').FirstOrDefault();
if ( colDataType == "VARCHAR")
{
colDataType = "VARCHAR";
}
switch(colDataType)
{
case "INTEGER": case "INT": case "NUMERIC": case "REAL":
vals += String.Format(" {0} = {1},", pair.Key.ToString(), pair.Value.ToString());
break;
case "TEXT": case "VARCHAR": case "DATE": case "DATETIME":
vals += String.Format(" {0} = '{1}',", pair.Key.ToString(), pair.Value.ToString());
break;
}
}
vals = vals.Substring(0, vals.Length - 1);
}
try
{
string sql = String.Format("update {0} set {1} where {2};", tableName, vals, where);
//dbl.AppendLine(sql);
dbl.AppendLine(sql);
this.ExecuteNonQuery(sql);
}
catch(Exception crap)
{
OutCrap(crap);
returnCode = false;
}
return returnCode;
}