Purpose of ForeignKeyConstraint.AcceptRejectRule in ADO.Net? - c#

What is the purpose in life of the AcceptRejectRule property of ForeignKeyConstraint class in ADO.Net?
The MSDN Document doesn't carry sufficient explanation (for me) to make its purpose clear. After reading the documentation, I thought that setting the property to None will prevent cascading of any changes from the parent table to the child table. But, this assumption was proved to be wrong after running the following code:
DataTable table1 = new DataTable("Customers");
table1.Columns.Add(new DataColumn("CustomerID", typeof(int)));
table1.Columns.Add(new DataColumn("CustomerName", typeof(string)));
DataTable table2 = new DataTable("Orders");
table2.Columns.Add(new DataColumn("OrderID", typeof(int)));
table2.Columns.Add(new DataColumn("CustomerID", typeof(int)));
DataSet dataSet = new DataSet();
dataSet.Tables.AddRange(new DataTable[] { table1, table2 });
dataSet.EnforceConstraints = true;
DataRelation dataRelation = new DataRelation("CustomerOrders", table1.Columns["CustomerID"],
table2.Columns["CustomerID"], true);
dataSet.Relations.Add(dataRelation);
Debug.WriteLine("No. of constaints in the child table = {0}", table2.Constraints.Count);
dataRelation.ChildKeyConstraint.AcceptRejectRule = AcceptRejectRule.None;
dataRelation.ChildKeyConstraint.DeleteRule = Rule.Cascade;
dataRelation.ChildKeyConstraint.UpdateRule = Rule.Cascade;
table1.Rows.Add(new object[] { 11, "ABC" });
table1.Rows.Add(new object[] { 12, "XYZ" });
table2.Rows.Add(new object[] { 51, 12 });
table2.Rows.Add(new object[] { 52, 11 });
table2.Rows.Add(new object[] { 53, 11 });
table1.Rows.RemoveAt(0);
table1.AcceptChanges();
table2.AcceptChanges();
Debug.WriteLine("No of rows in the parent table = {0}", table1.Rows.Count);
Debug.WriteLine("No of rows in the child table = {0}", table2.Rows.Count);
The output of the above code is:
No. of constaints in the child table = 1
No of rows in the parent table = 1
No of rows in the child table = 1
Thanks,
Dinesh

To avoid cascading, you need to set DeleteRule and UpdateRule to Rule.None.
I'm not certain but I believe AcceptRejectRule only affects whether the accept/reject command itself is cascaded or not. In your code, I would guess that the changes have been cascaded (since that's how DeleteRule and UpdateRule have been set) but only the changes on table1 have been accepted; the changes on table2 have not been accepted.

Here is my understanding. Let's say you have a parent child relationship and you set the relation.acceptrejectrule to cascade. You also have the dataadapter.acceptchangesduringupdate set to true, if you modify the parent and child record and then do a dataadapter.update on the parent first, the parent AND child records get "accepted" and if you do a subsequent dataadapter.update on the child records nothing will get updated (cause the "accept" was cascaded from parent to child records). So your parent got updated but not the child records. Possible solutions would be update the child recs first, then the parent, or simply set the relation.acceptrejectrule to none. By doing that the "accept" will not cascade down to the child recs and you will be able to update them. When you do an update on the child recs they, too, will be "accepted" because you have the dataadapter.acceptchangesduringupdate set to true. Of course, you can set acceptchangesduringupdate to false and do a manual dataset.acceptchanges to accept all the changes in the dataset. But if you do this, make sure you have done updates on all tables in the dataset.
I'm only stating here what I think is happening based on my testing. If anybody else knows differently please jump in.

Related

Insert current row from gridview into database

I'm supposed to create a master-detail form and I must add the details straight on the datagridview which is binded to the database. So I have a form with two tables: intrari (master) and intrari_detaliu (detail).
When I use the binding navigator to select a row in table intrari which is the parent table, I also get the corresponding details in table intrari_detaliu. I use text boxes/combox to add value in table intrari.
So how do I insert values straight into data grid view?
image for the form visual structure
What I tried:
First try:
DataClasses1DataContext db = new DataClasses1DataContext();
private void btnadd_Click(object sender, EventArgs e)
{
int ID; int id_intrari = 0; int id_produs = 0; decimal cantitate = 0; decimal valoare = 0;
for (int i = 0; i <tbl_intrari_detaliuDataGridView.RowCount - 1; i++)
{
if row
ID = Convert.ToInt32(tbl_intrari_detaliuDataGridView.Rows[i].Cells[0].Value.ToString());
id_intrari = Convert.ToInt32(tbl_intrari_detaliuDataGridView.Rows[i].Cells[1].Value.ToString());
id_produs = Convert.ToInt32(tbl_intrari_detaliuDataGridView.Rows[i].Cells[2].Value.ToString());
cantitate = Convert.ToDecimal(tbl_intrari_detaliuDataGridView.Rows[i].Cells[3].Value.ToString());
valoare = Convert.ToDecimal(tbl_intrari_detaliuDataGridView.Rows[i].Cells[4].Value.ToString());
var st = new tbl_intrari_detaliu
{
ID= ID,
id_intrari = id_intrari,
id_produs = id_produs,
cantitate = cantitate,
valoare = valoare,
};
db.tbl_intrari_detalius.InsertOnSubmit(st);
db.SubmitChanges();
}
}
but this first copies every row above and then it adds the new row. E.g.: I already have inserted row A and B, and when I want to insert row C, it first inserts row A and B again and inserts then C.
Second try:
public partial class Intrari2 : Form
{
SqlConnection con;
SqlDataAdapter adap;
DataSet ds;
SqlCommandBuilder cmbl;
private void Intrari2_Load(object sender, EventArgs e)
{
con = new SqlConnection();
con.ConnectionString = #"Data Source=DESKTOP-KVMM566;Initial
Catalog=MyDatabase;Integrated Security=True";
con.Open();
adap = new SqlDataAdapter("select ID,id_intrari,id_produs,cantitate,valoare from tbl_intrari_detaliu", con);
ds = new System.Data.DataSet();
adap.Fill(ds, "tbl_intrari_detaliu");
tbl_intrari_detaliuDataGridView.DataSource = ds.Tables[0];
this.tbl_intrari_detaliuTableAdapter.Fill(this.myDatabaseDataSet.tbl_intrari_detaliu);
this.tbl_IntrariTableAdapter.Fill(this.myDatabaseDataSet.tbl_Intrari);
//Add button------------
private void btnadd_Click(object sender, EventArgs e)
{
cmbl = new SqlCommandBuilder(adap);
adap.Update(ds, "tbl_intrari_detaliu");
//the Fill method doesn't work anymore because of the code above
this.tbl_intrari_detaliuTableAdapter.Fill(this.myDatabaseDataSet.tbl_intrari_detaliu);
}
}
This one updates rows correctly, but when I select a row in table intari, I don't get the corresponding details (according to the id) in table intrari_detaliu. I get the details for all rows in the parent table. I know it's because of the code I added to select the data from table using SqlAdapter, but the code doesn't work without that statement.
So, I need to insert/update/delete a selected row from "intrari_detaliu" and still be able to get the corresponding details when I select a row in the parent table "intrari".
Can you help me, please?
It's so much easier than that. Throw all that code away (genuinely, all of it). Here is a demo using two of my test tables. 1 Car (parent) has many Clowns (child)
Here's how your data (should) get into your parent grid. You have a query in your TA that selects stuff based on something you want to search by, eg Name:
You give the queries GOOD NAMES, not just Fill - one day you'll have many queries, don't do Fill, Fill1, Fill2. Name them well now:
In your child tableadapter you provide a query that does a lookup by Parent ID:
Or you do a query that looks up children of the same thing you just made a parent query for, like CarName here:
Or you can do both/more/whatever. You can have many queries per TA:
To get grids onto your form you drag them out of Data Sources (View menu, Other Windows). This will do all the data binding setup for you; you can examine how visual studio has done it if you ever want to replicate it manually; the parent grid binds through a binding source, to the table in the dataset. The child grid binds through a binding source to a data relation on the parent binding source
To get related data behavior you drag the CHILD NODE Clowns, not the top level Clowns out of data sources
In your code, to fill the grids with data you just actually fill the tables; the grids will update themselves. You must fill the parent first, then the child. You fill the child either using the query that selects a lot of children based on the parent criteria (like my ...ByCarName),
this.carsTableAdapter.FillByCarName(this.dataSet1.Cars, "Billy"); //fill parent first
this.clownsTableAdapter.FillByCarName(this.dataSet1.Clowns, "Billy"); //then fill the children by the same
or you can enumerate the parent after you fill, and fill the children by the parent ID (like my ...ByCarId does):
this.carsTableAdapter.FillByCarName(this.dataSet1.Cars, "Billy"); //fill parent first
clownsTableAdapter.ClearBeforeFill = false; //turn this off otherwise every call to FillBy.. erases the previous rows
foreach (var r in this.dataSet1.Cars) //for each parent
clownsTableAdapter.FillByCarId(this.dataSet1.Clowns, r.CarId); //fill the children
Saving
Dragging the grids wrote this:
this.Validate();
this.carsBindingSource.EndEdit();
this.tableAdapterManager.UpdateAll(this.dataSet1);
Add a line:
this.Validate();
this.carsBindingSource.EndEdit();
this.clownsBindingSource.EndEdit();
this.tableAdapterManager.UpdateAll(this.dataSet1);
That is all you need to do; click the button that runs that code; the tableadapter manager saves any changes you made to the data set, like adding a new row by writing in the bottom of the grid...
..or if you did it in code:
Regardless how it got there, it's a new row with the RowState of Added
When the tableadaptermanager.UpdateAll is called, or if you call an individual tableadapter.Update:
carsTableAdapter.Update(this.dataset1.Cars); //update doesn't just UPDATE, it runs INSERT and DELETE too
then the changes you made locally are saved. If someone else changed the data while you were editing it, and you have Optimistic Concurrency turned on (see above "Refresh the.." in the screenshot above) then you get an exception at the point of save. You can choose what to do to and write code that overwrites the new data, merges it, or discards the changes; ask the user what they would like to do
Note, if the DB is calculating your parent and child ID values, right clic on the relation line between your datatables, and ensure that Update is set to cascade:
This way when you add a new car and two new related clowns, and the car has -1 ID (generated locally by the dataset, pre-save), and the clowns use it too:
Then when the DB calculates the real ID, and "Refresh the dataset" option...
... causes the new ID to be retreived:
Then the changing of CarId -1 in the parent to e.g. 15 (what the DB calculated) causes the child Clowns' CarIds to be updated automatically too so the relation is preserved
TableAdapter.Update will save every kind of change; edits, deletions and inserts (new rows)

Can a DataSet table be filled with data from two sources?

I have a DataSet and read data from two sources in it. One table from a XML file and another table from a Firebird SQL database. What I try to get is only one table that has all columns from the XML file AND a few fields from the SQL data in a single table. Both tables have the same unique key field in it so it could be merged easily. I would like to bind all fields of this table to fields on a form.
Is it possible like described or do I not see that there is a simpler solution to my problem?
Edit:
To show what I try to do a bit of extra code.
DataSet dataSet = new DataSet();
DataTable table1 = new DataTable("test1", "test1");
table1.Columns.Add("id");
table1.Columns.Add("name");
table1.Columns[0].Unique = true;
table1.Rows.Add(new object[2] { 1, "name1" });
table1.Rows.Add(new object[2] { 2, "name2" });
DataTable table2 = new DataTable("test2", "test2");
table2.Columns.Add("id");
table2.Columns.Add("thing");
table2.Columns[0].Unique = true;
table2.Rows.Add(new object[2] { 1, "thing1" });
table2.Rows.Add(new object[2] { 2, "thing2" });
dataSet.Tables.Add(table1);
dataSet.Tables[0].Merge(table2, false);
When I run this code I get a ConstraintException. When I remove the unique on the id fields it fills the list with all the needed columns but one row with data from table1 and another one with table2 data. How can I merge them?
Edit 2:
I tried to use the PrimaryKey solution as follows in live data.
xmlData.Tables[0].PrimaryKey = new[] { xmlData.Tables[0].Columns["usnr"] };
dbData.PrimaryKey = new[] { dbData.Columns["usid"] };
xmlData.Tables[0].Merge(dbData, true, MissingSchemaAction.Add);
xmlData is a DataSet which comes from a XML file. It has id, usnr and a few other fields in it. dbData is a DataTable with data from db it has id, usid, name and a few other fields. The id fields are not relevant to my data. Both fields usnr and usid are strings in the table as I tested with GetType().
When I now add xmlData.Tables[0].Merge(dbData, true, MissingSchemaAction.Add); it throws a DataException
<target>.ID and <source>.ID have conflicting properties: DataType property mismatch.
While writing this I realized that the id fields where different in both tables but I dont need them anyways so did remove the column before changing the primaryKey entries and merging. Now I get a NullReferenceException with no further information in it. The tables are all fine and have data in them, where could the Exception come frome now?
Instead of ...
table1.Columns[0].Unique = true;
table2.Columns[0].Unique = true;
... add these lines:
table1.PrimaryKey = new[] { table1.Columns[0] };
table2.PrimaryKey = new[] { table2.Columns[0] };
Because the merge command needs tho know the primary keys of the data tables. Just indicating which columns are unique is not enough. With the primary keys the output will be:
id name thing
== ===== ======
1 name1 thing1
2 name2 thing2
Note that for this to work properly, the primary key fields must have matching data types and names. If the data types don't match, you get a decent error message. However, if the names don't match, a nondescript null reference exception is thrown. Microsoft could have done a better job there.
That means that in your case, I'd recommend to rename either usnr or usid before merging the data tables.
You can use Linq for this purpose and join your two DataTables like this:
.........
.........
dataSet.Tables.Add(table1);
//dataSet.Tables[0].Merge(table2, false);
var collection = from t1 in dataSet.Tables[0].AsEnumerable()
join t2 in table2.AsEnumerable()
on t1["id"] equals t2["id"]
select new
{
ID = t1["id"],
Name = t1["name"],
Thing = t2["thing"]
};
DataTable result = new DataTable("Result");
result.Columns.Add("ID", typeof(string));
result.Columns.Add("Name", typeof(string));
result.Columns.Add("Thing", typeof(string));
foreach (var item in collection)
{
result.Rows.Add(item.ID, item.Name, item.Thing);
}
The result in a DataGridView will be what you want as shown below:
dataGridView1.DataSource = result;
Here you cannot merge those two data tables together. you need to merge data in those two tables iterating each.
Create new data table with containing all the columns(Id,Name,Thing). Then populate that table reading other two.

Appending DataRow to DataTable filled from DataAdapter, clears all records

I have a DataGridView that has a DataTable bound to it, populated from PostgreSQL database with Npgsql .NET data provider library.
Populating records works, but when I want to append just a single records to already existing DataTable, previous records vanish:
NpgsqlDataAdapter npDataAdapterSingle = new NpgsqlDataAdapter("SELECT * from \"Weight\" ORDER BY id DESC LIMIT 1", this.npConnection);
DataTable tmpTable = new DataTable("tmpTable");
npDataAdapterSingle.Fill(tmpTable);
DSWeight.WeightRow row = this.dSWeight.Weight.NewWeightRow();
row.ItemArray = tmpTable.Rows[0].ItemArray;
this.dSWeight.Weight.Rows.InsertAt(row, 0); //Prepend, but i also tried this.dsWeight.Weight.Rows.Add(row);
If I select all records, without LIMIT'ing, then it works as expected. But I thought - why would i need to query the database all over again if I already have those records? That's why I want to LIMIT.
Maybe there is another solution, because I manually add new records to database and query them to add them to datatable, not the way it is supposed to be: add new records to datatable and them to database. I do it this way because I want the database to manage the id and timestamp fields and have datagridview to have these fields populated.
What am I missing?
Am not sure about the data type of DSWeight.WeightRow and the scope of 'this' object since I get to see only a portion of the code and not the full method. This should probably work for you. please have a try.
NpgsqlDataAdapter npDataAdapterSingle = new NpgsqlDataAdapter("SELECT * from \"Weight\" ORDER BY id DESC LIMIT 1", this.npConnection);
DataTable tmpTable = new DataTable("tmpTable");
npDataAdapterSingle.Fill(tmpTable);
row = tmpTable.NewRow();
foreach (DataColumn col in tmpTable.Columns)
row[col.ColumnName] = tmpTable.Rows[0][col.ColumnName];
tmpTable.Rows.Add(row, 0);

DataSet Serialization Problem in .Net

I have a dataset containing several tables, which is populated from a stored procedure. I want to make it nested for the GetXml() method.
I added the relation:
set.Relations.Add(
new DataRelation("Author_Document",
new DataColumn[] { set.Tables["Author"].Columns["lngDocumentSeriesId"], set.Tables["Author"].Columns["strAuthorName"] },
new DataColumn[] { set.Tables["Document"].Columns["lngDocumentSeriesId"], set.Tables["Document"].Columns["strAuthorName"] }, true));
I made it nested:
foreach (DataRelation relation in set.Relations)
{
relation.Nested = true;
}
And enforced:
set.EnforceConstraints = true;
All of which run fine, with no errors. The problem is when I call set.GetXml(), which throws a DataException: "Cannot proceed with serializing DataTable 'Document'. It contains a DataRow which has multiple parent rows on the same Foreign Key".
Upon inspection, the tables in question have each just a single row. The columns lngDocumentSeriesId and strAuthorName match. Even if there were a data integrety problem, it should have caused the exception on the set.EnforceConstraints = true; line, as I understand it.
What could cause this error (when all tables have just a single row), and how can it be fixed?
any other relations on the dataset ? ('there are two different tables that are each the parent, and each has one row')

DataRelation Insert and ForeignKey

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!

Categories