How would you map this?

1 view
Skip to first unread message

Kevin Pang

unread,
Nov 25, 2009, 4:16:53 AM11/25/09
to nhusers
Let's say my system allows users to specify other users as "friends".
The database tables would look something like this:

Users
- UserId

Friends
- UserId
- FriendId

The most logical way for me to map this, in my opinion, is using a
<bag> with a <many-to-many> so that my User class looks like this:

public class User
{
public virtual IList<User> Friends {get; set;}
}

Now, if I want to enforce a rule so that you can't add a friend that
you already have as a friend, I might do it like this:

public virtual void AddFriend(User user)
{
foreach (User friend In Friends)
{
if (friend.UserId == user.UserId)
return; // Friend already exists, don't add
}

Friends.Add(user);
}

This all seems perfectly reasonable from a domain perspective. My
only problem with this is the foreach loop inside AddFriend will
trigger a call to the database to load up all of the user's friends.
This seems excessive since all I really care about is whether there
exists one row with a specific UserId and FriendId.

How would you model this?

Diego Mijelshon

unread,
Nov 25, 2009, 5:22:21 AM11/25/09
to nhu...@googlegroups.com
First... you have a collection where items are unique, and order doesn't matter, so use a <set> instead of a <bag> (map it to a ICollection<User> or ISet<User>). That should take care of uniqueness.

Second... this is an ORM, so you *have* to load the collection (yes, you can use DML-syle operations, but it defeats the purpose if you do it all the time).
However, there's something that can help you. This is the mapping I'd use:

 <class name="User" table="Users"> <id column="UserId" type="int"> <generator class="identity"/> </id> <!--other properties...--> <set name="Friends" table="Friends" lazy="true"> <key column="UserId"/> <many-to-many class="User" column="FriendId" fetch="select"/> </set> </class>

That fetch="select" property tells NHibernate to use proxies for the set's elements, so you'll just be loading the friends' primary keys (from the Friends table, no join) instead of the whole User object.

You can then do this:

var user = session.Get<User>(id);
var friend = session.Load<User>(friendId);
user.Friends.Add(friend);
user.Friends.Add(friend); //does nothing

Voila... That was one select for the user, one for the collection.
Flushing at that point would issue one insert (the friend object wasn't even retrieved from the DB)

Hope this helps.

   Diego



--

You received this message because you are subscribed to the Google Groups "nhusers" group.
To post to this group, send email to nhu...@googlegroups.com.
To unsubscribe from this group, send email to nhusers+u...@googlegroups.com.
For more options, visit this group at http://groups.google.com/group/nhusers?hl=en.



Roger Kratz

unread,
Nov 25, 2009, 5:27:23 AM11/25/09
to nhu...@googlegroups.com
<< This all seems perfectly reasonable from a domain perspective. >>

I don't think you should use the OOID that explicitly in your domain. Use...
If(!Friends.Contains(user))
Friends.Add(user);
...or similar and define what "equals" mean elsewhere.

Anyhow, that won't solve your problem. Don't know if this was more of a general question or if you just want to make sure no duplicates exists in a colletion? If the later, use <set> instead of <bag> and just add the user to the friend collection.

/Roger

Kevin Pang

unread,
Nov 25, 2009, 11:08:54 AM11/25/09
to nhusers
Diego,

Perfect. That's exactly what I was looking for. Thanks! :-)

When you say that you *have* to load the collection, I assume you mean
that since we're dealing with an ORM the default approach should be to
load collections into memory so that you can perform business logic on
it? For instance, if this business logic were something more complex
than uniqueness and depended on more properties within the collection,
then loading it and traversing it would be the best approach as
opposed to directly hitting the database with a query?

Kevin

On Nov 25, 2:22 am, Diego Mijelshon <di...@mijelshon.com.ar> wrote:
> First... you have a collection where items are unique, and order doesn't
> matter, so use a <set> instead of a <bag> (map it to a ICollection<User> or
> ISet<User>). That should take care of uniqueness.
>
> Second... this is an ORM, so you *have* to load the collection (yes, you can
> use DML-syle operations<http://www.nhforge.org/doc/nh/en/index.html#batch-direct>,
> > nhusers+u...@googlegroups.com<nhusers%2Bunsu...@googlegroups.com >
> > .

Diego Mijelshon

unread,
Nov 25, 2009, 12:11:47 PM11/25/09
to nhu...@googlegroups.com
Well, the ORM works by creating a representation of what's in the database, which allows you to work with persistence ignorance on the object model, and that includes adding and removing elements from a collection, which is then mapped to inserting and deleting records from a DB table.

Now, there *IS* an alternative to what I proposed that might be more aligned with your original idea.
Instead of loading the collection, you could try to get the record from the Friends table which represents the relationship. However, in order to do that, you have to change your domain model and usage to adjust to that particular use case, which is what you should avoid.

There are always tradeoffs with ORMs, but the benefits largely are way bigger than the costs.

   Diego


To unsubscribe from this group, send email to nhusers+u...@googlegroups.com.

Kevin Pang

unread,
Nov 25, 2009, 5:12:23 PM11/25/09
to nhusers
Diego,

Thanks again. This really helped and I appreciate you walking through
it with me. I think I was too wrapped up in database performance and
the queries that were being executed. I forgot that domain
cleanliness should also play a factor in my decision.

One more question:

If I wanted to get a list of a user's friends whose name began with
the letter "A", would you recommend creating a separate query to fetch
that information using HQL/Criteria/SQL or would you iterate over the
Friends collection and do it in memory? My initial impression would
be to use a query for situations like this, and the object graph for
things like adding/removing a user's friends.

Kevin

On Nov 25, 9:11 am, Diego Mijelshon <di...@mijelshon.com.ar> wrote:
> Well, the ORM works by creating a representation of what's in the database,
> which allows you to work with persistence ignorance on the object model, and
> that includes adding and removing elements from a collection, which is then
> mapped to inserting and deleting records from a DB table.
>
> Now, there *IS* an alternative to what I proposed that might be more aligned
> with your original idea.
> Instead of loading the collection, you could try to get the record from the
> Friends table which represents the relationship. However, in order to do
> that, you have to change your domain model and usage to adjust to that
> particular use case, which is what you should avoid.
>
> There are always tradeoffs with ORMs, but the benefits largely are way
> bigger than the costs.
>
>    Diego
>
> > <nhusers%2Bunsu...@googlegroups.com<nhusers%252Buns...@googlegroups.com>>

Diego Mijelshon

unread,
Nov 25, 2009, 6:35:53 PM11/25/09
to nhu...@googlegroups.com
An HQL, Criteria or Linq query would be the way to go, because doing it in memory in this case would imply loading the actual friend data from the DB.

    Diego


To unsubscribe from this group, send email to nhusers+u...@googlegroups.com.

Kevin Pang

unread,
Nov 25, 2009, 7:11:42 PM11/25/09
to nhusers
Diego,

Makes sense. Now what if there were a restriction placed so that you
could only have 5 friends with a name starting with "A" (yes, this is
a contrived example). I could either run a query to get the number of
friends with a name starting with "A", or traverse the object graph.
I'd lean towards traversing the object graph in this instance, since
the logic would probably be in the AddFriend() function which should
be ignorant of persistence and database issues, however I can see how
that might result in some suboptimal queries since you'd have to load
up all the friends in order to count the number of ones whose names
start with "A".

I suppose the alternative solution would be to run a count query
outside of the AddFriend() function in a service of some sort, and
call AddFriend() only if the number of "A" friends was less than 5,
but this feels a bit ugly. Maybe only resort to this if performance
becomes an issue with the first option?

Kevin

On Nov 25, 3:35 pm, Diego Mijelshon <di...@mijelshon.com.ar> wrote:
> An HQL, Criteria or Linq query would be the way to go, because doing it in
> memory in this case would imply loading the actual friend data from the DB.
>
>     Diego
>
> > > > <nhusers%2Bunsu...@googlegroups.com<nhusers%252Buns...@googlegroups.com>
> > <nhusers%252Buns...@googlegroups.com<nhusers%25252Bun...@googlegroups.com>

Diego Mijelshon

unread,
Nov 26, 2009, 6:34:04 AM11/26/09
to nhusers
Kevin,

You can do what you suggested. But doing it before any actual performance/stress testing is definitely premature optimization, especially considering multiple requests might be sharing the resources.

   Diego


To unsubscribe from this group, send email to nhusers+u...@googlegroups.com.
Reply all
Reply to author
Forward
0 new messages