I am trying to bulk copy from one table to another by mapping the column names as the source and destination may not have same columns always.
Source can have 8 columns and destination can have 10 .. I need to map the columns and bulk copy.
Tried the below code..didn't work..getting Error: The given ColumnName 'moduleid' does not match up with any column in data source.
Source: existingtablecolumnsPresent has [collection time],[logtime],[moduleid],[node],[reason],[time],[timestamp],[usecaseid]
Destination: dataTable.Columns has [Node],[Time],[Reason],[Moduleid],[Usecaseid]
Please advise
public static void BatchBulkCopy(DataTable dataTable, string DestinationTbl, List<string> columnMapping,string filename)
{
var program = new Program();
// Get the DataTable
DataTable dtInsertRows = dataTable;
using (SqlBulkCopy sbc = new SqlBulkCopy(program.connectionStr.ToString()))
{
try {
sbc.DestinationTableName = DestinationTbl.ToLower();
string sourceTableQuery = "Select top 1 * from " + "[" + dataTable.TableName + "]";
DataTable dtSource = SqlHelper.ExecuteDataset(program.connectionStr.ToString(), CommandType.Text, sourceTableQuery).Tables[0];
for (int i = 0; i < dataTable.Columns.Count; i++)
{ //check if destination Column Exists in Source table
if (dtSource.Columns.Cast<DataColumn>().Select(a => "[" + a.ColumnName.ToLower() + "]").Contains(dataTable.Columns[i].ToString().ToLower()))//contain method is not case sensitive
{
List<string> existingtablecolumnsPresent = dtSource.Columns.Cast<DataColumn>().Select(a => "[" + a.ColumnName.ToLower() + "]").Distinct().OrderBy(t => t).ToList();
int sourceColumnIndex = existingtablecolumnsPresent.IndexOf(dataTable.Columns[i].ToString().ToLower());//Once column matched get its index
sbc.ColumnMappings.Add(dtSource.Columns[sourceColumnIndex].ToString(), dtSource.Columns[sourceColumnIndex].ToString());//give coluns name of source table rather then destination table so that it would avoid case sensitivity
}
}
sbc.WriteToServer(dtInsertRows);
sbc.Close();
}
catch (Exception ex)
{
Log.WriteLog("BatchBulkCopy" + " - " + filename, dataTable.TableName, ex.Message.ToString());
// To move a file or folder to a new location:
//if (File.Exists(program.sourceFile + filename))
// System.IO.File.Move(program.sourceFile + filename, program.failedfiles + filename);
}
As requested (create a DataTable with the columns you want to insert in them- leave the others out. Make sure any columns you leave out are marked in the table for NULL or have a DEFAULT VALUE constraint (I can't show you how to do that unless you show me your table);
//This first method is psuedoCode to explain how to create your datatable. You need to do it in the way that makes sense for you.
public DataTable createDataTable(){
List<string> excludedColumns = new List<string>();
excludedColumns.Add("FieldToExclude");
//...
DataTable dt = new DataTable();
foreach(string col in getColumns(myTable)){
if(!excludedColumns.Contains(name)){
DataColumn dC = new DataColumn(name,type);
DataTable.Add(dC);
}
return dt;
}
public List<string> getColumns(string tableName)
{
List<string> ret = new List<string>();
using (SqlConnection conn = getConn())
{
conn.Open();
using (SqlCommand com = conn.CreateCommand())
{
com.CommandText = "select column_Name from information_schema.COLUMNS where table_name = #tab";
com.Parameters.AddWithValue("#tab", tableName);
SqlDataReader read = com.ExecuteReader();
While(read.Read()){
ret.Add(Convert.ToString(read[0]);
}
conn.Close();
}
return ret;
}
//Now, you have a DataTable that has all the columns you want to insert. Map them yourself in code by adding to the appropriate column in your datatable.
public bool isCopyInProgess = false;//not necessary - just part of my code
public void saveDataTable(string tableName, DataTable table)
{
using (SqlConnection conn = getConn())
{
conn.Open();
using (var bulkCopy = new SqlBulkCopy(conn))//, SqlBulkCopyOptions.KeepIdentity))//un-comment if you want to use your own identity column
{
// my DataTable column names match my SQL Column names, so I simply made this loop. However if your column names don't match, just pass in which datatable name matches the SQL column name in Column Mappings
foreach (DataColumn col in table.Columns)
{
//Console.WriteLine("mapping " + col.ColumnName+" ("+does_Column_Exist(col.ColumnName,"Item").ToString()+")");
bulkCopy.ColumnMappings.Add(col.ColumnName, "["+col.ColumnName+"]");
// Console.WriteLine("ok\n");
}
bulkCopy.BulkCopyTimeout = 8000;
bulkCopy.DestinationTableName = tableName;
bulkCopy.BatchSize = 10000;
bulkCopy.EnableStreaming = true;
//bulkCopy.SqlRowsCopied += BulkCopy_SqlRowsCopied;
//bulkCopy.NotifyAfter = 10000;
isCopyInProgess = true;
bulkCopy.WriteToServer(table);
}
conn.Close();
}
}
Also, use this as your bolumn checker:
public bool does_Column_Exist(string colName,string tableName)
{
bool ret = false;
using (SqlConnection conn = getConn())
{
conn.Open();
using (SqlCommand com = conn.CreateCommand())
{
com.CommandText = "select count(*) from information_schema.COLUMNS where column_name = #col and table_name = #tab";
com.Parameters.AddWithValue("#tab", tableName);
com.Parameters.AddWithValue("#col", colName);
ret = Convert.ToInt32(com.ExecuteScalar()) == 0 ? false : true;
}
conn.Close();
}
return ret;
}
Is there a specific reason you need C# for this? It seems like the path of least resistance would be to use SQL to do the job.
INSERT INTO table2
(column_name(s))
SELECT column_name(s)
FROM table1;
Related
I've been developing a C# WPF project with VS2015 using SQL Server Express LocalDb with Entity Framework. I have built a custom seeder for the database, that reads test data from an Excel file, that simply combines the Excel data into a command string, and this is inserted using context.Database.ExecuteSQLCommand.
Now, I was thinking of launching the project with SQL Server Compact Edition 4.0, but I find this command is not working anymore. Do I have to write my uploader again using SqlCeConnection and SqlCeCommand or am I missing something?
Also, from somewhere I have understood that with EF you can switch the SQL provider and the code would not need other changes. Am I in for more surprises down the road?
Example of the uploader command:
string cmd = "INSERT INTO Venues(Name, City, Telephone) Values ('X','Y','Z')"
context.Database.ExecuteSqlCommand(cmd);
The error:
There was an error parsing the query. [ Token line number = 2,Token line offset = 1,Token in error = INSERT ]
This is not just a testing issue, as I would want to include this uploader in the production version, too, for quick inserting of master data (e.g. employee list).
EDIT: Uploader code. If this can be done without resorting to raw SQL, that would be a good solution, too.
This loops through Excel sheets (named after entities) and columns (first row has property name) and rows 2->n (data). This handles the upload of basically any amount of data within Excel limitations. The point is that the code has no knowledge of the entities (might have been possible to parameterize DataContext too). Code might not be optimal, as I'm just a beginner, but has worked for me, except not with SQL CE. Editing to suit CE is not a big issue, but I wanted to ask for possibly better ways.
public static class ExcelUploader
{
static ArrayList data;
static List<string> tableNames;
public static string Upload(string filePath)
{
string result = "";
data = new ArrayList();
tableNames = new List<string>();
ArrayList upLoadData = ReadFile(filePath);
List<string> dataList = ArrayListToStringList(upLoadData);
using (var db = new DataContext())
{
using (var trans = db.Database.BeginTransaction())
{
try
{
foreach (var cmd in dataList)
{
Console.WriteLine(cmd);
db.Database.ExecuteSqlCommand(cmd);
}
db.SaveChanges();
trans.Commit();
}
catch (Exception e)
{
trans.Rollback();
result = e.Message;
MessageBox.Show(result);
}
}
}
return result;
}
private static ArrayList ReadFile(string fileName)
{
List<string> commands = new List<string>();
var xlApp = new Microsoft.Office.Interop.Excel.Application();
var wb = xlApp.Workbooks.Open(fileName, ReadOnly: true);
xlApp.Visible = false;
foreach (Worksheet ws in wb.Worksheets)
{
var r = ws.UsedRange;
var array = r.Value;
data.Add(array);
tableNames.Add(ws.Name);
}
wb.Close(SaveChanges: false);
xlApp.Quit();
return data;
}
private static List<string> ArrayListToStringList(ArrayList arrList)
{
List<string> result = new List<string>();
for(int tableAmount = 0;tableAmount<data.Count;tableAmount++)
{
result.Add(ArrayToSqlCommand(arrList[tableAmount] as Array, tableNames[tableAmount]));
}
return result;
}
private static string ArrayToSqlCommand(Array arr, string tableName)
{
int propertyRow = 1;
int firstDataRow = 2;
string command = "";
// loop rows
for (int rowIndex = firstDataRow; rowIndex <= arr.GetUpperBound(0); rowIndex++)
{
command += "INSERT INTO " + tableName + "(";
//add column names
for (int colIndex = 1; colIndex <= arr.GetUpperBound(1); colIndex++)
{
//get property name
command += arr.GetValue(propertyRow, colIndex);
//add comma if not last column, otherwise close bracket
if (colIndex == arr.GetUpperBound(1))
{
command += ") Values (";
}
else
{
command += ", ";
}
}
//add values
for (int colIndex = 1; colIndex <= arr.GetUpperBound(1); colIndex++)
{
//get property value
command += "'" + arr.GetValue(rowIndex, colIndex) + "'";
//add comma if not last column, otherwise close bracket
if (colIndex == arr.GetUpperBound(1))
{
command += ");";
}
else
{
command += ", ";
}
}
command += "\n";
}
return command;
}
}
There are two ways to use raw SQL queries I'd offer.
Initial data
1) Excel table
+=======+=======+===========+
| Name | City | Telephone |
|===========================|
| Adam | Addr1 | 111-11-11 |
|-------|-------|-----------|
| Peter | Addr2 | 222-22-22 |
+-------+-------+-----------+
2) SQL Server CE table
CREATE TABLE Venues
(
Id int identity primary key,
[Name] nvarchar(100) null,
City nvarchar(100) null,
Telephone nvarchar(100) null
);
3) Getting data from Excel
Here we're interested in getting array from Excel sheet. As soon as we get it, we can safely close Excel. The code assumes file "Employees.xlsx" to be next to executable file.
private object[,] GetExcelData()
{
xlApp = new Excel.Application { Visible = false };
var xlBook =
xlApp.Workbooks.Open(System.IO.Path.Combine(
Environment.CurrentDirectory,
"Employees.xlsx"));
var xlSheet = xlBook.Sheets[1] as Excel.Worksheet;
// For process termination
var xlHwnd = new IntPtr(xlApp.Hwnd);
var xlProc = Process.GetProcesses()
.Where(p => p.MainWindowHandle == xlHwnd)
.First();
// Get Excel data: it's 2-D array with lower bounds as 1.
object[,] arr = xlSheet.Range["A1"].CurrentRegion.Value;
// Shutdown Excel
xlBook.Close();
xlApp.Quit();
xlProc.Kill();
GC.Collect();
GC.WaitForFullGCComplete();
return arr;
}
Now you can use one of the ways to generate query.
Option 1. Use ExecuteSqlCommand
When using ExecuteSqlCommand, it's advisable to use parameterized queries to avoid errors. You can pass explicitly created SqlCeParameter or just pass a value.
private void UseExecuteSqlCommand()
{
object[,] arr = GetExcelData();
using (var db = new EmpContext())
{
db.Database.Initialize(true);
int count = 0;
string sql = "INSERT INTO Venues (Name, City, Telephone) " +
"VALUES (#name, #city, #phone);";
// Start from 2-nd row since we need to skip header
for (int r = 2; r <= arr.GetUpperBound(0); ++r)
{
db.Database.ExecuteSqlCommand(
sql,
new SqlCeParameter("#name", (string)arr[r, 1]),
new SqlCeParameter("#city", (string)arr[r, 2]),
new SqlCeParameter("#phone", (string)arr[r, 3])
);
++count;
}
conn.Close();
MessageBox.Show($"{count} records were saved.");
}
}
Option 2. Use DbConnection
If you want your code to be more generic, you can create method which would accept DbConnection. This will allow to pass either SqlConnection or SqlCeConnection. But the code becomes more verbose because we can't use constructors since these classes are abstract.
private void UseDbConnection()
{
object[,] arr = GetExcelData();
using (var db = new EmpContext())
{
db.Database.Initialize(true);
int count = 0;
string sql = "INSERT INTO Venues (Name, City, Telephone) " +
"VALUES (#name, #city, #phone);";
DbParameter param = null;
DbConnection conn = db.Database.Connection;
conn.Open();
DbCommand command = conn.CreateCommand();
command.CommandText = sql;
command.CommandType = CommandType.Text;
// Create parameters
// Name
param = command.CreateParameter();
param.ParameterName = "#name";
command.Parameters.Add(param);
// City
param = command.CreateParameter();
param.ParameterName = "#city";
command.Parameters.Add(param);
// Telephone
param = command.CreateParameter();
param.ParameterName = "#phone";
command.Parameters.Add(param);
// Start from 2-nd row since we need to skip header
for (int r = 2; r <= arr.GetUpperBound(0); ++r)
{
command.Parameters["#name"].Value = (string)arr[r, 1];
command.Parameters["#city"].Value = (string)arr[r, 2];
command.Parameters["#phone"].Value = (string)arr[r, 3];
command.ExecuteNonQuery();
++count;
}
conn.Close();
MessageBox.Show($"{count} records were saved.");
}
}
You can also use ordinal positions for parameters which eliminates creating parameters names and makes code much shorter:
private void UseDbConnection()
{
object[,] arr = GetExcelData();
using (var db = new EmpContext())
{
db.Database.Initialize(true);
int count = 0;
// Take a note - use '?' as parameters
string sql = "INSERT INTO Venues (Name, City, Telephone) " +
"VALUES (?, ?, ?);";
DbConnection conn = db.Database.Connection;
conn.Open();
DbCommand command = conn.CreateCommand();
command.CommandText = sql;
command.CommandType = CommandType.Text;
// Create parameters
command.Parameters.Add(command.CreateParameter());
command.Parameters.Add(command.CreateParameter());
command.Parameters.Add(command.CreateParameter());
for (int r = 2; r <= arr.GetUpperBound(0); ++r)
{
// Access parameters by position
command.Parameters[0].Value = (string)arr[r, 1];
command.Parameters[1].Value = (string)arr[r, 2];
command.Parameters[2].Value = (string)arr[r, 3];
command.ExecuteNonQuery();
++count;
}
conn.Close();
MessageBox.Show($"{count} records were saved.");
}
}
P.S.
I didn't check whether the underlying connection is opened, but it's a good idea to do so.
Based on JohnyL's excellent input, I was able to modify my code so that it works with either SQL Server Express and and SQL Server CE. I'll put my new code as an answer, as I had to parameterize it further, as I couldn't write the property names in the code either. But this was a simple step, once I got the idea from JohnyL. Not sure though, if the database writing operation should be wrapped inside a DbTransaction, but this worked for now.
public static class ExcelUploader
{
static ArrayList data;
static List<string> tableNames;
static List<DbCommand> cmdList = new List<DbCommand>();
static DbConnection conn;
public static void Upload(string filePath)
{
data = new ArrayList();
tableNames = new List<string>();
//get Excel data to array list
ArrayList upLoadData = ReadFile(filePath);
using (var db = new DataContext())
{
conn = db.Database.Connection;
//transform arraylist into a list of DbCommands
ArrayListToCommandList(upLoadData);
conn.Open();
try
{
foreach (var cmd in cmdList)
{
//Console.WriteLine(cmd.CommandText);
cmd.ExecuteNonQuery();
}
}
catch (Exception e)
{
var result = e.Message;
MessageBox.Show(result);
}
}
}
//opens Excel file and reads worksheets to arraylist
private static ArrayList ReadFile(string fileName)
{
List<string> commands = new List<string>();
var xlApp = new Microsoft.Office.Interop.Excel.Application();
var wb = xlApp.Workbooks.Open(fileName, ReadOnly: true);
xlApp.Visible = false;
foreach (Worksheet ws in wb.Worksheets)
{
var r = ws.UsedRange;
var array = r.Value;
data.Add(array);
tableNames.Add(ws.Name);
}
wb.Close(SaveChanges: false);
xlApp.Quit();
return data;
}
//transforms arraylist to a list of DbCommands
private static void ArrayListToCommandList(ArrayList arrList)
{
List<DbCommand> result = new List<DbCommand>();
for (int tableAmount = 0; tableAmount < data.Count; tableAmount++)
{
ArrayToSqlCommands(arrList[tableAmount] as Array, tableNames[tableAmount]);
}
}
private static void ArrayToSqlCommands(Array arr, string tableName)
{
//Excel row which holds property names
int propertyRow = 1;
//First Excel row with values
int firstDataRow = 2;
string sql = "";
DbCommand cmd = conn.CreateCommand();
sql += "INSERT INTO " + tableName + "(";
//add column names to command text
for (int colIndex = 1; colIndex <= arr.GetUpperBound(1); colIndex++)
{
//get property name
sql += arr.GetValue(propertyRow, colIndex);
//add comma if not last column, otherwise close bracket
if (colIndex == arr.GetUpperBound(1))
{
sql += ") Values (";
}
else
{
sql += ", ";
}
}
//add value parameter names to command text
for (int colIndex = 1; colIndex <= arr.GetUpperBound(1); colIndex++)
{
//get property name
sql += "#" + arr.GetValue(propertyRow, colIndex);
//add comma if not last column, otherwise close bracket
if (colIndex == arr.GetUpperBound(1))
{
sql += ");";
}
else
{
sql += ", ";
}
}
//add data elements as command parameter values
for (int rowIndex = firstDataRow; rowIndex <= arr.GetUpperBound(0); rowIndex++)
{
//initialize command
cmd = conn.CreateCommand();
cmd.CommandText = sql;
cmd.CommandType = CommandType.Text;
for (int colIndex = 1; colIndex <= arr.GetUpperBound(1); colIndex++)
{
//set parameter values
DbParameter param = null;
param = cmd.CreateParameter();
param.ParameterName = "#" + (string)arr.GetValue(propertyRow, colIndex);
cmd.Parameters.Add(param);
cmd.Parameters[param.ParameterName].Value = arr.GetValue(rowIndex, colIndex);
}
//add command to command list
cmdList.Add(cmd);
}
}
}
I am getting data from excel and showing it in DataGridWiew.
I have two textboxes, one is for starting index for first record and other is for last record.
Code works fine. But lets suppose starting record is 1 and ending is 10 when I change 10 to 1 or 2 it gives me an error in this line:
adapter.Fill(dataTable);
Full Code is below:
public DataSet Parse(string fileName)
{
string connectionString = string.Format("provider = Microsoft.Jet.OLEDB.4.0; data source = {0}; Extended Properties = Excel 8.0;", fileName);
DataSet data = new DataSet();
foreach (var sheetName in GetExcelSheetNames(connectionString))
{
using (OleDbConnection con = new OleDbConnection(connectionString))
{
string query = "";
var dataTable = new DataTable();
if(tbStarting.Text.Trim()=="" && tbEnding.Text.Trim() == "")
{
query = string.Format("SELECT * FROM [{0}]", sheetName);
}
else
{
query = string.Format("SELECT * FROM [{0}] where SrNo between " + int.Parse(tbStarting.Text.Trim()) + " and " + int.Parse(tbEnding.Text.Trim()) + " order by SrNo", sheetName);
}
con.Open();
OleDbDataAdapter adapter = new OleDbDataAdapter(query, con);
adapter.Fill(dataTable);
data.Tables.Add(dataTable);
con.Close();
}
}
return data;
}
static string[] GetExcelSheetNames(string connectionString)
{
OleDbConnection con = null;
DataTable dt = null;
con = new OleDbConnection(connectionString);
con.Open();
dt = con.GetOleDbSchemaTable(OleDbSchemaGuid.Tables, null);
if (dt == null)
{
return null;
}
String[] excelSheetNames = new String[dt.Rows.Count];
int i = 0;
foreach (DataRow row in dt.Rows)
{
excelSheetNames[i] = row["TABLE_NAME"].ToString();
i++;
}
return excelSheetNames;
}
Why this is happening please help me?
Looking at the code, it seems that your procedure is working when you ask to retrieve all the record in each table. But you are not showing which table (Sheet) is actually used afterwars.
Chances are, you are using the first one only.
When you submit some parameters, only one of the tables (Sheets) can fulfill those requirements. The other(s) don't, possibly because a field named [SrNo] is not present.
This causes the More Parameters Required error when trying to apply a filter.
Not related to the error, but worth noting: you don't need to recreate the whole DataSet + DataTables to filter your DataSources.
The DataSet.Tables[N].DefaultView.RowFilter can be used to get the same result without destroying all the objects each time a filter is required.
RowFilter has some limitations in the language (e.g. does not support BETWEEN, Field >= Value1 AND Field <= Value2 must be used), but it's quite effective.
This is a possible setup:
(xDataSet is a placeholder for your actual DataSet)
//Collect the values in the TextBoxes in a string array
private void button1_Click(object sender, EventArgs e)
{
string[] Ranges = new string[] { tbStarting.Text.Trim(), tbEnding.Text.Trim() };
if (xDataSet != null)
FilterDataset(Ranges);
}
private void FilterDataset(string[] Ranges)
{
if (string.IsNullOrEmpty(Ranges[0]) & string.IsNullOrEmpty(Ranges[1]))
xDataSet.Tables[0].DefaultView.RowFilter = null;
else if (string.IsNullOrEmpty(Ranges[0]) | string.IsNullOrEmpty(Ranges[1]))
return;
else if (int.Parse(Ranges[0]) < int.Parse(Ranges[1]))
xDataSet.Tables[0].DefaultView.RowFilter = string.Format("SrNo >= {0} AND SrNo <= {1}", Ranges[0], Ranges[1]);
else
xDataSet.Tables[0].DefaultView.RowFilter = string.Format("SrNo = {0}", Ranges[0]);
this.dataGridView1.Update();
}
I've modified your code you code a bit to handle those requirements.
(I've left here those filters anyway; they're not used, but if you still want them, they are in a working condition)
DataSet xDataSet = new DataSet();
string WorkBookPath = #"[Excel WorkBook Path]";
//Query one Sheet only. More can be added if necessary
string[] WBSheetsNames = new string[] { "Sheet1" };
//Open the Excel document and assign the DataSource to a dataGridView
xDataSet = Parse(WorkBookPath, WBSheetsNames, null);
dataGridView1.DataSource = xDataSet.Tables[0];
dataGridView1.Refresh();
public DataSet Parse(string fileName, string[] WorkSheets, string[] ranges)
{
if (!File.Exists(fileName)) return null;
string connectionString = string.Format("provider = Microsoft.ACE.OLEDB.12.0; " +
"data source = {0}; " +
"Extended Properties = \"Excel 12.0;HDR=YES\"",
fileName);
DataSet data = new DataSet();
string query = string.Empty;
foreach (string sheetName in GetExcelSheetNames(connectionString))
{
foreach (string WorkSheet in WorkSheets)
if (sheetName == (WorkSheet + "$"))
{
using (OleDbConnection con = new OleDbConnection(connectionString))
{
DataTable dataTable = new DataTable();
if ((ranges == null) ||
(string.IsNullOrEmpty(ranges[0]) || string.IsNullOrEmpty(ranges[1])) ||
(int.Parse(ranges[0]) > int.Parse(ranges[1])))
query = string.Format("SELECT * FROM [{0}]", sheetName);
else if ((int.Parse(ranges[0]) == int.Parse(ranges[1])))
query = string.Format("SELECT * FROM [{0}] WHERE SrNo = {1}", sheetName, ranges[0]);
else
query = string.Format("SELECT * FROM [{0}] WHERE (SrNo BETWEEN {1} AND {2}) " +
"ORDER BY SrNo", sheetName, ranges[0], ranges[1]);
con.Open();
OleDbDataAdapter adapter = new OleDbDataAdapter(query, con);
adapter.Fill(dataTable);
data.Tables.Add(dataTable);
};
}
}
return data;
}
static string[] GetExcelSheetNames(string connectionString)
{
string[] excelSheetNames = null;
using (OleDbConnection con = new OleDbConnection(connectionString))
{
con.Open();
using (DataTable dt = con.GetOleDbSchemaTable(OleDbSchemaGuid.Tables, null))
{
if (dt != null)
{
excelSheetNames = new string[dt.Rows.Count];
for (int i = 0; i < dt.Rows.Count; i++)
{
excelSheetNames[i] = dt.Rows[i]["TABLE_NAME"].ToString();
}
}
}
}
return excelSheetNames;
}
We recently upgraded our solution to the new Managed ODP.Net (v4.121.2.0, from v4.121.1.0) and have come across a problem with udpating to CLOB fields when the field has between 1001 and 4000 characters. When you attempt to do that an error ORA-1461 is thrown from ODP.Net.
Run the same code and data, using the earlier version of ODP.Net and it works fine. Also, you can insert records with 1001 and 4000 characters, you just can't update them.
I've created a sample program in C# that demonstrates the problem. The program does the following:
It creates a 3 column table, one column a CLOB, in the database
It creates an in-memory .Net DataSet object to match.
Creates a new record in the DataSet with 1400 characters in the CLOB field.
Saves the DataSet to the database with an INSERT statement.
Updates the CLOB field in the DataSet with some new data, again 1400 characters worth of data.
Saves the DataSet to the database, and the ORA-1461 is thrown.
I don't have an Oracle Support account, so where do you report to Oracle problems with ODP.Net?
Sample C# code that demonstrates the problem:
using System;
using System.Data;
using System.Data.Common;
using System.Diagnostics;
using Oracle.ManagedDataAccess.Client;
class CLOBTest
{
string _TableName = "CLOBTEST";
string _ServerName = "servername";
string _UserName = "username";
string _Password = "password";
public void CLOBTest1()
{
// Create a physical data table, if needed, in the Oracle DB that has the CLOB column
CreateTable();
// Create a dataset for the CLOBTEST table, fill it with a new row
DataSet CLOBInfo = BuildCLOBTestDataSet();
DataTable CLOBTable = CLOBInfo.Tables[_TableName];
DataRow CLOBRow = CLOBTable.NewRow();
CLOBRow["ACTION_CODE"] = DateTime.Now.ToString("s");
CLOBRow["DESCRIPTION"] = "CLOB Slim Test";
// The size of text in the CLOB field is critical to reproducing this defect.
// It *only* happens when the field has between 1001 and 4000 characters.
int LOBFieldSize = 1400;
string CLOBText = DateTime.Now.ToString("s") + " " + new string('-', LOBFieldSize);
CLOBRow["SCRIPT_TEXT"] = CLOBText.Substring(0, LOBFieldSize);
CLOBTable.Rows.Add(CLOBRow);
// Add that row to the DB, and then mark the DS with AcceptChanges
InsertRow(CLOBInfo);
// Update that row with some new data.
CLOBText = DateTime.Now.ToString("s") + " :: " + CLOBText;
CLOBRow["SCRIPT_TEXT"] = CLOBText.Substring(0, LOBFieldSize);
// Error (ORA-1461) happens in the UPDATE when the CLOB has 1001 - 4000 characters in it.
UpdateRow(CLOBInfo);
}
private void CreateTable()
{
if (TableExists())
return;
using (OracleConnection oc = OpenConnection())
{
using (OracleCommand ocmd = oc.CreateCommand())
{
string SQL = "CREATE TABLE " + _TableName + " (ACTION_CODE VARCHAR2(30) NOT NULL, DESCRIPTION VARCHAR2(50) NOT NULL, SCRIPT_TEXT CLOB, CONSTRAINT CLOBTEST_PK PRIMARY KEY (ACTION_CODE))";
ocmd.CommandText = SQL;
ocmd.ExecuteNonQuery();
AddLogMessage("Table created.");
}
}
}
private bool TableExists()
{
using (OracleConnection oc = OpenConnection())
{
using (OracleCommand ocmd = oc.CreateCommand())
{
string SQL = "SELECT COUNT(*) FROM USER_TABLES WHERE TABLE_NAME = '" + _TableName + "'";
ocmd.CommandText = SQL;
object teRaw = ocmd.ExecuteScalar();
bool te = (bool)(int.Parse(teRaw.ToString()) > 0);
AddLogMessage("Table exists? " + te.ToString());
return te;
}
}
}
private void InsertRow(DataSet CLOBInfo)
{
string SQL = "INSERT INTO " + _TableName + " (ACTION_CODE, DESCRIPTION, SCRIPT_TEXT) VALUES (:pACTION_CODE, :pDESCRIPTION, :pSCRIPT_TEXT)";
using (OracleConnection oc = OpenConnection())
{
using (OracleDataAdapter oda = new OracleDataAdapter())
{
using (OracleCommand ocmd = oc.CreateCommand())
{
CreateDataParameters(ocmd);
ocmd.CommandText = SQL;
oda.InsertCommand = ocmd;
DataRow[] updRows = CLOBInfo.Tables[_TableName].Select(null, null, DataViewRowState.Added);
if (updRows.Length > 0)
{
int rc = oda.Update(updRows);
CLOBInfo.AcceptChanges();
AddLogMessage("Row inserted into CLOBTEST. rc = " + rc.ToString());
}
else
AddLogMessage("No rows to insert.");
}
}
}
}
private void UpdateRow(DataSet CLOBInfo)
{
string SQL = "UPDATE " + _TableName + " SET ACTION_CODE = :pACTION_CODE, DESCRIPTION = :pDESCRIPTION, SCRIPT_TEXT = :pSCRIPT_TEXT WHERE ACTION_CODE = :pOLDACTION_CODE";
using (OracleConnection oc = OpenConnection())
{
using (OracleDataAdapter oda = new OracleDataAdapter())
{
using (OracleCommand ocmd = oc.CreateCommand())
{
ocmd.CommandText = SQL;
CreateDataParameters(ocmd);
OracleParameter kp = new OracleParameter();
kp.ParameterName = "pOLDACTION_CODE";
kp.SourceColumn = "ACTION_CODE";
kp.SourceVersion = DataRowVersion.Original;
ocmd.Parameters.Add(kp);
oda.UpdateCommand = ocmd;
DataRow[] updRows = CLOBInfo.Tables[_TableName].Select(null, null, DataViewRowState.ModifiedCurrent);
if (updRows.Length > 0)
{
int rc = oda.Update(updRows);
CLOBInfo.AcceptChanges();
AddLogMessage("CLOBTEST row updated. rc = " + rc.ToString());
}
else
AddLogMessage("No rows to update.");
}
}
}
}
private void CreateDataParameters(OracleCommand ocmd)
{
OracleParameter pActionCode = new OracleParameter();
pActionCode.ParameterName = "pACTION_CODE";
pActionCode.SourceColumn = "ACTION_CODE";
ocmd.Parameters.Add(pActionCode);
OracleParameter pDescription = new OracleParameter();
pDescription.ParameterName = "pDESCRIPTION";
pDescription.SourceColumn = "DESCRIPTION";
ocmd.Parameters.Add(pDescription);
OracleParameter pScriptText = new OracleParameter();
pScriptText.ParameterName = "pSCRIPT_TEXT";
pScriptText.SourceColumn = "SCRIPT_TEXT";
ocmd.Parameters.Add(pScriptText);
}
private DataSet BuildCLOBTestDataSet()
{
DataSet ads = new DataSet();
DataTable at = new DataTable("CLOBTEST");
DataColumn ac = at.Columns.Add("ACTION_CODE", typeof(string));
at.Columns.Add("DESCRIPTION", typeof(string));
at.Columns.Add("SCRIPT_TEXT", typeof(string));
at.PrimaryKey = new DataColumn[] { ac };
ads.Tables.Add(at);
return ads;
}
private OracleConnection OpenConnection()
{
OracleConnection oc = null;
try
{
OracleClientFactory ocf = new OracleClientFactory();
DbConnectionStringBuilder csb = ocf.CreateConnectionStringBuilder();
csb["Data Source"] = _ServerName;
csb["User ID"] = _UserName;
csb["Password"] = _Password;
string cs = csb.ConnectionString;
oc = new OracleConnection(cs);
oc.Open();
AddLogMessage("Connection opened.");
}
catch (Exception ex)
{
AddLogMessage("Error Opening Connection! " + ex.Message);
throw;
}
return oc;
}
private void AddLogMessage(string msg)
{
Debug.WriteLine(string.Format("{0:T} - {1}", DateTime.Now, msg));
}
}
Two workarounds are shown in this thread: https://community.oracle.com/thread/3649551
Workaround one:
Configure your CLOB parameters as OracleDbType.Clob and ParameterDirection.InputOutput.
Workaround two:
Configure your CLOB parameters as OracleDbType.Clob and explicitly set the value to a OracleClob object.
Fix:
The bug is now known to Oracle, so hopefully a fixed version will be released soon.
Edit: Patch 20361140 should fix it.
I was happy coding my application and I faced this issue that scary me a little bit.
I have a SQLite db file and when I try to read a table using the OdcbDataReader and load it into a table using the DataTable.Load I get different results on the column name depending on the application I'm working on.
Sometimes it returns table_name.column_name and sometimes it returns only column_name.
The code is only this:
public DataTable GetTable(string table_name)
{
table = null;
if (conn_str != null)
{
try
{
using (OdbcConnection conn = new OdbcConnection(conn_str.ToString()))
{
StringBuilder query = new StringBuilder();
query.Append("SELECT * ");
query.Append("FROM [");
query.Append(table_name + "]");
using (OdbcCommand cmd = new OdbcCommand(query.ToString(), conn))
{
conn.Open();
table = new DataTable();
using (OdbcDataReader dr = cmd.ExecuteReader())
{
DataSet ds = new DataSet();
ds.EnforceConstraints = false;
ds.Tables.Add(table);
table.Load(dr);
}
}
}
}
catch (Exception ex)
{
Debug.Print(ex.Message);
table = null;
}
}
return table;
}
The connection string used is exactly the same:
"DRIVER={SQLite3 ODBC Driver};DATABASE=databesename.db3;"
Any ideas why this is happening?
Don't resolve the issue but at least gives an workaround.
Adding a replace to column name to remove the table_name if the Reader insert it.
foreach (DataColumn col in table.Columns)
{
//Fix column names if the Reader insert the table name into the ColumnName
col.ColumnName = col.ColumnName.Replace(table_name + ".", "");
}
Code After the change:
public DataTable GetTable(string table_name)
{
table = null;
if (conn_str != null)
{
try
{
using (OdbcConnection conn = new OdbcConnection(conn_str.ToString()))
{
StringBuilder query = new StringBuilder();
query.Append("SELECT * ");
query.Append("FROM [");
query.Append(table_name + "]");
using (OdbcCommand cmd = new OdbcCommand(query.ToString(), conn))
{
conn.Open();
table = new DataTable();
using (OdbcDataReader dr = cmd.ExecuteReader())
{
DataSet ds = new DataSet();
ds.EnforceConstraints = false;
ds.Tables.Add(table);
table.Load(dr);
foreach (DataColumn col in table.Columns)
{
//Fix column names if the Reader insert the table name into the ColumnName
col.ColumnName = col.ColumnName.Replace(table_name + ".", "");
}
}
}
}
}
catch (Exception ex)
{
Debug.Print(ex.Message);
table = null;
}
}
return table;
}
I have filled a DataSet with a Table that was created from another database file. The table is NOT in the database file which I want to be able to copy the Table to.
Now I want to save all those records (DataTable) to a newly created SQLite database file...
How can i do that?
Also I really want to avoid loops if this is possible.
The best answer is by me :) so i'll share it.This is loop but writes 100k entries in 2-3secs.
using (DbTransaction dbTrans = kaupykliuduomConn.BeginTransaction())
{
downloadas.Visible = true; //my progressbar
downloadas.Maximum = dataSet1.Tables["duomenys"].Rows.Count;
using (DbCommand cmd = kaupykliuduomConn.CreateCommand())
{
cmd.CommandText = "INSERT INTO duomenys(Barkodas, Preke, kiekis) VALUES(?,?,?)";
DbParameter Field1 = cmd.CreateParameter();
DbParameter Field2 = cmd.CreateParameter();
DbParameter Field3 = cmd.CreateParameter();
cmd.Parameters.Add(Field1);
cmd.Parameters.Add(Field2);
cmd.Parameters.Add(Field3);
while (n != dataSet1.Tables["duomenys"].Rows.Count)
{
Field1.Value = dataSet1.Tables["duomenys"].Rows[n]["Barkodas"].ToString();
Field2.Value = dataSet1.Tables["duomenys"].Rows[n]["Preke"].ToString();
Field3.Value = dataSet1.Tables["duomenys"].Rows[n]["kiekis"].ToString();
downloadas.Value = n;
n++;
cmd.ExecuteNonQuery();
}
}
dbTrans.Commit();
}
In this case dataSet1.Tables["duomenys"] is already filled with all the data i need to transfer to another database. I used loop to fill dataset too.
When you load the DataTable from the source database, set the AcceptChangesDuringFill property of the data adapter to false, so that loaded records are kept in the Added state (assuming that the source database is SQL Server)
var sqlAdapter = new SqlDataAdapter("SELECT * FROM the_table", sqlConnection);
DataTable table = new DataTable();
sqlAdapter.AcceptChangesDuringFill = false;
sqlAdapter.Fill(table);
Create the table in the SQLite database, by executing the CREATE TABLE statement directly with SQLiteCommand.ExecuteNonQuery
Create a new DataAdapter for the SQLite database connection, and use it to Update the db:
var sqliteAdapter = new SQLiteDataAdapter("SELECT * FROM the_table", sqliteConnection);
var cmdBuilder = new SQLiteCommandBuilder(sqliteAdapter);
sqliteAdapter.Update(table);
If the source and target tables have the same column names and compatible types, it should work fine...
The way to import SQL data to SQLite will take long time. When you want to import data in millions, It will take lot of time. So the shortest and easiest way to do that is just fill fetch the data from SQL database in a DataTable and insert all its rows to SQLite database.
public bool ImportDataToSQLiteDatabase(string Proc, string SQLiteDatabase, params object[] obj)
{
DataTable result = null;
SqlConnection conn = null;
SqlCommand cmd = null;
try
{
result = new DataTable();
using (conn = new SqlConnection(ConStr))
{
using (cmd = CreateCommand(Proc, CommandType.StoredProcedure, obj))
{
cmd.Connection = conn;
conn.Open();
result.Load(cmd.ExecuteReader());
}
}
using (SQLiteConnection con = new SQLiteConnection(string.Format("Data Source={0};Version=3;New=False;Compress=True;Max Pool Size=100;", SQLiteDatabase)))
{
con.Open();
using (SQLiteTransaction transaction = con.BeginTransaction())
{
foreach (DataRow row in result.Rows)
{
using (SQLiteCommand sqlitecommand = new SQLiteCommand("insert into table(fh,ch,mt,pn) values ('" + Convert.ToString(row[0]) + "','" + Convert.ToString(row[1]) + "','"
+ Convert.ToString(row[2]) + "','" + Convert.ToString(row[3]) + "')", con))
{
sqlitecommand.ExecuteNonQuery();
}
}
transaction.Commit();
new General().WriteApplicationLog("Data successfully imported.");
return true;
}
}
}
catch (Exception ex)
{
result = null;
return false;
}
finally
{
if (conn.State == ConnectionState.Open)
conn.Close();
}
}
It will take a very few time as compare to upper given answers.