views:

570

answers:

2

Here is my class:

public class User
{
  public int Id { get; set; }
  public string Name { get; set; }

  public ISet<User> Friends { get; set; }
}

Here is my mapping:

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
  namespace="Test" assembly="test">

  <class name="User" table="Users">
      <id name="Id" column="id">
          <generator class="native"/>
      </id>
      <property name="Name" column="name"/>
      <set name="Friends" table="Friends">
          <key column="user_id"/>
          <many-to-many class="User" column="friend_id"/>
      </set>
  </class>
</hibernate-mapping>

Here is the problem:

User user = session.Load<User>(1);

User friend = new User();
friend.Name = "new friend";

user.Friends.Add(friend);

At the last line [user.Friends.Add(friend)], I noticed that it will initialize the Friends collection before add new friend to it.

My question is: Is there anyway to avoid this behavior in NHibernate? Because I just want to have only single INSERT command to be executed for performance reason.

+2  A: 

From Hibernate.org

Why does Hibernate always initialize a collection when I only want to add or remove an element?

Unfortunately the collections API defines method return values that may only be computed by hitting the database. There are three exceptions to this: Hibernate can add to a , or declared with inverse="true" without initializing the collection; the return value must always be true.

If you want to avoid extra database traffic (ie. in performance critical code), refactor your model to use only many-to-one associations. This is almost always possible. Then use queries in place of collection access.

Further, reading this blog entry NHibernate and Inverse=True|False Attribute, will definitely help.

[Edited]

Well, think of many-to-one and another many-to-one. Where one is the one and the same. Thats why they said to refactor the model. You need to introduce another entity, say UserFriend or something. Now you will make many-to-one for both User-to-UserFriend, Friend-to-UserFriend.

Hence, this will make it many-to-many, as you can see. I hope this make the refactoring thingy clear. You might not want to do that, unless you are experiencing poor performance for real. As Darin already mentioned, in one of the comments that, Don't do pre-mature optimization. Further, I want to quote that infamous maxim of Donald E. Knuth, "Premature optimization is the root of all evils".

Adeel Ansari
This is many-to-many to itself. How can I apply inverse to this problem? Could you give me an example? According to what Hibernate.org said, I think Inverse doesn't apply to Set. Too bad.
ensecoz
They said to refactor it, to many-to-one, and use query in place of collection access.
Adeel Ansari
I am afraid refactoring to many-to-one would change the semantics of the relation. Many-to-one is used for Parent/Child relations which is not your case.
Darin Dimitrov
I have edited the post to make the refactoring thingy clear, I hope.
Adeel Ansari
A: 

The reason NHibernate will issue a SELECT statement when you try to access the Friends collection is because it wasn't initialized at the moment you loaded the user entity. You can force the collection to be loaded either by using the following query:

User user = session
    .CreateCriteria(typeof(User))
    .SetFetchMode("Friends", FetchMode.Eager)
    .Add(Expression.IdEq(1))
    .UniqueResult<User>();

or by adding fetch="join" in your mapping file:

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
  namespace="Test" assembly="test">

  <class name="User" table="Users">
      <id name="Id" column="id">
          <generator class="native"/>
      </id>
      <property name="Name" column="name"/>
      <set name="Friends" table="Friends" fetch="join">
          <key column="user_id"/>
          <many-to-many class="User" column="friend_id"/>
      </set>
  </class>
</hibernate-mapping>

It is worth mentioning that there is a difference between UniqueResult and session.Load. If the user is not found in the database you will get an ObjectNotFoundException with the second approach while UniqueResult will return null. You can use either approach based on your needs.

Darin Dimitrov
FetchMode.Lazy and FetchMode.Eager were deprecated. The more accurately named FetchMode.Select and FetchMode.Join have the same effect. However, I don't think he wants to load 'Friends' by any mean. Not from separate SELECT, nor from an OUTER JOIN.
Adeel Ansari
Adeel is correct. I don't want to fetch any data into friends. I just want to add new friend to the collection.
ensecoz
IMHO you can't achieve this with many-to-many relation. I would avoid premature optimization in this case without confirming it is a bottleneck for your application.
Darin Dimitrov
And I would go with Darin here.
Adeel Ansari
:) Thanks for your opinion
ensecoz