-->
These old forums are deprecated now and set to read-only. We are waiting for you on our new forums!
More modern, Discourse-based and with GitHub/Google/Twitter authentication built-in.

All times are UTC - 5 hours [ DST ]



Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 4 posts ] 
Author Message
 Post subject: Inefficient changes to many-to-many
PostPosted: Sun Jun 03, 2007 11:47 pm 
Beginner
Beginner

Joined: Fri Jan 12, 2007 1:08 am
Posts: 41
In the domain objects the other side of the many-to-many relationship is accessed using a generic IList. In the mapping file bag semantics are being used. Everything works but generated SQL is very inefficient when I change the contents of the IList because the entire collection is deleted and then the new collection is inserted row by row.

I understand why this happens but I don't know how to fix it in the mapping file (can use custom ISQLQuery but I don't want to have to write these for every many-to-many). Can someone help me?


Top
 Profile  
 
 Post subject:
PostPosted: Tue Jun 26, 2007 1:03 pm 
Beginner
Beginner

Joined: Fri Jan 12, 2007 1:08 am
Posts: 41
The silence was deafening!

Here's the solution that I implemented (SQL Server only but easy to adapt for another RDBMS).

I should have been able to do is use an <idbag> rather than a <bag>…but due to a bug (http://jira.nhibernate.org/browse/NH-364 - please vote for this) they don’t work. The workaround is to set inverse=”true” on both sides the <many-to-many> relationship. As a result NHibernate will no longer try to persist changes. Now we can use a custom ISQLQuery to manage the association more efficiently. I chose to use a stored procedure to which we can pass XML that contains the current associations.

Here’s an example (I know that strictly there is no parent and child in a many to many but I did not want to tie the example to a specific domain):

Code:
CREATE PROCEDURE update_parent_child
  @ParentID INT,
  @XMLDoc NTEXT
AS
  SET NOCOUNT ON
 
  DECLARE @XMLHndl INT
  EXEC sp_xml_preparedocument @XMLHndl OUTPUT, @XMLDoc

  -- Delete associations that no longer exist
  DELETE FROM parent_child
  WHERE  parent_id = @ParentID
  AND    child_id NOT IN
         (
           SELECT xmlID
           FROM   OPENXML (@XMLHndl, '//children/child', 2)
           WITH   (xmlID INT '@id')
         )
 
  -- Create new associations
  INSERT INTO parent_child
         (parent_id, child_id)
  SELECT @ParentID,
         xmlID,
  FROM   OPENXML (@XMLHndl, '//children/child', 2)
  WITH   (xmlID INT '@id')
  WHERE  xmlID NOT IN
         (
           SELECT child_id
           FROM   parent_child
           WHERE  parent_id = @ParentID
         )


Note that for this process to be efficient both foreign keys in the many to many resolution table must be on the left hand side of an index (this would also have applied with the <idbag>).

Calling the SP is trivial:

Code:
public void ChangeParentChildAssociations(Parent parent)
{
  ISQLQuery q = session.CreateSQLQuery("exec update_parent_child ?, ?");
  q.SetInt32(0, parent.ID);
  q.SetString(1, GetChildXml(parent));
  // Must define a query result
  q.AddScalar("ignored", NHibernateUtil.Int32);
  q.UniqueResult();
}

public string GetChildXml(Parent parent){
  StringBuilder tmp = new StringBuilder();
  foreach (Child child in parent.Children)
  {
    tmp.Append("<child id=\"" + child.ID + "\"/>");
  }
  return "<children>" + tmp + "</children>";
  }


Code generating this is trivial.


Top
 Profile  
 
 Post subject:
PostPosted: Tue Jun 26, 2007 1:39 pm 
Contributor
Contributor

Joined: Wed May 11, 2005 4:59 pm
Posts: 1766
Location: Prague, Czech Republic
If you don't want NHibernate to manage the collection, maybe you don't need the collection at all? Have you considered using a query instead? And what about using ISet collections?


Top
 Profile  
 
 Post subject:
PostPosted: Tue Jun 26, 2007 2:38 pm 
Beginner
Beginner

Joined: Fri Jan 12, 2007 1:08 am
Posts: 41
Thanks for the ideas Sergey.

I did consider a query rather than a SP but even with IMultiQuery which will batch multiple db calls into a single request I would have to pass the XML twice (I believe, please correct me if I am wrong). Even if I reduce the verbosity of the XML (e.g. <a><b c="1"/></a>) with large associations the double send (and parse) could become material.

Of course that is really the problem with this whole approach. Ideally I would track changes in the application and then only send them to the db. But that is a future optimization!

I did contemplate using an ISet but with a relatively mature code base I was wary of a change that would affect all layers of the application. However next time we start a new app I will definitely look at this.

You said "If you don't want NHibernate to manage the collection, maybe you don't need the collection at all?". Are you suggesting removing the many-to-many associations entirely? I did evaluate this approach but I was not keen on introducing a 2nd data access paradigm. Basically I am happy with the reads I just need more control over the nature of the writes...until bug 364 is fixed that is (I understand that it is non-trivial otherwise I would have looked into it myself).


Top
 Profile  
 
Display posts from previous:  Sort by  
Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 4 posts ] 

All times are UTC - 5 hours [ DST ]


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum

Search for:
© Copyright 2014, Red Hat Inc. All rights reserved. JBoss and Hibernate are registered trademarks and servicemarks of Red Hat, Inc.