Archive

Archive for the ‘EntityFramework’ Category

NHibernate Work-Around is not really a Work-Around…

August 15th, 2011 2 comments

Over the weekend I came across a blog post about NHibernate, and an apparent work-around for a feature not supported by NHibernate.

The original post can be found here: nHibernate LINQ workaround for System.NotSupportedException

Basically the author wanted to write something along the lines of:

var fruitIds = new List<int> { 5, 8, 13 };

using (var session = factory.OpenSession())
{
    var result = from f in session.Query<NHFruit>()
                 join i in fruitIds
                     on f.Id equals i
                 select f;

    foreach (var fruit in result)
        Console.WriteLine(fruit.Name);
}

Where the query joins to a list of Ids to filter the results out. This however, happens to throw an exception:

image

Normally this sort of query would be written like so:

var fruitIds = new List<int> { 5, 8, 13 };

using (var session = factory.OpenSession())
{
    var result = from f in session.Query<NHFruit>()
                 where fruitIds.Contains(f.Id)
                 select f;

    foreach (var fruit in result)
        Console.WriteLine(fruit.Name);
}

This generates an ‘in’ clause in the SQL.

select nhfruit0_.Id   as Id0_,
       nhfruit0_.Name as Name0_
from   Fruit nhfruit0_
where  nhfruit0_.Id in (5 /* @p0 */,8 /* @p1 */,13 /* @p2 */)

Nice and clean, pretty standard SQL.

Now I must admit when I first read his code I made the assumption that it would actually pull ALL results into memory, and do the join in memory.

I commented on his blog with the following comment:

I don”t see how this is a ”work-around” your first attempt at the query is attempting to join something in memory, to something still in the database. Thats clearly not, and will never be a supported feature of NHibernate… The second query is passing a list of ID”s into the database to use for comparison.

At the time of writing the comment, I was a bit annoyed. The author replied:

Never be supported in NHi”bernate… well it works using Entity Framework and that”s why I consider it to be a workaround.

So lets give it a go.

Give the following query:

var fruitIds = new List<int> { 5, 8, 13 };

using (var ctx = new EFTestEntities())
{
    var result = from f in ctx.Fruits
                 join i in fruitIds
                     on f.Id equals i
                 select f;

    foreach (var fruit in result)
        Console.WriteLine(fruit.Name);
}

The exact same as the attempt in NHibernate, it generates the following SQL…

SELECT [Extent1].[Id]   AS [Id],
       [Extent1].[Name] AS [Name]
FROM   [dbo].[Fruit] AS [Extent1]
       INNER JOIN (SELECT [UnionAll1].[C1] AS [C1]
                   FROM   (SELECT 5 AS [C1]
                           FROM   (SELECT 1 AS X) AS [SingleRowTable1]
                           UNION ALL

                           SELECT 8 AS [C1]
                           FROM   (SELECT 1 AS X) AS [SingleRowTable2]) AS [UnionAll1]
                   UNION ALL

                   SELECT 13 AS [C1]
                   FROM   (SELECT 1 AS X) AS [SingleRowTable3]) AS [UnionAll2]
         ON [Extent1].[Id] = [UnionAll2].[C1]

WTF.

image

It creates a select statement, for each value that you want to select… I thought “oh ok, i wonder what the execution plan looks like for that, compared to a normal in-clause from NHibernate.

Using both the queries already shown above, showing the actual execution plans, I get the following:

image

Wow… But it gets better, I have 20 records in my database, and I thought, what happens if my list of Ids contains 10 different ids:

Same queries, but with:

var fruitIds = new List<int> { 2, 4, 6, 8, 10, 12, 14, 16, 18, 20 };

Entity Framework (I don’t even want to paste this in here…):

SELECT [Extent1].[Id]   AS [Id],
       [Extent1].[Name] AS [Name]
FROM   [dbo].[Fruit] AS [Extent1]
       INNER JOIN (SELECT [UnionAll8].[C1] AS [C1]
                   FROM   (SELECT [UnionAll7].[C1] AS [C1]
                           FROM   (SELECT [UnionAll6].[C1] AS [C1]
                                   FROM   (SELECT [UnionAll5].[C1] AS [C1]
                                           FROM   (SELECT [UnionAll4].[C1] AS [C1]
                                                   FROM   (SELECT [UnionAll3].[C1] AS [C1]
                                                           FROM   (SELECT [UnionAll2].[C1] AS [C1]
                                                                   FROM   (SELECT [UnionAll1].[C1] AS [C1]
                                                                           FROM   (SELECT 2 AS [C1]
                                                                                   FROM   (SELECT 1 AS X) AS [SingleRowTable1]
                                                                                   UNION ALL

                                                                                   SELECT 4 AS [C1]
                                                                                   FROM   (SELECT 1 AS X) AS [SingleRowTable2]) AS [UnionAll1]
                                                                           UNION ALL

                                                                           SELECT 6 AS [C1]
                                                                           FROM   (SELECT 1 AS X) AS [SingleRowTable3]) AS [UnionAll2]
                                                                   UNION ALL

                                                                   SELECT 8 AS [C1]
                                                                   FROM   (SELECT 1 AS X) AS [SingleRowTable4]) AS [UnionAll3]
                                                           UNION ALL

                                                           SELECT 10 AS [C1]
                                                           FROM   (SELECT 1 AS X) AS [SingleRowTable5]) AS [UnionAll4]
                                                   UNION ALL

                                                   SELECT 12 AS [C1]
                                                   FROM   (SELECT 1 AS X) AS [SingleRowTable6]) AS [UnionAll5]
                                           UNION ALL

                                           SELECT 14 AS [C1]
                                           FROM   (SELECT 1 AS X) AS [SingleRowTable7]) AS [UnionAll6]
                                   UNION ALL

                                   SELECT 16 AS [C1]
                                   FROM   (SELECT 1 AS X) AS [SingleRowTable8]) AS [UnionAll7]
                           UNION ALL

                           SELECT 18 AS [C1]
                           FROM   (SELECT 1 AS X) AS [SingleRowTable9]) AS [UnionAll8]
                   UNION ALL

                   SELECT 20 AS [C1]
                   FROM   (SELECT 1 AS X) AS [SingleRowTable10]) AS [UnionAll9]
         ON [Extent1].[Id] = [UnionAll9].[C1]

NHibernate:

select nhfruit0_.Id   as Id0_,
       nhfruit0_.Name as Name0_
from   Fruit nhfruit0_
where  nhfruit0_.Id in (2 /* @p0 */,4 /* @p1 */,6 /* @p2 */,8 /* @p3 */,
                        10 /* @p4 */,12 /* @p5 */,14 /* @p6 */,16 /* @p7 */,
                        18 /* @p8 */,20 /* @p9 */)

Now if you run with the execution plans, (I pressed execute 10 times before taking this screenshot)

image

The more parameters you add, the slower it gets.

You can’t blame Entity Framework though. EF supports the same query as NHibernate!

var fruitIds = new List<int> { 2, 4, 6, 8, 10, 12, 14, 16, 18, 20 };

using (var ctx = new EFTestEntities())
{
    var result = from f in ctx.Fruits
                 where fruitIds.Contains(f.Id)
                 select f;

    foreach (var fruit in result)
        Console.WriteLine(fruit.Name);
}

Entity Framework generates the following query:

SELECT [Extent1].[Id]   AS [Id],
       [Extent1].[Name] AS [Name]
FROM   [dbo].[Fruit] AS [Extent1]
WHERE  [Extent1].[Id] IN (2,4,6,8,
                          10,12,14,16,
                          18,20)

So I personally think the original approach, to do a join on an in-memory list, is bad practice, regardless of weather it works or not. It’s bad.

It’s also bad to blindly write LINQ queries without ever looking at the SQL they produce. Sure it’s great to not have to worry about writing SQL, but you should still always be aware of the SQL that is been run, sometimes slightly changing the LINQ can massively alter the SQL that is produced.

‘let’ keyword in ORMs

January 29th, 2011 1 comment

A while ago MindScape posted a new feature called Ninja Properties for their ORM, LightSpeed. Their post happened to be a scenario I used to solve with LINQ 2 SQL using the ‘let’ keyword to create a composite type to query against.

This would allow you to do something along the lines of:

var poo = from s in ctx.Members
            let fullname = s.FirstName + " " + s.LastName
            where fullname == "Robert Williams"
            select s;

This nifty query would generate SQL like so:

SELECT [t1].[Id], [t1].[FirstName], [t1].[LastName]
FROM (
    SELECT [t0].[Id], [t0].[FirstName],
           [t0].[LastName],
           ([t0].[FirstName] + ' ') + [t0].[LastName] AS [value]
    FROM [Member] AS [t0]
    ) AS [t1]
WHERE [t1].[value] = 'Robert Williams'

After commenting in their blog post about it they implemented it and put it into the nightly build. So after some months I finally got around to testing it, for a couple of reasons. Back when NHibernate 3.0 was in Alpha, this technique didn’t work! So I thought I would test it out and see if it does, as well as checkout the SQL that’s generated.

I’m using the following ORM’s

  • LightSpeed 3.11 Nightly Build (3.1 it doesn’t work)
  • NHibernate 3.0
  • Linq 2 Sql
  • Entity Framework 4
    The data model is simple, but I have two scenarios I want to test. I created a Member and Task. The Member has a One-to-Many with Tasks.The test data:
    insert into member (firstname, lastname) values ('Robert', 'Williams')
    insert into member (firstname, lastname) values ('Michael', 'Jones')
    insert into member (firstname, lastname) values ('William', 'Brown')
    insert into member (firstname, lastname) values ('David', 'Davis')
    insert into member (firstname, lastname) values ('Richard', 'Miller')
    insert into member (firstname, lastname) values ('Charles', 'Wilson')

    insert into task (MemberId, Name) values(1, 'Task 1')
    insert into task (MemberId, Name) values(1, 'Task 2')
    insert into task (MemberId, Name) values(1, 'Task 3')
    insert into task (MemberId, Name) values(1, 'Task 4')
    insert into task (MemberId, Name) values(2, 'Task 1')
    insert into task (MemberId, Name) values(2, 'Task 2')
    insert into task (MemberId, Name) values(2, 'Task 3')
    insert into task (MemberId, Name) values(3, 'Task 4')

    Scenario 1: Find all Members where fullname contains ‘w’, case insensitive (lowercase the fullname)
    Scenario 2: Find all Members who have more than 2 tasks.
    These tests are not to say one ORM is better than another, I did this purely out of curiosity and am sharing my results.

     

    Scenario 1:

    I ran the same query against each ORM (to see the full script click here), the query is:

    var poo = from s in ctx.Members
                let fullname = s.FirstName + " " + s.LastName
                where fullname.ToLower().Contains("w")
                select s;

All queries are identical with the exception of the ‘in’ part, varying between ORMs.

LightSpeed : from s in uow.Members
NHibernate 3.0 : from s in session.Query<NHibernate.Member>()
Linq 2 Sql : from s in ctx.Members
Entity Framework 4: from s in mmc.Members

The output was as follows:

————
LINQ 2 SQL
Robert
William
Charles
————
LightSpeed
Robert
William
Charles
————
NHibernate 3.0
Robert
William
Charles
————
EntityFramework 4
Robert
William
Charles

 

All ORM’s worked perfectly. How about the SQL Generated from these?

Linq 2 SQL:

SELECT [t1].[Id], [t1].[FirstName], [t1].[LastName]
FROM (
    SELECT [t0].[Id], [t0].[FirstName],
           [t0].[LastName],
           ([t0].[FirstName] + ' ') + [t0].[LastName] AS [value]
    FROM [Member] AS [t0]
    ) AS [t1]
WHERE LOWER([t1].[value]) LIKE '%w%'

LightSpeed:

SELECT
  Member.Id AS [Member.Id],
  Member.FirstName AS [Member.FirstName],
  Member.LastName AS [Member.LastName]
FROM
  Member
WHERE
  LOWER(((Member.FirstName + ' ') + Member.LastName)) LIKE '%w%'

NHibernate:

select member0_.Id as Id0_,
       member0_.FirstName as FirstName0_,
       member0_.LastName as LastName0_
from [Member] member0_
where lower(member0_.FirstName+' '+member0_.LastName) like ('%'+'w'+'%')


Entity Framework 4.0:

SELECT
    [Extent1].[Id] AS [Id],
    [Extent1].[FirstName] AS [FirstName],
    [Extent1].[LastName] AS [LastName]
FROM [dbo].[Member] AS [Extent1]
WHERE LOWER([Extent1].[FirstName] + N' ' + [Extent1].[LastName])
      LIKE N'%w%'

Note: The first 3 queries are RPC’s and show in SQL Profiler with parameters, I’ve stripped the parameters out to show the readable query.

Great, so all 3 work fine. Tho I don’t like the SQL generated by Linq 2 Sql. Hard to believe Stack Overflow runs off it.

 

Scenario 2

The second scenario is again, the exact same query, you could write this multiple ways, however I found it cleaner to use ‘let’ back when I was using Linq 2 Sql. (to see the full script click here), the query is:

var poo = from s in ctx.Members
            let tasks = s.Tasks.COUNT() as Computed
            where tasks > 2
            select s;

Again, the only difference is the ‘in’ part of the query.

The output was as follows:

————
LINQ 2 SQL
Robert
Michael
————
LightSpeed
Exception: Specified method is not supported.
————
NHibernate 3.0
Robert
Michael
————
EntityFramework 4
Robert
Michael

 

All achieved the same results, except for LightSpeed. However I didn’t think NHibernate would support this?!? Seems they have done quite a bit of work implementing LINQ since Alpha.

How about the SQL generated?

Linq 2 Sql:

SELECT [t2].[Id], [t2].[FirstName], [t2].[LastName]
FROM (
    SELECT [t0].[Id], [t0].[FirstName], [t0].[LastName], (
        SELECT COUNT(*)
        FROM [Task] AS [t1]
        WHERE [t1].[MemberId] = [t0].[Id]
        ) AS [value]
    FROM [Member] AS [t0]
    ) AS [t2]
WHERE [t2].[value] > 2

NHibernate:

select member0_.Id as Id0_,
       member0_.FirstName as FirstName0_,
       member0_.LastName as LastName0_
from [Member] member0_
where (select cast(count(*) as INT)
        from [Task] tasks1_
        where member0_.Id=tasks1_.MemberId) > 2

Entity Framework:

SELECT
[Project1].[Id] AS [Id],
[Project1].[FirstName] AS [FirstName],
[Project1].[LastName] AS [LastName]
FROM ( SELECT
    [Extent1].[Id] AS [Id],
    [Extent1].[FirstName] AS [FirstName],
    [Extent1].[LastName] AS [LastName],
    (SELECT
        COUNT(1) AS [A1]
        FROM [dbo].[Task] AS [Extent2]
        WHERE [Extent1].[Id] = [Extent2].[MemberId]) AS [C1]
    FROM [dbo].[Member] AS [Extent1]
)  AS [Project1]
WHERE [Project1].[C1] > 2

 

I get the feeling that NHibernate generates more efficient queries than L2S/EF. Might be worth doing some performance testing against them oneday. Smile

Anyway as you can see, ‘let’ keyword allows you to create some nice queries that are easy to ready instead of trying to complicate things in your ‘where’ clause, when writing LINQ.