Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NHibernate querying on a string collection using Linq results in either error or empty collection

I have a problem with selecting a string collection and have reproduced it with the following small example.

Given the following SQL:

CREATE TABLE [Post] (
    [Id]    INT     IDENTITY NOT NULL,
    [Name]  NVARCHAR(20) NOT NULL,
    CONSTRAINT [PK_Post] PRIMARY KEY CLUSTERED ([Id])
)
CREATE TABLE [Category] (
    [Id]    INT     IDENTITY NOT NULL,
    [PostId]    INT NOT NULL,
    [Name]  NVARCHAR(20) NOT NULL,
    CONSTRAINT [FK_Category_Post] FOREIGN KEY ([PostId]) REFERENCES [Post]([Id])
)
INSERT INTO [Post] ([Name]) VALUES ('Post 1')
INSERT INTO [Category] ([PostId], [Name]) VALUES (1, 'Alpha')

And the code (I used LINQPad):

void Main()
{
    using (var sessionFactory = Fluently.Configure()
        .Database(MsSqlConfiguration.MsSql2008.Dialect<MsSql2012Dialect>().ConnectionString(@"Data Source=(localdb)\Projects;Initial Catalog=NhTest;"))
        .Mappings(x => {
            x.FluentMappings.Add(typeof(PostMap));
        })
        .BuildSessionFactory())
    using (var session = sessionFactory.OpenSession())
    {
        var post = session.Get<Post>(1);
        Debug.Assert(post.Categories.First() == "Alpha");

        try {
            var second = session.Query<Post>()
                .Where(x => x.Id == 1)
                .Select(x => new {
                    x.Categories,
                    x.Name,
                })
                .Single();
        }
        catch (Exception ex) {
            Debug.Fail(ex.ToString());
        }

        var third = session.Query<Post>()
            .Where(x => x.Id == 1)
            .Select(x => new {
                x.Categories,
                x.Name,
            })
            .ToList().First();

        Debug.Assert(third.Categories.Count() == 1, "Category count was " + third.Categories.Count());
    }
}

// Define other methods and classes here
class Post
{
    public virtual int Id { get; protected set; }
    public virtual string Name { get; protected set; }
    public virtual IList<string> Categories { get; protected set; }
}

class PostMap : ClassMap<Post>
{
    public PostMap()
    {
        Id(x => x.Id);
        Map(x => x.Name);

        HasMany(x => x.Categories)
            .Table("Category")
            .Element("Name")
            .KeyColumn("PostId");
    }
}

The first assert passes and this, in my mind, validates my mapping of the categories onto the post.

The query in the try block throws an exception

'System.Linq.EnumerableQuery`1[System.Collections.Generic.IList`1[System.String]]' cannot be converted to type 'System.Linq.IQueryable`1[System.Object[]]

So I've changed it to the 3rd attempt you see in the code, calling .ToList().First(). This query does not throw any exception but the categories list is returned empty.

Am I doing something wrong? Is there a better mapping technique to use here? Or are there workarounds to get my query working?

like image 291
Matt Avatar asked Apr 07 '14 19:04

Matt


3 Answers

This was a bug in NHibernate. It would try to get the class mapping data for the Enumerable type parameter. When projecting an Enumerable of a mapped class this works perfectly. In this instance, however, the Enumerable type parameter is string. String does not have a class map and so it was substituting a null constant in the query which then caused issues later on.

To further illustrate this, the SQL query for this example would be:

SELECT c.Name, p.Id, p.Name FROM Post p LEFT JOIN Category c ON p.Id = c.PostId WHERE p.Id = 1

This would return data like so:

 c.Name | p.Id   | p.Name 
--------------------------
 Cat 1  | 1      | Post A
 Cat 2  | 1      | Post A

NHibernate would then, in memory, perform a group by operation, including null checks, to create a list of categories for each unique value of p.Id. The group by operation is performed using column indexes.

That is what should be happening. The bug was causing the results to be transformed, before the group by operation, into:

 NULL   | Cat 1  | 1      | Post A
 NULL   | Cat 2  | 1      | Post A

NHibernate was then filtering results where column 0 was null, meaning none of them survive.

The pull request containing the fix is here: https://github.com/nhibernate/nhibernate-core/pull/262

like image 136
Matt Avatar answered Sep 29 '22 11:09

Matt


You can create CategoryMap, Category entity and change to:

public virtual IList<Category> Categories { get; protected set; }

I'm presuming is a linq to nhibernate querying limitation.

like image 23
Najera Avatar answered Sep 29 '22 12:09

Najera


Try changing

var second = session.Query<Post>()
    .Where(x => x.Id == 1)
    .Select(x => new {
        x.Categories,
        x.Name,
    })
    .Single();

To ToList().Single() instead of Single().

I've seen this issue before, the problem is that the query will return multiple rows that are then coerced into a single anonymous type. The query fails because the intermediate results contain multiple rows instead of a single row.

like image 24
Jamie Ide Avatar answered Sep 29 '22 11:09

Jamie Ide