consider silly domain:
namespace tryhibernate.example { public class employee { public int id { get; set; } public string name { get; set; } } public class workitem { public int id { get; set; } public string description { get; set; } public datetime startdate { get; set; } public datetime enddate { get; set; } } public class task { public int id { get; set; } public employee assignee { get; set; } public workitem workitem { get; set; } public string details { get; set; } public datetime? startdateoverride { get; set; } public datetime? enddateoverride { get; set; } } }
the idea each work item may assigned multiple employees different details, potentially overriding start/end dates of work item itself. if overrides null, should taken work item instead.
now i'd perform query restrictions on effective dates. i've tried first:
ilist<task> tasks = db.queryover<task>(() => taskalias) .joinalias(() => taskalias.workitem, () => wialias) .where(() => taskalias.startdateoverride.coalesce(() => wialias.startdate) <= end) .and(() => taskalias.enddateoverride.coalesce(() => wialias.enddate) >= start) .list();
unfortunately, doesn't compile coalesce
expects constant, not property expression.
ok, i've tried this:
.where(() => (taskalias.startdateoverride == null ? wialias.startdate : taskalias.startdateoverride) <= end) .and(() => (taskalias.enddateoverride == null ? wialias.enddate : taskalias.enddateoverride) >= start)
this throws nullreferenceexception. not sure why, either because nhibernate doesn't translate ternary operator (and tries invoke instead) or because == null
isn't right way check nulls. anyway, didn't expect work.
finally, 1 works:
ilist<task> tasks = db.queryover<task>(() => taskalias) .joinalias(() => taskalias.workitem, () => wialias) .where(restrictions.leproperty( projections.sqlfunction("coalesce", nhibernateutil.datetime, projections.property(() => taskalias.startdateoverride), projections.property(() => wialias.startdate)), projections.constant(end))) .and(restrictions.geproperty( projections.sqlfunction("coalesce", nhibernateutil.datetime, projections.property(() => taskalias.enddateoverride), projections.property(() => wialias.enddate)), projections.constant(start))) .list();
but there no way can call clean code. maybe can extract expressions separate methods clean little bit, better use expression syntax rather these ugly projections. there way it? there reason behind nhibernate not supporting property expressions in coalesce
extension?
one obvious alternative select , filter results using linq or whatever. become performance problem large number of total rows.
here full code in case wants try it:
using (isessionfactory sessionfactory = fluently.configure() .database(sqliteconfiguration.standard.usingfile("temp.sqlite").showsql()) .mappings(m => m.automappings.add( automap.assemblyof<employee>(new exampleconfig()) .conventions.add(defaultlazy.never()) .conventions.add(defaultcascade.all()))) .exposeconfiguration(c => new schemaexport(c).create(true, true)) .buildsessionfactory()) { using (isession db = sessionfactory.opensession()) { employee empl = new employee() { name = "joe" }; workitem wi = new workitem() { description = "important work", startdate = new datetime(2016, 01, 01), enddate = new datetime(2017, 01, 01) }; task task1 = new task() { assignee = empl, workitem = wi, details = "do this", }; db.save(task1); task task2 = new task() { assignee = empl, workitem = wi, details = "do that", startdateoverride = new datetime(2016, 7, 1), enddateoverride = new datetime(2017, 1, 1), }; db.save(task2); task taskalias = null; workitem wialias = null; datetime start = new datetime(2016, 1, 1); datetime end = new datetime(2016, 6, 30); ilist<task> tasks = db.queryover<task>(() => taskalias) .joinalias(() => taskalias.workitem, () => wialias) // doesn't compile: //.where(() => taskalias.startdateoverride.coalesce(() => wialias.startdate) <= end) //.and(() => taskalias.enddateoverride.coalesce(() => wialias.enddate) >= start) // throws nullreferenceexception: //.where(() => (taskalias.startdateoverride == null ? wialias.startdate : taskalias.startdateoverride) <= end) //.and(() => (taskalias.enddateoverride == null ? wialias.enddate : taskalias.enddateoverride) >= start) // works: .where(restrictions.leproperty( projections.sqlfunction("coalesce", nhibernateutil.datetime, projections.property(() => taskalias.startdateoverride), projections.property(() => wialias.startdate)), projections.constant(end))) .and(restrictions.geproperty( projections.sqlfunction("coalesce", nhibernateutil.datetime, projections.property(() => taskalias.enddateoverride), projections.property(() => wialias.enddate)), projections.constant(start))) .list(); foreach (task t in tasks) console.writeline("found task: {0}", t.details); } }
and configuration simple:
class exampleconfig : defaultautomappingconfiguration { public override bool shouldmap(type type) { return type.namespace == "tryhibernate.example"; } }
let start this:
// doesn't compile: //.where(() => taskalias.startdateoverride.coalesce(() => wialias.startdate) <= end) //.and(() => taskalias.enddateoverride.coalesce(() => wialias.enddate) >= start)
and modify to:
.where(() => taskalias.startdateoverride.coalesce(wialias.startdate) <= end) .and(() => taskalias.enddateoverride.coalesce(wialias.enddate) >= start)
now compile. @ runtime generates same nullreferenceexception
. no good.
it turns out nhibernate indeed tries evaluate coalesce
argument. can seen looking @ projectionextensions
class implementation. following method handles coalesce
translation:
internal static iprojection processcoalesce(methodcallexpression methodcallexpression) { iprojection projection = expressionprocessor.findmemberprojection(methodcallexpression.arguments[0]).asprojection(); object obj = expressionprocessor.findvalue(methodcallexpression.arguments[1]); return projections.sqlfunction("coalesce", (itype) nhibernateutil.object, projection, projections.constant(obj)); }
notice different handling of first argument (findmemberexpresion
) vs second argument (findvalue
). well, findvalue
tries evaluate expression.
now know causing issue. have no idea why implemented way, concentrate on finding solution.
fortunately, expressionprocessor
class public , allows register custom methods via registercustommethodcall
/ registercustomprojection
methods. leads solution:
- create custom extensions methods similar
coalesce
(let call themifnull
instance) - register custom processor
- use them instead of
coalesce
here implementation:
public static class customprojections { static customprojections() { expressionprocessor.registercustomprojection(() => ifnull(null, ""), processifnull); expressionprocessor.registercustomprojection(() => ifnull(null, 0), processifnull); } public static void register() { } public static t ifnull<t>(this t objectproperty, t replacevalueifisnull) { throw new exception("not used directly - use inside queryover expression"); } public static t? ifnull<t>(this t? objectproperty, t replacevalueifisnull) t : struct { throw new exception("not used directly - use inside queryover expression"); } private static iprojection processifnull(methodcallexpression mce) { var arg0 = expressionprocessor.findmemberprojection(mce.arguments[0]).asprojection(); var arg1 = expressionprocessor.findmemberprojection(mce.arguments[1]).asprojection(); return projections.sqlfunction("coalesce", nhibernateutil.object, arg0, arg1); } }
since these methods never called, need ensure custom processor registered calling register
method. it's empty method make sure static constructor of class invoked, actual registration happens.
so in example, include @ beginning:
customprojections.register();
then use inside query:
.where(() => taskalias.startdateoverride.ifnull(wialias.startdate) <= end) .and(() => taskalias.enddateoverride.ifnull(wialias.enddate) >= start)
and work expected.
p.s. above implementation works both constant , expression arguments, it's safe replacement of coalesce
.
Comments
Post a Comment