Many to many relation (m:n relation)

Posts   
 
    
gustavo
User
Posts: 15
Joined: 17-Nov-2006
# Posted on: 22-Nov-2006 18:15:16   

I have two tables tableA and tableB with a many to many relation in an intermediate table (tableAB).

I am using adapter and a Firebird database and I want to create a tableA record, then a tableB record and relate them.

My generated code entities are recognizing the m:n relation, but I have not been able to find out how to insert a tableB record into tableA.

What I have in my code is using this approach but I am getting an error. Is it the right approach?

tableAEntity a = new tableAEntity(); a.name="this is the name"; a.telephone = "121212"; DataAccessAdapter adapter = new DataAccessAdapter(); adapter.SaveEntity(a,true);

tableBEntity b = new tableBEntity(); b.accountName = "account name"; b.TableACollectionViaTableAB.Add(a); DataAccessAdapter adapter = new DataAccessAdapter(); adapter.SaveEntity(b,true);

Thank you

bclubb
User
Posts: 934
Joined: 12-Feb-2004
# Posted on: 23-Nov-2006 02:20:39   

In Best Practices under How Do I... The last entry is "How do I add an entity A to an entity B's collection of A's if A and B have an m:n relation ?"

This will go into more detail.

For you situation you should do something like this.


tableAEntity a = new tableAEntity();
a.name="this is the name";
a.telephone = "121212";

tableBEntity b = new tableBEntity();
b.accountName = "account name";

TableABEntity tableAB = new TableABEntity();
tableAB.TableA = a;
tableAB.TableB = b;

//  You can save any of the entities recursively and all will be saved.
using(DataAccessAdapter adapter = new DataAccessAdapter)
{
    adapter.Save(tableAB);
}

Keep in mind that the m:n relation is then saved in the database, but the situation in memory isn't updated, i.e.: a.TableB doesn't contain 'TableBEntities'. This is by design, as the m:n relation is based on a 3rd entity, which can be altered by any thread in the system, making the m:n relation, which is a result of the values of the intermediate entity, a 'view' of a situation at a given point in time.

gustavo
User
Posts: 15
Joined: 17-Nov-2006
# Posted on: 23-Nov-2006 07:00:24   

bclubb

Thank you very much for your orientation, it was very clear documented but I didnt find it before posting my question.

Llblgen is a great tool !!

gustavo
User
Posts: 15
Joined: 17-Nov-2006
# Posted on: 23-Nov-2006 14:54:12   

I have got another problem saving an m:n relation

The relation table is not getting the autogenerated key values of the Table A and Table B

Do you know how to deal with this?

I tryed saving the entity A and then entity B before making the TableAB relation and saving the middle element but it is not working.

If I use something like the code below, th the relation table will get null keys for the table A and B (relation table has a unique autogenerated key too.


// C#
DepartmentEmployees newDepEmp = new DepartmentEmployees();
newDepEmp.Employee = employee;
newDepEmp.Department = department;

// the 3 entities are now linked. We can now save recursively any of the three (doesn't matter) 
// to save the relation in the database.
adapter.SaveEntity(employee);

And does not work, so what I tryed to do was to save employee and department before and refetching them, but still not working.

I am using firebird and adapter. The keys are autogenerated by a trigger and in a helpdesk question I got the help and could fix a Savecollection probem in a datagrid by reloading the Entitycollection after saving (because I can not drop the triggers).

[--- Updated: 17-11-2006 14:31:27 by user ---]

I am using adapter and a firebird database on .Net 2.0 using Winforms.

I am loading an editable grid and I put a SaveButton, I am using the adapter.SaveEntityCollection and passing the collection of objects, however I am getting this error:

"The entity is out of sync with its data in the database. Refetch this entity before using this in-memory instance."

What I use to save is the following code

private void btnSave_Click(object sender, EventArgs e) { UnloadGrid(); int empresasGrabadas = adapter.SaveEntityCollection(_empresasCollection,true,true); MessageBox.Show(empresasGrabadas.ToString() + "saved elements"); LoadGrid(); }

public void LoadGrid() { adapter.FetchEntityCollection(_empresasCollection, null); }

private void UnloadGrid() { this.gridControl1.DataSource = null; }

I just want to know if this is the recomended way to do a grid save and why it may be throwing an error.

Solution:

It's not really more inefficient, as you now avoid a select statement for every inserted instance and do it in one select afterwards, so it's not that big of a deal performance wise.

The Firebird DQE doesn't have a feature build in to work around the insert triggers unfortunately.


[--- Updated: 20-11-2006 9:58:59 : Frans Bouma ---]


[--- Updated: 18-11-2006 11:50:17 : Frans Bouma ---]

Saved entities aren't reloaded after the save action unless you specify that. This is done to prevent unnecessary data load if you don't need the entities anymore. They are marked 'out-of-sync' to signal that the entity in the db is newer and perhaps has different values (due to triggers, default constraints etc.) and before you can access the data again the entity has to be reloaded

However you do specify true to refetch the entities after the save. So could you test this for me: - what is succeeded after the save call in SaveChanges? - after the save call in SaveChanges, if you loop through the entities and check: entity.Fields.State, are there entities with the state EntityState.OutOfSync? If so, are there entities with this state BEFORE the save call in SaveChanges?

Reasons why refetch fails There are a couple of reasons why a refetch fails. One of them is that a trigger updated the PK as well. Or that the PK wasn't inserted. The refetch is tried with the PK value known in the entity.

With firebird databases it's often the case that people define a trigger to insert the new PK value into a row. If you've defined in the llblgen pro designer that that entity also has a sequenced PK, you'll get this error. The reason is this: - llblgen pro inserts a new entity, using the sequence / generator it inserts for the PK the value 1, this is the value stored in the entity as the PK value. - the trigger inserts the next value in the entity and that's the value 2 as that's the first new value for the sequence - llblgen pro tries to load the entity again, but that fails, as the entity has now the pk value 2, while the entity has the pk value 1.

To solve this: drop the trigger.

[--- Updated: 23-11-2006 7:37:30 by user ---]

Thank you very much for your help. I understand now why it was not working properly. I am making a system using databases from third party systems so I can not change their design. So what I am doing is to regenerate the collection after saving the changes.

It is working ok now, but I would like to know your opinion about it, I know I am loosing eficiency but I dont know if there may be another solution if I can not drop the triggers....

Llblgen is Great !!

I am using this now:

private void SaveChanges() {

UnbindGrid(); DataAccessAdapter adapter = new DataAccessAdapter(); int succeeded = adapter.SaveEntityCollection(_paisCollection, true,false); adapter.CloseConnection();

// Regenerate collection to avoid Synck problem with Firebird _paisCollection.Dispose(); GenerateCollection();

BindGrid(); if (succeeded > 0) { MessageBox.Show("Cambios guardados exitosamente", "Guardar cambios", MessageBoxButtons.OK, MessageBoxIcon.Information); btnGuardaCambios.Enabled = false;

} else { MessageBox.Show(this, "No se pudieron guardar los cambios", "Save result", MessageBoxButtons.OK, MessageBoxIcon.Information); } }

private void GenerateCollection() { WxSysPaisEntityFactory pais = new WxSysPaisEntityFactory(); _paisCollection = new EntityCollection(pais); _paisCollection.ListChanged += new ListChangedEventHandler(_paisCollection_ListChanged);

}

private void BindGrid() { DataAccessAdapter adapter = new DataAccessAdapter(); adapter.FetchEntityCollection(_paisCollection, null); adapter.CloseConnection(); paisSource.DataSource = _paisCollection.DefaultView; }

private void _paisCollection_ListChanged(object sender, ListChangedEventArgs e) { this.btnGuardaCambios.Enabled = true; }

private void btnGuardaCambios_Click(object sender, EventArgs e) { SaveChanges(); }


jbb avatar
jbb
User
Posts: 267
Joined: 29-Nov-2005
# Posted on: 23-Nov-2006 15:08:19   

Hello,

and if you save A,B with refetch like this :


DepartmentEmployees newDepEmp = new DepartmentEmployees();

adapter.SaveEntity(employee,true);
adapter.SaveEntity(departement,true);

newDepEmp.Employee = employee;
newDepEmp.Department = department;
adapter.SaveEntity(newDepEmp);

And after each save, did the entity had a good key?

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39927
Joined: 17-Aug-2003
# Posted on: 23-Nov-2006 15:15:42   

This is a tough call. You really should get rid of the triggers. The problem is that the PK value in the entity is changed by a trigger after the entity is saved. So whatever you want to do, you can't read back the entity in a single entity fetch as the entity's PK value in-memory isn't found anymore in the database.

So you can then only fetch the entity again when you fetch a collection fetch.

Frans Bouma | Lead developer LLBLGen Pro
Otis avatar
Otis
LLBLGen Pro Team
Posts: 39927
Joined: 17-Aug-2003
# Posted on: 23-Nov-2006 15:19:36   

The Oracle DQE has a setting which can deal with triggers (that's a global setting, so not per table, it's either ALL tables are considered to have triggers or NONE are considered to have triggers), I could check to see if I can build that into the FB DQE as well. It's more of a scheduling when a sequence value is read after the insert, using CURRVAL. So the insert statement triggers the trigger, which inserts the value, then the sequence query is executed which retrieves the current value for the sequence, which is set by the trigger.

Frans Bouma | Lead developer LLBLGen Pro
gustavo
User
Posts: 15
Joined: 17-Nov-2006
# Posted on: 23-Nov-2006 15:39:14   

Hello,

and if you save A,B with refetch like this :

Code:

DepartmentEmployees newDepEmp = new DepartmentEmployees();

adapter.SaveEntity(employee,true); adapter.SaveEntity(departement,true);

newDepEmp.Employee = employee; newDepEmp.Department = department; adapter.SaveEntity(newDepEmp);

And after each save, did the entity had a good key?

I also tryed


DepartmentEmployees newDepEmp = new DepartmentEmployees();

adapter.SaveEntity(employee,true);
adapter.SaveEntity(departement,true);

adapter.FetchEntity(employee);
adapter.FetcEntity(departement);

newDepEmp.Employee = employee;
newDepEmp.Department = department;
adapter.SaveEntity(newDepEmp);

But still no success, so after I readed what Otis said I started to see how can I get rid of the triggers, I will try to see if I can do this because is not my own Db, but still I have some unique values that will help me to find and refetch the entity I hope..

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39927
Joined: 17-Aug-2003
# Posted on: 24-Nov-2006 10:43:45   

Please let me know if you need me to add code to the DQE for Firebird (in the case where you can't remove the triggers)

Frans Bouma | Lead developer LLBLGen Pro
gustavo
User
Posts: 15
Joined: 17-Nov-2006
# Posted on: 24-Nov-2006 19:08:03   

Thank you very much Otis, it will be great if you can add the code. With this code all the tables Must have trigger?

Meanwhile, I am trying to use a stored procedure to get the next ID on the table and insert my data with my ID already (I think that there can be a duplicate in case that someone inserts at the same time), but will try to test today if it can work.

I tryed sending the key manually and it is now refetching ok.

What I am using is to call a store procedure that returns me the key using this command:

'SELECT gen_id(TABLE_A_GEN, 1) AS NEXT_ID FROM TABLE_A'

I learned this from this post: http://forums.devshed.com/firebird-sql-development-61/auto-generated-ids-225732.html

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39927
Joined: 17-Aug-2003
# Posted on: 26-Nov-2006 10:42:58   

I'll try to add this feature to the Firebird DQE on monday.

Frans Bouma | Lead developer LLBLGen Pro
Otis avatar
Otis
LLBLGen Pro Team
Posts: 39927
Joined: 17-Aug-2003
# Posted on: 27-Nov-2006 16:43:54   

While testing this I saw that the example employee database of firebird has triggers as well, but these have a proper check in place:

AS
BEGIN
    if (new.emp_no is null) then
    new.emp_no = gen_id(emp_no_gen, 1);
END

Which shows, that if the emp_no (pk, sequenced) is null, it needs to insert a value using the generator, otherwise it shouldn't. Your triggers likely simply set the pk and don't test.

I've added a new setting which you should add to the .config file of your application (to the appSettings section): <add key="FirebirdTriggerSequences" value="true"/>

The DQE for firebird will then switch to use sequences after the insert query by reading their current value. If this setting isn't present or false is specified, it will simply execute the sequence before the query, request a new value and use that value in the insert query (which is the behavior it has now and always has had).

Added to build 2.0.0.061127 of the runtime library.

Frans Bouma | Lead developer LLBLGen Pro
gustavo
User
Posts: 15
Joined: 17-Nov-2006
# Posted on: 27-Nov-2006 17:09:39   

Thank you very much for your help I will try to check my triggers.