 |
|
 |
I used your code and it works fine in my project. Thanks!
|
|
|
|
 |
|
 |
Hi,
I have modified your code to detect exceptions of primary key on existing records on destination table. Now, the function create to cmd's. One for insert (yours) and other for update (me).
There is also the option to pass to the function a BackGroundWorker if somebody wants to inform the user the progress of the inserts / updates.
Finally, I have took care in the case that you are updating a table with only one field. If the copy fails, the code has only to ignore
(Update Table set ***** where field1=@field1 --> this case has to be ignored on updating)
(update table set field1=@field1 where ***** --> this too)
I hope this helps somebody, yes for me.
Im sorry by my english.
mouthbow
Here is the code
public static void CopyTable(IDbConnection source,
IDbConnection destination, String sourceSQL, String destinationTableName,System.ComponentModel.BackgroundWorker worker)
{
IDbCommand cmd = source.CreateCommand();
cmd.CommandText = sourceSQL;
int numfilas = 0;
int posfilas = 0;
if (worker != null)
{
IDataReader contarfilas = cmd.ExecuteReader();
while (contarfilas.Read()) { numfilas++; }
contarfilas.Close();
}
try
{
IDataReader rdr = cmd.ExecuteReader(CommandBehavior.KeyInfo);
DataTable schemaTable = rdr.GetSchemaTable();
IDbCommand insertCmd = destination.CreateCommand();
IDbCommand updateCmd = destination.CreateCommand();
string paramsSQL = String.Empty;
string comandoSQLactualizar = "Update " + destinationTableName + " set ";
string filtroSQLactualizar = "Where";
int numfiltros = 0;
//build the insert statement
for (int i = 0; i < schemaTable.Rows.Count; i++)
{
DataRow row = schemaTable.Rows[i];
bool eskey = (bool)row["iskey"];
string comaFinal = i < schemaTable.Rows.Count - 1 ? "," : "";
if (paramsSQL.Length > 0)
paramsSQL += ", ";
paramsSQL += "@" + row["ColumnName"].ToString();
if (eskey)
{
filtroSQLactualizar += (numfiltros > 0 ? " and " : "") + " " + row["ColumnName"].ToString() + "=@" + row["ColumnName"].ToString();
numfiltros++;
}
else
{
comandoSQLactualizar += " " + row["ColumnName"].ToString() + "=@" + row["ColumnName"].ToString() + comaFinal;
}
IDbDataParameter param = insertCmd.CreateParameter();
IDbDataParameter paramupd = updateCmd.CreateParameter();
param.ParameterName = "@" + row["ColumnName"].ToString();
param.SourceColumn = row["ColumnName"].ToString();
paramupd.ParameterName = "@" + row["ColumnName"].ToString();
paramupd.SourceColumn = row["ColumnName"].ToString();
if (row["DataType"] == typeof(System.DateTime))
{
param.DbType = DbType.DateTime;
paramupd.DbType = DbType.DateTime;
}
insertCmd.Parameters.Add(param);
updateCmd.Parameters.Add(paramupd);
}
insertCmd.CommandText =
String.Format("insert into {0} ( {1} ) values ( {2} )",
destinationTableName, paramsSQL.Replace("@", String.Empty),
paramsSQL);
updateCmd.CommandText = comandoSQLactualizar + " " + filtroSQLactualizar;
int counter = 0;
int errors = 0;
while (rdr.Read())
{
try
{
if (worker != null)
{
posfilas++;
worker.ReportProgress(posfilas * 100 / numfilas);
}
for (int k=0;k<insertCmd.Parameters.Count;k++)
{
IDbDataParameter paramins = (IDbDataParameter)insertCmd.Parameters[k];
IDbDataParameter paramupd = (IDbDataParameter)updateCmd.Parameters[k];
object colins = rdr[paramins.SourceColumn];
object colupd = rdr[paramupd.SourceColumn];
//foreach (IDbDataParameter param in insertCmd.Parameters)
//if (param.DbType == DbType.DateTime)
//{
// if (col != DBNull.Value)
// {
// if (((DateTime)col).Year < 1753)
// {
// param.Value = DBNull.Value;
// continue;
// }
// }
//}
paramins.Value = colins;
paramupd.Value = colupd;
}
insertCmd.ExecuteNonQuery();
}
catch (Exception ex)
{
if (ex.Message.IndexOf("PRIMARY KEY") > 0)
{
try
{
if (updateCmd.Parameters.Count>1)
updateCmd.ExecuteNonQuery();
}
catch (Exception exc)
{
rdr.Close();
throw exc;
}
}
else
{
//if (comandofinalactualizar != "") throw ex;
;
}
}
}
rdr.Close();
}
catch (Exception ex)
{
;
}
finally
{
//destination.Close();
//source.Close();
}
}
|
|
|
|
 |
|
 |
I have a problem where the cmdInsert returns a "column 'x' cannot be null" exception on every row. The problem is, I don't think x is null. When I step through the code the parameter @x is added and set with a integer value. I look through the debugger and everything has values but when that insertCmd executes it tosses the above exception.
Any clue why that would occur?
|
|
|
|
 |
|
 |
I have never seen that issue before. Can you share more of your code so I can get a better understanding of what you are trying to do?
|
|
|
|
 |
|
 |
Thanks for the response.
I hadn't changed any of the code from what you had, I found the issue though in case any one else experiences it. I was using your code in this instance with the MySql ADO.Net provider. The variable names on the parameterized statement the MySql provider expects won't work with the "@" symbol. Instead it requires a "?". I believe there are other providers that behave this way (SQL Server's default is "@"). Once I changed it to a ? the load proceded fine (and it explains why when I manually checked, all of the values were in place).
I created a Vb.Net class based off of your code for a friend of mine to use. Instead of it being a static procedure I created a class with a few properties. I liked your code for it's simplicity that I figured I'd add my own bells and whistles to it:
1.) The ability to select the variable symbol to use (the default is "@", the property can then be changed)
2.) IsDestinationSqlServer property to know whether to do the SQL Server specific date checks you used
3.) UseTransaction property. If set to true will use an IDBTransaction for the load. Loading a 26 record test table it sped up the load time by about 40% (and, it doesn't commit the records if there's an error).
4.) DeleteAction property. Whether or not to delete the destination table's data before hand. It can be set to Delete, Truncate or NoAction (NoAction being the default).
5.) ElapsedTime property. Shows the time it took to run the last CopyTable sub.
If you want, I can post a copy of what I translated/added to. Let me know. Great work, this is a simple/useful way to move data, especially between different database engines.
|
|
|
|
 |
|
 |
It would be great if you posted your changes. I really only used the library with SQL Server so I never ran into the other problems that you faced.
I am sure you will save someone else from a lot of problems by sharing your code!
|
|
|
|
 |
|
 |
This is exactly what I'm looking for. And, you used the IDbConnection so I can use it on things other than SQL Server (like, SQL Server to MySql, bravo!
|
|
|
|
 |
|
 |
all the best my friend you saved my lot of days work. one thing i want to ask you about how can i use a progress bar control to show the progress of database tables being copyied. i appreciate for all your help and good luck with your future..
|
|
|
|
 |
|
 |
Using a progress bar should be straight forward...
You would have to count the records in the table first using a "select count" sql statement. Then you could set the maximum value on the progress bar. Then each time you copy a record you could update the progress bar.
|
|
|
|
 |
|
 |
Hi,
The code doesn't seem to copy any tables between access if they have a date/time field. I've not yet worked out why, but it seems to be to do with the format of the date.
You get the following error:
"Data type mismatch in criteria expression."
Trace output:
insert into tblBasket ( [UserID], [ProductCode], [Time] )
values ( @UserID, @ProductCode, @Time )
System.Data.OleDb.OleDbException: Data type mismatch in criteria expression.
at System.Data.OleDb.OleDbCommand.ExecuteCommandTextErrorHandling(Int32 hr)
...
UserID --> @UserID = 2 Datatype: Int32
ProductCode --> @ProductCode = 2S00182 Datatype: String
Time --> @Time = 18/09/2007 16:24:15 Datatype: DateTime
It's the "time" field that causes the problem. I think Access expect dates in the format: #yyyy-MM-dd# but I don't know how to pass in dates with TIMES specified or how to alter the code to do this.
Nick...
|
|
|
|
 |
|
 |
Set the format in the field of the table to "yyyy-mm-dd". Also, when querying the data from access force it to use the right format by using the FORMAT function...
ex. Format([FieldName],"mm-dd-yyyy")
|
|
|
|
 |
|
 |
Hi,
Thanks for posting this great solution!
However I have a problem. I need to copy a table in Access which has an Autonumber column as the primary key. How can I copy the data without the ID of the data changing? When I try and run the code, it generates the following SQL:
insert into tblWebUsers ( ID, Username, Password, AccountNumber, EmailAddress, InvoiceAddressID, LastUsedDeliveryAddressID )
values ( @ID, @Username, @Password, @AccountNumber, @EmailAddress, @InvoiceAddressID, @LastUsedDeliveryAddressID )
...but then throws the error "Syntax error in INSERT INTO statement". I presume the syntax error is that i'm including the column ID which is an autonumber?
If I remove this column from the select statement, then the ID will change and all the other data that relates to that table will no longer be properly linked.
Any ideas how I can get round this?
Nick...
|
|
|
|
 |
|
 |
I assume the destination system is SQL Server?
In order to make the code work I would just disable the identity column in sql server until you are finished with the copy. This way you can preserve the values of the ID column.
|
|
|
|
 |
|
 |
No it's Access to Access. It's part of the installation routine for the product. So what it's actually doing is copying the existing user's data into the new product database (their favourites etc).
If it was SQL Server it would be easy because I could just enable IDENTITY INSERT inside a stored procedure. Access doesn't seem to have this, so I'm going to have to remove all the relationships, remove the autonumber primary key, copy the data and then recreate the autonumber and relationships.
I was hoping there would be an easier way?
|
|
|
|
 |
|
 |
I have a couple other ideas....
can't you just change the field to a number instead of an autonumber. The field will still be an integer type of the same precision so I do not think it will invalidate your relationships.
Also when looking for a solution via google I saw a lot of people referencing a similar problem. A lot of them mentioned that they got around this issue by using an Appendquery. Read this post:
http://www.dbforums.com/archive/index.php/t-1252826.html[^]
I hope this helps.
|
|
|
|
 |
|
 |
Actually it turns out that the autonumber isn't the problem after all. I still get the syntax error even if I change the column to be a normal number column.
Perhaps the problem is that the code always assumes all of the values are strings (param.DbType is always String even if the source column was an integer. Not sure if this would cause a syntax error or not.
If I copy and paste the insert statement into access, and manually enter values instead of using the parameters, it seems to work fine. So the syntax error must be caused by properties of the insert statement perhaps?
|
|
|
|
 |
|
 |
OK, the syntax error was a small bug in your code. It won't copy a table if the column is called "ID" because ID seems to be a keyword. I've modified the code to wrap all column names in square brackets.
string paramsSQL = String.Empty;
string paramsQuoted = String.Empty;
//build the insert statement
foreach (DataRow row in schemaTable.Rows) {
if (paramsSQL.Length > 0) paramsSQL += ", ";
if (paramsQuoted.Length > 0) paramsQuoted += ", ";
paramsSQL += "@" + row["ColumnName"].ToString();
paramsQuoted += "[" + row["ColumnName"].ToString() + "]";
IDbDataParameter param = insertCmd.CreateParameter();
param.ParameterName = "@" + row["ColumnName"].ToString();
param.SourceColumn = row["ColumnName"].ToString();
if (row["DataType"] == typeof(System.DateTime)) {
param.DbType = DbType.DateTime;
} else if (row["DataType"] == typeof(System.Int32)) {
param.DbType = DbType.Int32;
}
//HttpContext.Current.Trace.Warn(param.SourceColumn);
insertCmd.Parameters.Add(param);
}
(note that I've altered the debug line for use in an ASP.NET environment but change that back or remove it if you decide to update the published code).
I no longer get the syntax error
I still need to test some more to find out if the autonumber field copies correctly as currently it seems to be working but I don't understand WHY it works!
|
|
|
|
 |
|
 |
hmmm...I though access would do an implicit conversion in that case.
May need to write some conditional code in the method to detect the datatypes and set the parameters appropriately. Usually the code works fine if your source and destination tables have the same schema.
|
|
|
|
 |
|
 |
You probably CAN remove the bit that sets the DbType.
The important fix is the bit that wraps all the column names in square brackets.
eg
INSERT INTO tblTable (ID, Column2)
becomes:
INSERT INTO tblTable ([ID], [Column2])
..as otherwise, if any of your columns is a keyword or contains a space(!) then it won't work. I think you should modify your article to include at least this fix as it will make it much more reliable and the resulting error message ("Syntax Error") isn't very helpful. It could save people a lot of time and doesn't have any side-effects.
As you say, I think Access DOES do the implicit datatype conversion, but I didn't see any harm in leaving my modification in there (even though it didn't fix my original problem).
Nick...
|
|
|
|
 |
|
 |
The square brackets are a good idea. I will update the article. I appreciate the feedback.
I did not think about it because I generally would never but spaces in a columns name. It usually causes more problems.
|
|
|
|
 |
|
 |
Agreed - spaces in database columns are a stupid idea! However I don't always design the databases I have to work with and even some column names without spaces can cause problems - such as if the column name is a reserved word like ID, primaryKey, index etc
Again, thanks very much for sharing the code - it's saved me hours of time!
|
|
|
|
 |
|
 |
Amazing it worked and copied everything over from an Oracle db to a SQL 2005 db with no errors on the first try and saved me countless hours.
This rocks!
Al
|
|
|
|
 |
|
 |
When I hear that someone used my code, and it saved them time it makes me very happy. That is what codeproject is all about.
|
|
|
|
 |
|
 |
I updated the code to remove the calls to Program.Log. Replaced them with System.Diagnostics.Debug.WriteLine
|
|
|
|
 |