自定义F#QueryBuilder

时间:2018-07-20 00:13:48

标签: f# query-builder

如果我像(F#交互式)中那样创建自定义查询构建器

type T1 = T1 of int list
    with
        member this.Content = let (T1 list) = this in list

type QueryBuilder1() = 
    inherit Linq.QueryBuilder()
        member __.Source (source: T1) = base.Source(source.Content)

let qb1 = new QueryBuilder1()

let list = [1;2;3;4;5]
let t1 = T1 list

let q1 = query { for i in list do select i }
let q2 = query { for i in t1.Content do select i }
let q3 = qb1 { for i in t1 do select i } // OK

一切正常。

但是如果我尝试使用SQL数据库源进行同样的操作

#r @"C:\Root\Project\Ocnarf\packages\SQLProvider.1.1.44\lib\net451\FSharp.Data.SqlProvider.dll"

let [<Literal>] dbVendor = FSharp.Data.Sql.Common.DatabaseProviderTypes.MSSQLSERVER
let [<Literal>] schemaConnString = @"Data Source=..."
type internal Schema = FSharp.Data.Sql.SqlDataProvider<dbVendor, schemaConnString>
type DowntimeEntity = Schema.dataContext.``dbo.DowntimesEntity``
type DowntimeQuery = System.Linq.IQueryable<DowntimeEntity>

type T2 = T2 of DowntimeQuery
    with
        member this.Query = let (T2 q) = this in q

type QueryBuilder2() = 
    inherit Linq.QueryBuilder()
        member __.Source (source: T2) = base.Source(source.Query)

let db = Schema.GetDataContext()
let tables = db.Dbo
let qry = tables.Downtimes
let t2 = T2 qry
let qb2 = new QueryBuilder2()

let q4 = query { for d in qry do select d }
let q5 = query { for d in t2.Query do select d }
let q6 = qb2 { for d in t2 do select d } // exception

然后我得到以下运行时异常

  

System.NotSupportedException:这不是有效的查询表达式。   方法   'Microsoft.FSharp.Linq.QuerySource 2[FSharp.Data.Sql.Common.SqlEntity,System.Linq.IQueryable] Source[IQueryable](T2)' was used in a query but is not recognized by the F#-to-LINQ query translator. Check the specification of permitted queries and consider moving some of the operations out of the query expression at Microsoft.FSharp.Linq.QueryModule.TransInner$cont@1180-3(Boolean check, FSharpExpr immutQuery, Unit unitVar) at Microsoft.FSharp.Linq.QueryModule.TransInner(CanEliminate canElim, Boolean check, FSharpExpr immutQuery) at Microsoft.FSharp.Linq.QueryModule.TransInner(CanEliminate canElim, Boolean check, FSharpExpr immutQuery) at Microsoft.FSharp.Linq.QueryModule.TransInnerApplicative(Boolean check, FSharpExpr source, FSharpVar immutConsumingVar, FSharpExpr immutConsumingExpr) at Microsoft.FSharp.Linq.QueryModule.TransInner(CanEliminate canElim, Boolean check, FSharpExpr immutQuery) at Microsoft.FSharp.Linq.QueryModule.TransInnerAndCommit(CanEliminate canElim, Boolean check, FSharpExpr x) at Microsoft.FSharp.Linq.QueryModule.TransInnerWithFinalConsume(CanEliminate canElim, FSharpExpr immutSource) at Microsoft.FSharp.Linq.QueryModule.EvalNonNestedInner(CanEliminate canElim, FSharpExpr queryProducingSequence) at Microsoft.FSharp.Linq.QueryModule.EvalNonNestedOuter(CanEliminate canElim, FSharpExpr tm) at Microsoft.FSharp.Linq.QueryModule.clo@1727-1.Microsoft-FSharp-Linq-ForwardDeclarations-IQueryMethods-Execute[a,b](FSharpExpr 1   q)在。$ FSI_0005.main @()

问题

该如何解决此异常?

1 个答案:

答案 0 :(得分:1)

问题是SQL查询从F#报价转换为SQL。这是一个非常敏感的操作,它仅适用于具有标准F#查询生成器生成的确切格式的查询。在您的情况下,构造的查询将调用新的Source方法,而转换仅识别基类的Source方法。

没有简单简单的方法可以解决此问题,但是您可以做一些技巧。您可以重新定义在创建的查询中调用的Run方法(F#引号),然后将引号转换为F#到SQL转换器可以理解的形式。

根据您的情况,您可以将<inst>.Source(<arg>)替换为Source(您的<inst>.Source(<arg>.Query)方法),并调用基类的Source方法。以下代码可以做到这一点:

open Microsoft.FSharp.Quotations

let rec replace expr = 
  match expr with 
  | Patterns.Call(Some inst, mi, [arg]) when mi.Name = "Source" -> 
      let sourceMethod =
        typeof<Linq.QueryBuilder>.GetMethods() 
        |> Seq.filter (fun mi -> 
             mi.Name = "Source" && 
             mi.GetParameters().[0].ParameterType.Name = "IQueryable`1")
        |> Seq.head
      let sourceMethod = 
        sourceMethod.MakeGenericMethod 
          [| typeof<DowntimeEntity>; typeof<System.Linq.IQueryable> |]
      let queryProp = typeof<T2>.GetProperty("Query")
      Expr.Call(inst, sourceMethod, [ Expr.PropertyGet(arg, queryProp) ])
  | ExprShape.ShapeCombination(o, args) -> 
      ExprShape.RebuildShapeCombination(o, List.map replace args)
  | ExprShape.ShapeLambda(a, b) -> Expr.Lambda(a, replace b)
  | ExprShape.ShapeVar(v) -> Expr.Var(v)

type QueryBuilder2() = 
    inherit Linq.QueryBuilder()
    member __.Source (source: T2) = base.Source(source.Query)
    member __.Run(e:Expr<Linq.QuerySource<'a, System.Linq.IQueryable>>) = 
        let e = Expr.Cast<Linq.QuerySource<'a, System.Linq.IQueryable>>(replace e.Raw)
        base.Run(e) 

我测试了该翻译是否有效,但尚未针对真正的SQL数据库运行此翻译。诀窍在于,在将repalce函数传递给SQL转换器的F#报价之前,在查询上调用了replace函数,并且在var g = new dagreD3.graphlib.Graph({compound:true}) .setGraph({}).setDefaultEdgeLabel(function() { return {}; }); g.setNode('step1', {label: 'Step 1', style: "fill:gray" }); g.setNode('step2', {label: 'Step 2', style: "fill:gray" }); g.setNode('step3', {label: 'Step 3', style: "fill:gray" }); g.setEdge('step1', 'step2'); g.setEdge('step2', 'step3'); var render = new dagreD3.render(); var svg = d3.select("svg").append("g"); render(d3.select("svg g"), g); // example node color change function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } sleep(2000).then(() => { // ????? another way to do this without calling render()? g.node('step1').style = "fill:blue"; render(d3.select("svg g"), g); }); 中模式匹配的第一种情况下,它执行了上面概述的替换。