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!
Related
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
I have the following table in SQL server
projects
id(PK, int, not null)
name (varchar(255), not null)
public_key_token (varchar(50), null)
I have added a unique constraint to the name column using
ALTER TABLE dbo.projects
ADD CONSTRAINT name_unique UNIQUE (name);
which results in a Unique, Non-Clustered index on the table (trusting SSMS).
In the code I'm retrieving the table data using
using (SqlDataAdapter adapter = new SqlDataAdapter("select * from " + DbTabName, con))
{
using (DataTable table = new DataTable(DbTabName))
{
DataTable dt = adapter.FillSchema(table, SchemaType.Source);
PkColumns = dt.PrimaryKey.Select(c => c.ColumnName).ToList();
AutoIncrementColumns = dt.PrimaryKey.Where(c => c.AutoIncrement).Select(c => c.ColumnName).ToList();
UniqueColumns = dt.Columns.Cast<DataColumn>().Where(c => c.Unique).Select(c => c.ColumnName).ToList();
...
}
}
The PKs and AutoIncrement columns are OK but in UniqueColumns I only get the PK column again.
The name column arrives in C# without the Unique constraint.
Changing the SchemaType to Mapped did not alter the result.
Why do I lose this constraint on the way to C#? Am I missing something on the SQL Server side or in C#?
[UPDATE]
#Tim-Schmelter's answer only solves the problem half way.
Just adding the index did not work. Also adding the index when the PK on the id column exists doesn't work.
The only way I got it to work was delete the table, recreate it without any key and indexes and then add the unique index as in Tim's answer. However, after adding the PK for the id column once more I'm back to the old behaviour that only the id is listed as unique column.
This is really weird.
Use this to create a unique index or use the the gui of SSMS.
CREATE UNIQUE NONCLUSTERED INDEX [name_unique] ON [dbo].[projects]
(
name ASC
)
You have added a unique constraint not a unique index.
Your code now successfully retrieves the UniqueColumns(the single column name).
I have a .Net dataset and am adding a row to a table. This works and the record is saved to the database. How do I get the updated version of my row after the insert. Or, alternatively, how do I know the ID of the item that was added (so that I can then use it in a subsequent child table insert.
MyDataSet.ProjectRow r = dsMyDataSet.Projects.AddProjectRow(txtTitle.Text);
m_daProjects.Update(dsMyDataSet.Projects);
// What is the ID of the new item here?
If the column is an identity column you can find the new ID's in the inserted rows.
You: "thanks. which object maintains a list of inserted rows?"
You can use DataTable.GetChanges(DataRowState.Added) to get a DataTable with all DataRows which are going to be added. You need to use it before AcceptChanges was called. If i remember correctly TableAdapter.Update calls AcceptChanges at the end. Then you need to use it before m_daProjects.Update(dsMyDataSet.Projects):
DataTable addedRows = ds.modModel.GetChanges(DataRowState.Added);
MyDataSet.ProjectRow r = dsMyDataSet.Projects.AddProjectRow(txtTitle.Text);
m_daProjects.Update(dsMyDataSet.Projects);
now addedRows contains all DataRows with the new identity value in each row
foreach(DataRow addedRow in addedRows.Rows)
Console.WriteLine("New ID: {0}", addedRow.Field<int>("IdColumn"));
Update: However, in your case it's simpler. You have already the single row that you want to insert. So you dont need to call DataTable.GetChanges at all.
You can see the new identity value in the (typed DataRow) ProjectRow r after Update.
Thanks to Tim Schmelter. In the link he posted there's a reference to an article on Beth Massi's blog with a complete walkthrough of the solution. It worked for me.
http://blogs.msdn.com/bethmassi/archive/2009/05/14/using-tableadapters-to-insert-related-data-into-an-ms-access-database.aspx
The basic steps are:
1) Add RowUpdated event handler on the strongly typed table adapter. This event handler issues a new OleDBCommand to the database to retrieve ##Identity and then assigns the integer to the member column of the table.
public void _adapter_RowUpdated(dynamic sender, System.Data.OleDb.OleDbRowUpdatedEventArgs e)
{
HMUI.Classes.AccessIDHelper.SetPrimaryKey(this.Connection, e);
}
public static void SetPrimaryKey(OleDbConnection trans, OleDbRowUpdatedEventArgs e)
{
if (e.Status == System.Data.UpdateStatus.Continue && e.StatementType == System.Data.StatementType.Insert)
{
if (pk != null)
{
OleDbCommand cmdGetIdentity = new OleDbCommand("SELECT ##IDENTITY", trans);
// Execute the post-update query to fetch new ##Identity
e.Row.Table.Columns[pk(0)] = Convert.ToInt32(cmdGetIdentity.ExecuteScalar());
e.Row.AcceptChanges();
}
}
}
2) In the constructor of the form using the dataset and table adapter I attach the function in step 1 to the RowUpdated event on the table adapter's internal data adapter.
// Event to handle inserted records and retrieve the primary key ID
m_daDataSources.Adapter.RowUpdated += new System.Data.OleDb.OleDbRowUpdatedEventHandler(m_daDataSources._adapter_RowUpdated);
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 a Profile(as shown in below image) Table Which whenever I try to remove a row from it, I face this error which indicates there is still data in foreign key in Tempprice table which is related to the Id in Lots table .
the problem is when its Lots table turn to Delete , there is still rows in Tempprice which have different Userid but same lotsid that have Ownerid or Winnerid which I want to Delete.
And I don't know how to delete this rows!? or which query is needed?
Database Relationships and Tables:
Code:
while (checkbox.Checked)
{
// Retreive the ID
int ida = Convert.ToInt32(GridView1.DataKeys[row.RowIndex].Value);
// Pass the value of the selected Id(s) to the Delete //command.
//These numbers indicate in which order tables shoulde be deleted
/*1*/
new BLL.LoginBLL().Delete(ida);
/*2*/
new BLL.MessageBLL().Delete(ida);
/*3*/
new BLL.JointBLL().Delete(ida);
/*4*/
new BLL.TemppriceBLL().Delete(ida);
/*5*/
new BLL.LotsBLL().Delete(ida);
/*6*/
new BLL.AuctionBLL().Delete(ida);
/*7*/
new BLL.ProfileBLL().DeleteProfile(ida);
checkbox.Checked = false;
}
ShowUsers();
To delete references, you need to set the relation (by selecting the line connecting the two objects) to Cascade for the End 1 On Delete property.
Then you need to Load the referenced object or collection. Calling Delete in this case, will delete your object and all objects it references.