views:

130

answers:

3

I've been trying unsuccessfully to filter a collection of child objects for a few hours how and have finally thrown my hands up! I'm new to NHibernate and was hoping for a couple of pointers on this. I've tried various ICriteria etc. with no luck. I'm just not getting it.

I have a parent object 'Post' with a collection of child objects 'Comment'. The collection is mapped as a set with inverse on the Comment side.

What I am trying to do is to return only comments with a status enum value of 'Comment.Approved'

The relevant portions of the entity classes are as follows:

public class Post
{
    public virtual Guid Id { get; protected set; }
    private ICollection<Comment> _comments;
    public virtual ICollection<Comment> Comments
    {
        get { return _comments; }
        protected set { _comments = value; }
    }
}

public class Comment
{
    public virtual Guid Id { get; protected set; }
    public virtual Post Post { get; set; }
    public virtual CommentStatus Status { get; set; }

 }

My retrieval code looks like this at the moment:

var Id = __SomeGuidHere__;
var post = _session
            .CreateCriteria<Post>()
            .Add(Restrictions.Eq("Id", Id))
            .UniqueResult<Post>();

var comments = _session.CreateFilter(post.Comments, "where Status = :status").SetParameter("status", CommentStatus.Approved).List<Comment>();

While this works the SQL doesn't appear to be very efficient, I expected to be able to translate the following SQL into something similar in HQL or an ICriteria of some sort:

SELECT * FROM posts p LEFT JOIN comments c ON p.PostId = c.PostId AND c.Status = 0 WHERE p.PostId = '66a2bf13-1330-4414-ac8a-9d9b00ea0705';

I've had a look at the various answers related to this type of query here and none of them seem to address this specific scenario.

There's probably something very simple I'm missing here but I'm too tired now to see it. Here's hoping someone better with NHibernate can point me in the right direction.

Thanks for your time.

Edit: Still struggling with this, some of the answers here are good in that I'm starting to think that my post entity needs to be re-thought to perform the filtration itself, or that I should implement a ViewModel to filter the comments I want. The question still remains however, even if only from an academic perspective.

I've updated the selection to HQL and tried:

var post = _session
            .CreateQuery("select p from Post as p left join fetch p.Comments as c where p.Id = :id and c.Id in (select ac from p.Comments ac where ac.Status = :status)")
            .SetParameter("id", Id)
            .SetParameter("status", CommentStatus.Approved)
            .UniqueResult<Post>();

This works as long as a post has an approved comment, otherwise I get no post due to the SQL generated using 'AND' in the where clause.

Anyone? I'm stumped now!

Update: Thanks to all who have replied, it has been useful and has forced me to re-evaluate portions of my model. Since the most frequent use of comments as children of a post is in the viewing of a post, only the approved comments should be viewable in this scenario. In most other scenarios that I can think of comments would be accessed directly and filtered by status which is of course straight forward.

I have updated my mappings to filter all post > comment loading to only load approved posts as follows (in FluentNHibernate):

 HasMany(x => x.Comments).Where(x => x.Status == CommentStatus.Approved)
            .AsSet()
            .Inverse()
            .KeyColumn("PostId")
            .ForeignKeyConstraintName("PostComments")
            .OrderBy("CreatedOn")
            .Cascade.AllDeleteOrphan();

I wish I could mark all as the answer since all contributed to me working this thing out, but Peter was the first to point out that I may be thinking about the model incorrectly.

Thanks to all.

A: 

According to me the SQL is perfect for what you describe.

The database has its own optimizer and will know what to do to get your data efficiently delivered to your doorstep.

Peter Tillemans
Thanks for the comment, what I would have liked was the Post object to have had a nice filtered set of comments rather than having to use a second object (var comments) in the rest of the code. That's possibly just my retentiveness in action :) If there was an alternative that allowed me to maintain my model I'd rather have it. It bugs me that the post object still has the comments I don't want hanging off it!
Colin Mc Mahon
The danger then is that the "comments" field in the post object will have different meanings in different circumstances, which quickly gets complicated. Consider adding a getApprovedComment() or something. Typically you need these things only once in a request and then lazy loading will be efficient. If you do need them multiple times, you can cache the results in that method.
Peter Tillemans
Hmmm that's interesting, my knowledge isn't up to parr on this, could you explain a bit further please. I get what you mean by the comments having different meanings I think, i.e. actually being approved comments in this instance, and perhaps in an admin scenario being 'spam comments', 'dodgy comments' and approved comments. Would you suggest that perhaps the post object should have several collections, each with different mappings?
Colin Mc Mahon
I would use methods either on the object or in a service layer, depending on the conventions used in the codebase. I am not fundamentalist about svc.getApprovedMethods(post) or post.getApprovedMethods(). Both "sing" their meaning to me. If you like skinny objects the service approach will be better, if you do not mind "fat" objects the second one will be more natural.
Peter Tillemans
OK, your comment re: the comments taking on new meaning really got me thinking - when will I access the comments as children of the post? Not often! Possibly only when presenting the post for viewing, meaning that 90% of the time we will only want Approved posts. I'm going to change the mapping to reflect this.
Colin Mc Mahon
A: 

I think this would work fine:

var comments =
_session.CreateCriteria<Comment>
.CreateAlias("Post", "post")
.Add (Restriction.Eq("Status", status))
.Add (Restriction.Eq("post.Id", Id)).List();

I am not shure if you need the .CreateAlias part (if you don't need it, maybe Post.Id would work too - but I am not sure).

bernhardrusch
I See where you're coming from but this query only returns the comments what I would really like is the post object with its collection filtered to any posts that have the status I'm looking for, if It's possible. If not I'll have to live with what I've got but it seems like it should be straight forward!
Colin Mc Mahon
in this case I would make a property in the Post class which would return the correct comments (filtering the Comments property) - but this depends on your application (if this "suboptimal" querying is acceptable or not)
bernhardrusch
in general I would first start implementing - then test it - and if the performance is bad - I'd start to optimize these cases (no premature optimization)
bernhardrusch
A: 

I would solve this using an extension method for IEnumerable<Comment>:

public static IEnumerable<Comment> FilterByStatus(this IEnumerable<Comment> comments, CommentStatus status)
{
    return comments.Where(x => x.Status == status);
}

Let NHibernate return the entire collection then filter it as needed.

Jamie Ide
Starting to think that this, or one of the other methods in this vein from bernhardrusch or Peter might be my only choice.
Colin Mc Mahon