1

I have been struggling to get an old query translated to Nhibernate. We are upgrading an old project from Nhibernate 2 to the latest version. I am using the QueryOver syntax since Linq wasn't an option because of the complexity of the queries (advice of a colleague).

I want to query the DB (Oracle) to get some results which have to be grouped. As result I need a grouped collection of my DTO. I also noticed that nhibernate has trouble translating to a DTO with complex properties (nested DTO's) To fix this I found this topic. This works great but I am not a fan of the magic strings...

I will add some code snippets of how my monster query is looking at this moment. Problem is I can't seem to figure out how to add a group by without breaking everything else. So I would want to group on a property but still have the DTO in my results. Something like:

ILookup<int,IEnumerable<NieuwePrintopdrachtenInfo>>

Any help would be welcome.

Sorry the variables and classes are in Dutch -_-

SYNUITGAANDEBRIEF uitgaandebrief = null;
SYNAANVRAAG joinedAanvraag = null;
SYNDOSSIER joinedDossier = null;
SYNVERBRUIKSADRES joinedVerbruiksAdres = null;
SYNEAN joinedEan = null;
SYNCTENERGIETYPE joinedEnergieType = null;
SYNBRIEFBESTEMMELINGEN joinedBriefBestemmeling = null;
SYNCTBRIEFTYPE joinedBriefType = null;
SYNCTBRIEFSTATUS joinedBriefStatus = null;
SYNCONTACTPERSOON joinedContactpersoon = null;
SYNCTCONTACTPERSOONTYPE joinedBestemmelingType = null;
SYNCTVERZENDMODUSTYPE joinedVerzendModus = null;
SYNCTCONTACTPERSOONTYPE joinedContactpersoonType = null;
SYNCTTAAL joinedContactpersoonTaal = null;
SYNTOEWIJZVERBRUIKVERANT joinedVerbruiksVerantw = null;
SYNCTPROFIELGROEP joinedProfielGroep = null;

var baseQuery = SessionHandler.CurrentSession.QueryOver(() => uitgaandebrief)
    .JoinAlias(() => uitgaandebrief.AANVRAAGCollection, () => joinedAanvraag)
    .JoinAlias(() => joinedAanvraag.DOSSIER, () => joinedDossier)
    .JoinAlias(() => joinedDossier.VERBRUIKSADRES, () => joinedVerbruiksAdres)
    .JoinAlias(() => joinedAanvraag.EAN, () => joinedEan)
    .JoinAlias(() => joinedEan.CtEnergietype, () => joinedEnergieType)
    .JoinAlias(() => uitgaandebrief.BRIEFBESTEMMELINGENCollection, () => joinedBriefBestemmeling)
    .JoinAlias(() => uitgaandebrief.CtBriefType, () => joinedBriefType)
    .JoinAlias(() => uitgaandebrief.CtBriefStatus, () => joinedBriefStatus)
    .JoinAlias(() => joinedBriefBestemmeling.CONTACTPERSOONCollection, () => joinedContactpersoon, JoinType.LeftOuterJoin)
    .JoinAlias(() => joinedBriefBestemmeling.CtContactPersoonType, () => joinedBestemmelingType, JoinType.LeftOuterJoin)
    .JoinAlias(() => joinedBriefBestemmeling.CtVerzendModus, () => joinedVerzendModus, JoinType.LeftOuterJoin)
    .JoinAlias(() => joinedContactpersoon.CtContactpersoonType, () => joinedContactpersoonType, JoinType.LeftOuterJoin)
    .JoinAlias(() => joinedContactpersoon.CtTaal, () => joinedContactpersoonTaal, JoinType.LeftOuterJoin)
    .JoinAlias(() => joinedContactpersoon.TOEWIJZVERBRUIKVERANTCollection, () => joinedVerbruiksVerantw, JoinType.LeftOuterJoin)
    .JoinAlias(() => joinedContactpersoon.CtProfielGroep, () => joinedProfielGroep, JoinType.LeftOuterJoin);

This is only the beginning. Here comes the part to filter the results (when needed).

if (briefType.HasValue)
{
    baseQuery.Where(() => uitgaandebrief.BriefType == briefType.Value);
}

if (verzendModus.HasValue)
{
    baseQuery.Where(() => joinedBriefBestemmeling.VerzendModus == verzendModus.Value);
}

if (!string.IsNullOrEmpty(binnenland) && binnenland.Trim() != "-1")
{
    baseQuery.Where(() => joinedBriefBestemmeling.BinnenLand == binnenland.ToBoolean());
}

Then I got the part to select the stuff I need and translate it into the DTO (NieuwePrintopdrachtenInfo).

NieuwePrintopdrachtenInfo nieuwePrintopdrachtInfo = null;
baseQuery.SelectList(list => list
    .Select(() => uitgaandebrief.UitgaandebriefId).WithAlias(() => nieuwePrintopdrachtInfo.UitgaandeBriefId)
    .Select(() => uitgaandebrief.DatumInplanning).WithAlias(() => nieuwePrintopdrachtInfo.InplanningsDatum)
    .Select(() => uitgaandebrief.ErrorReden).WithAlias(() => nieuwePrintopdrachtInfo.Probleem)
    .Select(() => uitgaandebrief.ErrorNr).WithAlias(() => nieuwePrintopdrachtInfo.ErrorNummer)
    .Select(() => uitgaandebrief.DatumCreatie).WithAlias(() => nieuwePrintopdrachtInfo.CreatieDatumBrief)
    .Select(() => uitgaandebrief.DatumUpdate).WithAlias(() => nieuwePrintopdrachtInfo.DatumLaatsteWijzigingBrief)
    .Select(() => uitgaandebrief.UserCreatie).WithAlias(() => nieuwePrintopdrachtInfo.BrieUserCreatie)
    .Select(() => uitgaandebrief.UserUpdate).WithAlias(() => nieuwePrintopdrachtInfo.BriefUserUpdate)
    .Select(() => uitgaandebrief.DatumAnnulatieElektriciteit).WithAlias(() => nieuwePrintopdrachtInfo.DatumElektriciteitGeannuleerd)
    .Select(() => uitgaandebrief.DatumAnnulatieGas).WithAlias(() => nieuwePrintopdrachtInfo.DatumGasGeannuleerd)
    .Select(() => joinedDossier.DossierId).WithAlias(() => nieuwePrintopdrachtInfo.DossierId)
    .Select(() => joinedDossier.DossierNr).WithAlias(() => nieuwePrintopdrachtInfo.DossierNr)
    .Select(() => joinedEnergieType.Omschrijving).WithAlias(() => nieuwePrintopdrachtInfo.EnergieTypeBrief)
    .Select(() => joinedBriefType.Omschrijving).WithAlias(() => nieuwePrintopdrachtInfo.TypeBrief)
    .Select(() => joinedVerzendModus.Omschrijving).WithAlias(() => nieuwePrintopdrachtInfo.VerzendModus)
    .Select(() => joinedVerzendModus.Omschrijving).WithAlias(() => nieuwePrintopdrachtInfo.BestemmelingVerzendModus)
    .Select(() => joinedBriefBestemmeling.BriefBestemmelingenId).WithAlias(() => nieuwePrintopdrachtInfo.BestemmelingId)
    .Select(() => joinedBestemmelingType.Omschrijving).WithAlias(() => nieuwePrintopdrachtInfo.BestemmelingContactpersoonType)
    .Select(() => joinedBriefBestemmeling.BestemmelingElektriciteit).WithAlias(() => nieuwePrintopdrachtInfo.BestemmelingElek)
    .Select(() => joinedBriefBestemmeling.BestemmelingGas).WithAlias(() => nieuwePrintopdrachtInfo.BestemmelingGas)
    .Select(() => joinedBriefBestemmeling.BinnenLand).WithAlias(() => nieuwePrintopdrachtInfo.BestemmelingBinnenLand)
    .Select(() => joinedVerbruiksAdres.Land).WithAlias(() => nieuwePrintopdrachtInfo.LandVerbuiksadres)
    .Select(Projections.Property(() => joinedContactpersoon.ContactpersoonId).As("BestemmelingContactPersoon.ContactPersoonId"))
    .Select(Projections.Property(() => joinedContactpersoonType.Omschrijving).As("BestemmelingContactPersoon.TypeContactPersoon"))
    .Select(Projections.Property(() => joinedContactpersoon.VoorNaam).As("BestemmelingContactPersoon.VoorNaam"))
    .Select(Projections.Property(() => joinedContactpersoon.Naam).As("BestemmelingContactPersoon.Naam"))
    .Select(Projections.Property(() => joinedContactpersoon.Straat).As("BestemmelingContactPersoon.Straat"))
    .Select(Projections.Property(() => joinedContactpersoon.HuisNr).As("BestemmelingContactPersoon.HuisNummer"))
    .Select(Projections.Property(() => joinedContactpersoon.BusNr).As("BestemmelingContactPersoon.BusNummer"))
    .Select(Projections.Property(() => joinedContactpersoon.Gemeente).As("BestemmelingContactPersoon.Gemeente"))
    .Select(Projections.Property(() => joinedContactpersoon.PostCode).As("BestemmelingContactPersoon.PostCode"))
    .Select(Projections.Property(() => joinedContactpersoon.Appartement).As("BestemmelingContactPersoon.Appartement"))
    .Select(Projections.Property(() => joinedContactpersoon.Verdieping).As("BestemmelingContactPersoon.Verdieping"))
    .Select(Projections.Property(() => joinedContactpersoon.Telefoon1).As("BestemmelingContactPersoon.Telefoon1"))
    .Select(Projections.Property(() => joinedContactpersoon.Telefoon2).As("BestemmelingContactPersoon.Telefoon2"))
    .Select(Projections.Property(() => joinedContactpersoon.FAXNr).As("BestemmelingContactPersoon.Fax"))
    .Select(Projections.Property(() => joinedContactpersoon.Email).As("BestemmelingContactPersoon.Email"))
    .Select(Projections.Property(() => joinedContactpersoon.DatumCreatie).As("BestemmelingContactPersoon.DatumCreatie"))
    .Select(Projections.Property(() => joinedContactpersoon.UserCreatie).As("BestemmelingContactPersoon.UserCreatie"))
    .Select(Projections.Property(() => joinedContactpersoon.DatumUpdate).As("BestemmelingContactPersoon.DatumUpdate"))
    .Select(Projections.Property(() => joinedContactpersoon.UserUpdate).As("BestemmelingContactPersoon.UserUpdate"))
    .Select(Projections.Property(() => joinedContactpersoon.AdresBijTeWerken).As("BestemmelingContactPersoon.IsAdresBijTeWerken"))
    .Select(Projections.Property(() => joinedContactpersoon.Titel).As("BestemmelingContactPersoon.Titel"))
    .Select(Projections.Property(() => joinedContactpersoon.NietBesteldeBrief).As("BestemmelingContactPersoon.NietBesteldeBrief"))
    .Select(Projections.Property(() => joinedContactpersoon.Land).As("BestemmelingContactPersoon.Land"))
    .Select(Projections.Property(() => joinedContactpersoon.ContactpersoonAlsAanbrengerGebruikt).As("BestemmelingContactPersoon.ContactPersoonIdAlsAanbrenger"))
    .Select(Projections.Property(() => joinedContactpersoon.ContactpersoonIsBetrokken).As("BestemmelingContactPersoon.ContactPersoonIsBetrokken"))
    .Select(Projections.Property(() => joinedContactpersoon.NietAfgehaaldeBrief).As("BestemmelingContactPersoon.NietAfgehaaldeBrief"))
    .Select(Projections.Property(() => joinedContactpersoonTaal.Omschrijving).As("BestemmelingContactPersoon.Taal"))
    .Select(Projections.Property(() => joinedProfielGroep.Omschrijving).As("BestemmelingContactPersoon.IngegevenDoor"))
    .Select(Projections.Property(() => joinedEan.Energietype).As("BestemmelingContactPersoon.EnergieType"))
    .Select(Projections.Property(() => joinedVerbruiksVerantw.ToewijzigingVerbruiksVerantwoordelijkeId).As("BestemmelingContactPersoon.VerbruiksVerantwoordelijkeId")));

Yeah I know it is a mess. Now that you made it this far, you'll be happy to know we are almost there. This is the code I use to return the results (It is generic and uses the DeepTransform which I found here)

protected IEnumerable<TR> GetDeepTransformedPagedList<T, TR>(IQueryOver<T, T> query) where TR : class
{
    PagingSettings.Count = query.Clone().Select(Projections.CountDistinct(PagingSettings.PropertyNameToCountOn)).FutureValue<int>().Value;
    query = query.TransformUsing(new DeepTransformer<TR>());

    if (PagingSettings.Enabled)
    {
        var pagedQuery = query.Skip(GetPagingStartRowIndex()).Take(PagingSettings.PageSize);
        return pagedQuery.List<TR>();
    }

    return query.List<TR>();
}

EDIT

After the helpful post of Radim Köhler I found out that a group by won't help me with my problem. That's why I'll explain the real problem. In code the previous query is build and extended with a Skip & Take for paging purpose. In my situation I get 50 results when executing the query. These 50 results contain duplicates and need to be grouped by UitgaandeBriefId. That's why the original developers wrote this code that is executed once the results are back from the DB.

ILookup<int, IEnumerable<NieuwePrintopdrachtenInfo>> groupedbrieven =
            (from tbInfo in brieven
             group tbInfo by tbInfo.UitgaandeBriefId into g
             let UitgaandeBriefId = g.Key
             let Group = g as IEnumerable<NieuwePrintopdrachtenInfo>
             select new { UitgaandeBriefId, Group })
             .ToLookup(result => result.UitgaandeBriefId, result => result.Group);

This code still works but results in getting only 32 results. This causes my pages to never contain 50 results. The original developer used server side paging instead of doing it on the DB so he never got this problem (performance wise this was a huge problem). That's why I refactored it so it would execute a lot faster, but this results in not getting exectly 50 results. I guess I'll need to add a distinct then but I have no clue how I get this to work in NHibernate since I am used to work with EntityFramework.

Community
  • 1
  • 1
Beejee
  • 1,836
  • 2
  • 17
  • 31

1 Answers1

4

In general, if we want to change our projection to be using GROUP BY, we have to change all "SELECT" parts to be either part of GROUP BY or SUM, MIN ...

We can do it with this kind of syntax

// firstly 
// the original part from the question above
baseQuery.SelectList(list => list
    ...
    .Select(() => joinedBriefBestemmeling.BinnenLand)
       .WithAlias(() => nieuwePrintopdrachtInfo.BestemmelingBinnenLand)
    .Select(() => joinedVerbruiksAdres.Land)
       .WithAlias(() => nieuwePrintopdrachtInfo.LandVerbuiksadres)
    .Select(Projections.Property(() => joinedContactpersoon.ContactpersoonId)
       .As("BestemmelingContactPersoon.ContactPersoonId"))
    .Select(Projections.Property(() => joinedContactpersoonType.Omschrijving)
       .As("BestemmelingContactPersoon.TypeContactPersoon"))
    ...



// changed, to use GROUP BY
baseQuery.SelectList(list => list
    ...
    .SelectGroup(() => joinedBriefBestemmeling.BinnenLand)
       .WithAlias(() => nieuwePrintopdrachtInfo.BestemmelingBinnenLand)
    .SelectGroup(() => joinedVerbruiksAdres.Land)
       .WithAlias(() => nieuwePrintopdrachtInfo.LandVerbuiksadres)
    .Select
       (Projections.Alias
         (Projections.GroupProperty
           (Projections.Property(() => joinedContactpersoon.ContactpersoonId))
         , "BestemmelingContactPersoon.ContactPersoonId"))
    .Select
       (Projections.Alias
         (Projections.GroupProperty
           (Projections.Property(() => joinedContactpersoonType.Omschrijving))
         , "BestemmelingContactPersoon.TypeContactPersoon"))
     ...

So, now we have the GROUP BY (instead of just a SELECT) replacing the original code. But we can do more, we can introduce these (just a quick version) Extension methods (just a light version, really - but working)

public static class Extensions
{
    public static NHibernate.Criterion.Lambda.QueryOverProjectionBuilder<T> GroupByProperty<T>(
        this NHibernate.Criterion.Lambda.QueryOverProjectionBuilder<T> builder, 
        System.Linq.Expressions.Expression<Func<object>> propertyExpression,
        System.Linq.Expressions.Expression<Func<object>> aliasExpression)
    {
        var alias = aliasExpression.ParseProperty();

        var propertyProjection = Projections.Property(propertyExpression);
        var groupProjection = Projections.GroupProperty(propertyProjection);
        var withAliasProjection = Projections.Alias(groupProjection, alias);

        builder.Select(withAliasProjection);
        return builder;
    }

    public static string ParseProperty<TFunc>(this System.Linq.Expressions.Expression<TFunc> expression)
    {
        var body = expression.Body as System.Linq.Expressions.MemberExpression;
        if (body.IsNull())
        {
            return null;
        }

        string propertyName = body.Member.Name;

        ParseParentProperty(body.Expression as System.Linq.Expressions.MemberExpression, ref propertyName);

        // change the   alias.ReferenceName.PropertyName
        // to just            ReferenceName.PropertyName
        var justAPropertyChain = propertyName.Substring(propertyName.IndexOf('.') + 1);
        return justAPropertyChain;
    }

    static void ParseParentProperty(System.Linq.Expressions.MemberExpression expression, ref string propertyName)
    {
        if (expression.IsNull())
        {
            return;
        }
        // Parent.PropertyName
        propertyName = expression.Member.Name + "." + propertyName;

        ParseParentProperty(expression.Expression as System.Linq.Expressions.MemberExpression, ref propertyName);
    }
}

And the above code could be made more readable and common, without any magic string

baseQuery.SelectList(list => list
    ...
    .GroupByProperty(() => joinedBriefBestemmeling.BinnenLand)
       ,() => nieuwePrintopdrachtInfo.BestemmelingBinnenLand)
    .GroupByProperty(() => joinedVerbruiksAdres.Land)
       ,() => nieuwePrintopdrachtInfo.LandVerbuiksadres)
    .GroupByProperty(() => joinedContactpersoon.ContactpersoonId)
       .() => nieuwePrintopdrachtInfo.BestemmelingContactPersoon.ContactPersoonId)
    .GroupByProperty(() => joinedContactpersoonType.Omschrijving)
       .() => nieuwePrintopdrachtInfo.BestemmelingContactPersoon.TypeContactPersoon)
    ...

NOTE IsNull() is also extension

Radim Köhler
  • 122,561
  • 47
  • 239
  • 335
  • Thank you for your answer. It realy helped me a lot. But it seems like grouping on everything doesn't give me the result I want. Using SelectGroup works but doesn't seem to fix my situation. I will update my question to explain it. – Beejee Sep 02 '15 at 07:35
  • Also I tried out your extension method. It looks way better than what I was using. Problem is that I get NullReferenceExceptions now. As usual the error ocurs on the part where the query is executed so finding the real problem is a pain in the ... I started debugging the ParseProperty method and it looks like most of the time "body" is actualy null. Am i missing something? – Beejee Sep 02 '15 at 07:36
  • K looks like i've been able to fix it. I think your version has been striped down too much. I added some extra stuff to check the "NodeType" and when it is "ExpressionType.Convert", I use this as my body "((UnaryExpression)expression.Body).Operand as MemberExpression" Works like a charm now. Now I only need to have the right results returned – Beejee Sep 02 '15 at 07:51
  • Sorry for NOT complete extensions... I just wanted to show you the way how to encapsulate that so complex Projection tree... into one small extension (and avoid the magic strings). I tested locally on small example, and all was working for me. Group over all is simply must (try to create SQL statement by your own, and not place every statement from S ELECT into GROUP BY - it will fail, unless you will use MIN, MAX... – Radim Köhler Sep 02 '15 at 07:59
  • Yeah I think group by is not usefull in my situation. I think i'll need to do a Distinct to make sure I don't get duplicates because of the Joining. Is it possible to do just one distinct on UitgaandebriefId but still get everything else? I'll now edit my question to better explain the situation. – Beejee Sep 02 '15 at 08:39
  • You should play. Because now you know how, I'd say. In general. Your scenario seems to me very exceptional in ORM world. Your query is more about reporting. So, 1) either create **view** and **map** it as an Entity. Or 2) simply use simplified query and count on Lazy loading and batch fetching - one of my many tries to show how could be found here http://stackoverflow.com/a/23126791/1679310 hope it helps a bit – Radim Köhler Sep 02 '15 at 08:43
  • Radim do you have another version of the DeepTransformer that works like the original DistinctRootEntity? Using Views is not an option because the whole applications needs to be refactored than... – Beejee Sep 02 '15 at 10:40
  • Do you have a version of your DeepTransformer that acts like DistinctRootEntity but can be used to map to a DTO? – Beejee Sep 03 '15 at 13:02
  • No, sorry... no. I would like to help.. but my experience is - do not do it complex. As I suggested above... Sorry, wish to have another answer, but this is the way I do use it.... – Radim Köhler Sep 03 '15 at 13:03