I have a relational database connecting meal_ingredients and ingredient nutritional values (see here for further information), utilising PostgreSQL. Within a WinForms application, there is a button that takes values from a DataGridView and then places each row into an array.
Each row in the DataGridView is an ingredient with its nutritional values. Above the DataGridView, a textbox takes a string for the meal name. Upon clicking the button (code below), the array values do one of two things:
If the meal name (meal_name being the PK) already exists in the meal_ingredients table, all rows in the database containing this string are removed. The rows from the DataGridView are then inserted, effectively 'overwriting' the ingredients for that meal.
If the meal name does not exist in the meal_ingredients table, the rows plus the meal name, entered into the textbox, are simply appended to the table.
In my code, as you can see once the data is placed into the array, a connection is made with the database, and the results from the SELECT query loaded into a DataTable.
The loop which follows triggers a MessageBox if the meal_name field matches the string value in the textbox. This works fine.
My issue is as follows. For however many rows exist in the DataGridView, the MessageBox will fire off that many times; so with two rows, I will see two MessageBoxes, for example. This, per se, is not a problem, unless replacing this MessageBox with DELETE and INSERT statements would throw an error.
In place of MessageBox.Show("test");, I would instead place a SQL statement to remove any records where meal_name == txtMealName.Text and then a second SQL statement to insert new records based upon the DataGridView rows. Of course, if the MessageBox fires off according to the number of rows, I expect the SQL would also occur that many times. Again, this is fine in principle. But I am just wondering if this would cause a conflict of any kind (that is, for example, the SQL throwing an exception because there are no remaining rows to delete)?
private void btnMealAdd_Click(object sender, EventArgs e)
{
if (txtMealName.Text != "")
{
foreach (DataGridViewRow row in dgvMealIngredient.Rows)
{
List<string> macroList = new List<string>();
macroList.Add(row.Cells[0].Value.ToString());
macroList.Add(row.Cells[9].Value.ToString());
macroList.Add(txtMealName.Text);
String[] str = macroList.ToArray();
NpgsqlConnection conn = new NpgsqlConnection(Globals.connectionString());
conn.Open();
NpgsqlCommand comm = new NpgsqlCommand();
comm.Connection = conn;
comm.CommandType = CommandType.Text;
comm.CommandText = "SELECT * FROM meal_ingredients";
NpgsqlDataReader dr = comm.ExecuteReader();
DataTable dt = new DataTable();
dt.Load(dr);
foreach (DataRow dataRow in dt.Rows)
{
if (dataRow[0].ToString() == txtMealName.Text)
{
MessageBox.Show("test");
}
Debug.WriteLine(dataRow[0]);
}
}
}
else
{
MessageBox.Show("error: enter a meal name");
}
}
A simplified form of the database relation (note that qty is one of the fields in the DataGridView):
just whether replacing that with INSERT or DELETE will throw an error
An SQL DELETE can be typically run multiple times without error. There either will be some rows for it to delete or there will not but it will only normally result an error if there are dependent records in another table and no arrangement for them to be deleted or disconnected in cascade fashion. It is not an error for a DELETE statement to affect 0 rows
An SQL INSERT can typically only be run multiple times when it is not subsequently(after the first run) inhibited by the presence of a unique constraint on one or more of the columns. As most tables you design should really have a primary key, you can only insert a row with a unique value for the key column. If you aren't devolving generation of the value to the database then re-running an identical INSERT will fail on the second run. If the table depends on another table to have a related row and a foreign key constraint backs this up, then an insert that doesn't relate to a row in the parent table will fail on first run
Related
My situation involves batch updates to individual tables in an SQLite database through ADO.NET objects. I use the DataAdapter.Update() method to push the changes which works well:
DataTable changes = dataset.Tables[table].GetChanges();
if (changes == null) return 0;
SQLiteCommandBuilder scb = new SQLiteCommandBuilder(adapter);
scb.ConflictOption = ConflictOption.CompareRowVersion;
int cnt = adapter.Update(changes);
return cnt;
However each time a record is inserted I also want the local DataSet tables to reflect with the newly inserted row id. For this I use the adapter_RowUpdated event :
static void adapter_RowUpdated(object sender,
System.Data.Common.RowUpdatedEventArgs e)
{
if (e.StatementType == StatementType.Insert)
{
SQLiteCommand cmd = new SQLiteCommand("select last_insert_rowid();", conn);
e.Row["id"] = cmd.ExecuteScalar();
}
}
The above fetches last_insert_rowid() because I'm able to see it when I debug by putting a breakpoint. However, the assignment statement to e.Row["id"] isn't working. The id change isn't reflected in my original DataSet and DataTable objects. For example when I test the following value (N refers to the specific row index), it still has a DBNull value. What is going wrong here? How can I ensure that the specific row which just got inserted is updated with its corresponding id field value?
dataset.Tables["projects"].row[N]["id"];
After a little experimenting, I found the solution to this myself.
As strange as it may sound but it looks like adapter.Update() requires a dataset along with the actual table name in order for this to work. I was passing the table object (DataTable.GetChanges()) so far which did the job of updating the database but failed only in this particular scenario. The moment I did that, the inserted id started reflecting in rows all over the dataset!
//int cnt = adapter.Update(changes); // doesn't work
int cnt = adapter.Update(dataset, tableName); // works perfectly!
edit
Lo and Behold! It even works when I just pass the table like this instead of entire dataset. It was only causing problem when I was just passing the changes table (got from dataset.Tables[tableName].GetChanges()).
int cnt = adapter.Update(dataset.Tables[tableName]); // works perfectly!
I have a DataGridView with a bit column which shows as a checkbox. The column is "IsDefault" by name, implying that if this cell is checked, all other cells in this column in the DataTable should be unchecked (only 1 cell in this column is allowed to be the default).
I managed to get this behaviour working by handling the change in CellContentClick event of the DataGridView, i.e.:
private void ViewIconsDataGrid_CellContentClick(object sender, DataGridViewCellEventArgs e)
{
if (e.RowIndex == -1 || e.ColumnIndex == -1)
return;
DataTable table = ViewIconsDataGrid.DataSource as DataTable;
DataGridViewCell cell = ViewIconsDataGrid.Rows[e.RowIndex].Cells[e.ColumnIndex];
Guid rowID = (Guid)ViewIconsDataGrid.Rows[e.RowIndex].Cells["ID"].Value;
if (cell.GetType() == typeof(DataGridViewCheckBoxCell))
{
DataGridViewCheckBoxCell checkBoxCell = cell as DataGridViewCheckBoxCell;
bool value = (bool)checkBoxCell.EditedFormattedValue, previous = (bool)checkBoxCell.Value;
if (value != previous)
{
if (value == true)
{
Guid currentDefault;
if(MyIcons.Defaults.TryGetValue(MyGroupID, out currentDefault))
{
DataRow [] rows = table.Select("ID = '" + currentDefault.ToString() + "'");
if(rows.Length == 1)
{
rows[0]["IsDefault"] = false;
}
}
}
ViewIconsDataGrid.EndEdit(); table.AcceptChanges();
}
}
}
Now, clearly I'm trying to make changes to two records in the same DataTable here. The first I'm hoping the DataGridView changes (when the user changes the state of the checkbox) and then I'm doing another change on the DataTable to uncheck whichever row already had the check, hoping that will propagate up to the DataGridView. I need to commit both changes to the database of course.
Here's my database "update" code, which is kind-of generic, i.e. I pass in a select query for the whole table and a data table to update against:
using (SqlConnection connection = new SqlConnection(ConfigurationManager.AppSettings["Connection"]))
{
connection.Open();
using (SqlDataAdapter adapter = new SqlDataAdapter(MyQuery, connection))
{
adapter.InsertCommand = new SqlCommandBuilder(adapter).GetInsertCommand();
adapter.DeleteCommand = new SqlCommandBuilder(adapter).GetDeleteCommand();
adapter.UpdateCommand = new SqlCommandBuilder(adapter).GetUpdateCommand();
adapter.Update(MyTable);
}
}
So, the problem is that I get an exception when I try to update the table after making these changes: "Concurrency violation: the UpdateCommand affected 0 of the expected 1 records.". If there is no existing default, just a single record gets updated and all is fine.
Can anyone spot my mistake, or see an easier way for me to do this?
Edit 1: In the above fragment, MyQuery looks like this:
#"SELECT * From Picture WHERE ID_Group = '{0}' AND Deleted = 0 ORDER BY Created DESC
{0} is the guid of the group of pictures I'm updating, i.e. a select query gives all records in the Pictures table which are members of that group.
The call to AcceptChanges changes the RowState of every row in the table to Unchanged.
The subsequent call to DataAdatper.Update(table) doesn't find anything to update/insert or delete.
You need to call the Update on user input (like a click on a Save button )
EDIT To complete this answer with the findings below from #Robinson
You could raise the RowChanged event for the table handling the CurrentCellDirtyStateChanged event and manually commit the edit on the DataGridView. Otherwise the RowChanged event is raised when the row lost focus.
There are two problems here:
First, DataGridView events don't send to cache the changes correctly, you must use an other event, like a button click, to force DataGridView to cache the changes.
The other problem "concurrency violation" its caused by inserting a new row in a table with an Identity column (usually the primary key). I suggest use another button for inserts and force the new identity value in the DataGridView.
Whene you updates, i suggest to make deletes first, and next make the updates:
// First process deletes.
da.Update(dt.Select(null, null, DataViewRowState.Deleted));
// Next process updates.
da.Update(dt.Select(null, null, DataViewRowState.ModifiedCurrent));
I am working with C# Window Form, I have created a form connect to mySQL database and it display list of databases, list of tables on each database and also tables contents of each table.
Questions that I have here:
After I selected a random cell in the table (datagridview) and click Delete button, I want that row (corresponding to the selected cell) to be deleted on the database.
I also need that the datagridview table content also will be refreshed and updated (with that row has been removed). (This part I think I can do if I know how to do the part 1)
So I need help with the question 1, one of those things I can't not figure out is that how can I write the SQL statement to put in the SQLadapter or SQLcommandbuilder or whatever it is. I know the SQL statement should be like:
Delete from (selected Table) Where (THIS IS THE PART WHERE I STUCK AT) => I dont know what to put in this condition, how to get it?
Any helps and advises is really appreciated!
The delete statement should consider all the selected table primary key columns and the selected row from the datagridview.
How to get the primary key columns:
SELECT `COLUMN_NAME`
FROM `information_schema`.`COLUMNS`
WHERE (`TABLE_SCHEMA` = 'dbName')
AND (`TABLE_NAME` = 'tableName')
AND (`COLUMN_KEY` = 'PRI');
Source: A better way to get Primary Key columns
How your delete statement should look like:
DELETE FROM <TABLE>
WHERE <PRIMARY_KEY_COLUMN_1> = <ROW_VALUE_1>
AND <PRIMARY_KEY_COLUMN_2> = <ROW_VALUE_2>
You see, the table could have multiple columns uniquely identifying a row. There is also the possibility of existing a reference for that very row on another table, which would prevent you from deleting it.
It would look like this:
List<string> primaryKeyColumns = GetPrimaryKeyColumns(SelectedDB, SelectedTable);
string deleteWhereClause = string.Empty;
foreach (string column in primaryKeyColumns)
{
DataGridViewRow row = datagridview.CurrentCell.OwningRow;
string value = row.Cells[column].Value.ToString();
if (string.IsNullOrEmpty(deleteWhereClause))
{
deleteWhereClause = string.Concat(column, "=", value);
}
else
{
deleteWhereClause += string.Concat(" AND ", column, "=", value);
}
}
string deleteStatement = string.Format("DELETE FROM {0} WHERE {1}", SelectedTable, deleteWhereClause);
The method GetPrimaryKeyColumns returns the names of all the primary key columns of the selected table using the select statement i posted.
You would also have to deal with other types of columns such as dates and strings, but that's basically what you will have.
I have been going through various sites and codes, but nothing seems to end my misery. Either they help to find and remove duplicates for a specific column or they remove only from the datatable, not the underlying database itself. I want to delete duplicate rows from table "table1" of my mdb file.
To make my requirements clearer:
there's no primary key set for any column in the table (and I cant afford to)
I want to delete all duplicate rows but one! (order has no significance)
I prefer deleting the duplicates from database more than first checking if such a row exist or not before updating database itself (if that's the last resort, which cant be, then that's welcome)
by duplicate rows I mean rows that are not distinct. for eg, in the following example,only 3rd and 5th row are duplicates. And I want to delete any of them.
Name1 Name2 Name3
tom dick harry
tom dick mike
ann sara mike
sara ann mike
ann sara mike
The duplicate rows should be deleted from database with a button click as follows
private void button1_Click(object sender, EventArgs e)
{
deletedupes();
}
private void deletedupes()
{
OleDbConnection con = new OleDbConnection("PROVIDER=Microsoft.Jet.OLEDB.4.0; Data Source=C:\\hi.mdb");
DataSet ds = new DataSet();
OleDbDataAdapter da = new OleDbDataAdapter("select * from table1", con);
con.Open();
da.Fill(ds, "table1");
// what could be rest of the code??
}
Thanks in advance. Yes I'm a novice..
If you haven't realized it already, database engines tends to think in absolutes. If you want it to delete a row, you have to tell it how to identify that row. Thus, primary keys.
Having said that, there is generally, but not always, two (2) ways you can do this:
Find out if Access supports syntax to tell DELETE to only consider the "first N rows", similar to DELETE TOP 1 FROM ...
Grab a distinct dataset from your table, delete all the rows in it, and insert the distinct rows back into it
The first might be possible, but it depends on whether Access supports any syntax that makes it possible. For instance. Microsoft SQL Server supports executing a statement SET ROWCOUNT 1 before a DELETE, and then DELETE will delete only 1 row, and then stop. I don't know if Access will do that.
The second will be a pain if you have foreign keys, but I'm going to go out on a limb here and assume that since you don't have primary keys, you don't have foreign keys, so data integrity is not a real problem here.
Here is an article discussing several approaches for deleting duplicate rows in SQL Server, but I suspect it would apply to MS Access, as well: Removing Duplicates from a Table in SQL Server
Ok, this is a complete hack, but it sounds like that's your only option...
Do a SELECT DISTINCTROW from your table. Delete all records from your table. Insert the distinct rows back in.
DISTINCTROW Syntax.
I have a similar problem were the values on rows are indentical but should keep only 1 row per combination of 2 columns. I was thinking about COUNT() and GROUP BY with HAVING COUNT() > 1 to get the combinations of these columns that have more than one occurence in a table. Then using recodset object from DAO to get rows, skip first and delete the rest. This is slow and cumbersome but works without adding a primary key.
As none of the answers were satsifactory for me (I'm just a tad too novice to understand the succint and slightly technicalized way spoken here by more knowledgable and experienced people), i tried my own variant to get this done. I could not follow what to be done with commands like distinct or set rowcount or delete from etc. Nowhere I could find a fully deployed code in an example. So I tried this. From scratch.
int id, k;
private void button2_Click(object sender, EventArgs e)
{
OleDbConnection con = new OleDbConnection("PROVIDER=Microsoft.Jet.OLEDB.4.0; Data Source=C:\\hi.mdb");
DataSet ds = new DataSet();
OleDbDataAdapter da = new OleDbDataAdapter("select * from table2", con);
con.Open();
da.Fill(ds, "table2");
for (int i = 0; i < ds.Tables["table2"].Rows.Count; i++)
{
DataRow row = ds.Tables["table2"].Rows[i];
k++;
for (int j = k; j < ds.Tables["table2"].Rows.Count; j++)
{
DataRow row2 = ds.Tables["table2"].Rows[j];
if (row.ItemArray.GetValue(1).ToString() == row2.ItemArray.GetValue(1).ToString())
{
if (row.ItemArray.GetValue(3).ToString() == row2.ItemArray.GetValue(3).ToString())
{
id = int.Parse(row2.ItemArray.GetValue(0).ToString());
deletedupes(id);
}
}
}
}
con.Close();
}
private void deletedupes(int num)
{
OleDbConnection con = new OleDbConnection("PROVIDER=Microsoft.Jet.OLEDB.4.0; Data Source=C:\\hi.mdb");
con.Open();
OleDbCommand c = new OleDbCommand("Delete from table2 where id =?", con);
c.Parameters.AddWithValue("id", num);
c.ExecuteNonQuery();
con.Close();
}
Edit: Sorry, I missed to say that I did use a unique column having a primary key to get this done. Nevertheless, this can be done without that as well. Just a matter of choice. And for unknown reasons, this method seems so fast too..
I have a winforms application with two DataGridViews displaying a master-detail relationship from my Person and Address tables. Person table has a PersonID field that is auto-incrementing primary key. Address has a PersonID field that is the FK.
I fill my DataTables with DataAdapter and set Person.PersonID column's AutoIncrement=true and AutoIncrementStep=-1. I can insert records in the Person DataTable from the DataGridView. The PersonID column displays unique negative values for PersonID. I update the database by calling DataAdapter.Update(PersonTable) and the negative PersonIDs are converted to positive unique values automatically by SQL Server.
Here's the rub. The Address DataGridView show the address table which has a DataRelation to Person by PersonID. Inserted Person records have the temporary negative PersonID. I can now insert records into Address via DataGridView and Address.PersonID is set to the negative value from the DataRelation mapping. I call Adapter.Update(AddressTable) and the negative PersonIDs go into the Address table breaking the relationship.
How do you guys handle primary/foreign keys using DataTables and master-detail DataGridViews?
Thanks!
Steve
EDIT:
After more googling, I found that SqlDataAdapter.RowUpdated event gives me what I need. I create a new command to query the last id inserted by using ##IDENTITY. It works pretty well. The DataRelation updates the Address.PersonID field for me so it's required to Update the Person table first then update the Address table. All the new records insert properly with correct ids in place!
Adapter = new SqlDataAdapter(cmd);
Adapter.RowUpdated += (s, e) =>
{
if (e.StatementType != StatementType.Insert) return;
//set the id for the inserted record
SqlCommand c = e.Command.Connection.CreateCommand();
c.CommandText = "select ##IDENTITY id";
e.Row[0] = Convert.ToInt32( c.ExecuteScalar() );
};
Adapter.Fill(this);
SqlCommandBuilder sb = new SqlCommandBuilder(Adapter);
sb.GetDeleteCommand();
sb.GetUpdateCommand();
sb.GetInsertCommand();
this.Columns[0].AutoIncrement = true;
this.Columns[0].AutoIncrementSeed = -1;
this.Columns[0].AutoIncrementStep = -1;
You need to double click the relationship in the dataset designer, and select Cascade Updates. When your real SQL server generated PK values for your Person table are generated, it will automatically set the foreign key values in the address table as well.
You don't need to do any of that RowUpdated event stuff. Its built into the dataset functionality.
I had a similar problem, but my solution was a little different.
#Noel Kennedy: Your solution does not work with SQL Server 2005 CE, because it doesn't support multiple statements and the TableAdapter won't generate the refresh code needed to update the autoincrement columns in the parent table.
NOTE: You still need Cascade Updates in the relationship so the child tables get updated.
I also add a method in my TableAdapter, which is generic enough to just copy/paste in all your parent TableAdapters. The only thing that I change is the identity row type and index (if needed). I also add a query to the TableAdapter called GetIdentity(). You can add it to the TableAdapter in the dataset designer by adding a scalar query with sql="SELECT ##IDENTITY;"
Now the custom function is:
public int InsertAndRefresh(System.Data.DataTable dataTable)
{
int updated = 0;
System.Data.DataRow[] updatedRows = dataTable.Select("", "", System.Data.DataViewRowState.Added);
bool closed = (this.Connection.State == System.Data.ConnectionState.Closed);
if (closed)
this.Connection.Open();
foreach (System.Data.DataRow row in updatedRows)
{
updated+=this.Adapter.Update(new global::System.Data.DataRow[] { row });
decimal identity = (decimal)this.GetIdentity();
row[0] = System.Decimal.ToInt64(identity);
row.AcceptChanges();
}
if (closed)
this.Connection.Close();
return updated;
}
You want to call this on the parent first. Then do everything as usual (update parent and then children).
Cheers!