【问题标题】:How can I implement batching in FSharp.Data.GraphQL?如何在 FSharp.Data.GraphQL 中实现批处理?
【发布时间】:2021-11-29 17:47:13
【问题描述】:

我正在使用 F# 和 .NET Core 构建一个 GraphQL 服务器。为了实现批处理和地址 N+1 选择,我正在构建一个数据加载器。 Facebook 的 dataloader 使用 Node.js event loop tick 来收集和分派批处理请求。

但是,这种机制在 .NET Core 中不可用。我知道我可以从可以手动调用的数据加载器实例中实现run/dispatch 方法。但是,在独立执行的解析器中很难做到这一点。所以我需要一些自动调度机制来运行批处理请求。

关于如何实现它有什么建议吗?

【问题讨论】:

  • 你问的不是很清楚——你试图通过解释你将如何在完全不同的技术中做到这一点来描述你想要什么。在像 .NET Core 这样的完全多线程环境中不需要大多数 Node 机制。您发布的不是what,而是不同的how。你想做什么?批处理是什么意思?批量发送多个请求?将多个操作合并为一个批次?执行批量操作(这绝对不是 GraphQL 的意义所在)?还是在页面中返回数据而不是一个长流?
  • 您使用的是哪个 GraphQL 库?这对可用或需要构建的内容的影响远远超过底层运行时。也许这个库已经实现了你想要的?
  • @PanagiotisKanavos,GraphQL.NET 的问题在于它不能很好地与我正在使用的 F# GraphQL 库配合使用。如果我使用别名调用它们,它也没有实现我可以在相同的顶级解析器中使用的 BatchingLoader。例如 - query { a: getPerson(123) { id, name } b: getPerson(456) { id, name, age }
  • @PanagiotisKanavos 由于上述查询会独立调用相同的解析器两次,因此我无法控制调用run/load 方法。调度应该自动发生。即使使用 GraphQL.NET DataLoader,我仍然必须手动调用它,这意味着它对上述类型的查询没有用处。它仅适用于嵌套的 N+1 问题解决方案。唯一可能的方法是使用某种自动批处理,尽管是乐观的。

标签: .net-core f# graphql


【解决方案1】:

这是一个很好的问题,我最近碰到了自己。

有一些用于 F# 和 .NET 的“数据加载器”类型库,但是如果您还使用 FSharp.Data.GraphQL,那么可以很好地集成的解决方案就更少了。

请注意,“Haxl”方法不适用于(轻松)FSharp.Data.GraphQL。这是因为 Haxl 类型必须集成到 GraphQL 查询模型中,但 FSharp.Data.GraphQL 只理解同步和 async

我能找到的最合适的实现是FSharp.Core.Extensions。这是一个相当新的库,但它的质量很高并且获得了 Apache 2.0 许可。

我确信有很多方法可以将它集成到 FSharp.Data.GraphQL 中,但是我更喜欢的方法是将数据加载器放入架构的根值中。这允许树下的所有 GraphQL 解析器访问它。

我认为最好的解释方式是举一个例子。

这里我们有一个“人”域,可以有零个或多个“追随者”,他们也是“人”。每个人都有一个全球唯一的 ID。人与人之间的追随者有很大的重叠,所以一个简单的解决方案可能会重复重新获取相同的数据。我们的数据库层可以在一个查询中获取多个人员记录,因此我们希望尽可能利用这一点。

您可以将此代码粘贴到.fsx 文件中并运行它。依赖项由 Paket 获取。

paket.dependencies

generate_load_scripts: true

source https://www.nuget.org/api/v2
source https://api.nuget.org/v3/index.json

storage: none
framework: net5.0, netstandard2.1

nuget FSharp.Core 5.0.0
nuget FSharp.Data.GraphQL.Server 1.0.7

github Horusiath/fsharp.core.extensions:0ff5753bb6f232e0ef3c446ddcc72345b74174ca

DataLoader.fsx

#load ".paket/load/net50/FSharp.Data.GraphQL.Server.fsx"

#load "paket-files/Horusiath/fsharp.core.extensions/src/FSharp.Core.Extensions/Prolog.fs"
#load "paket-files/Horusiath/fsharp.core.extensions/src/FSharp.Core.Extensions/AsyncExtensions.fs"

type Person =
  {
    ID : string
    Name : string
  }

// Mocks a real database access layer
module DB =

  // Used to avoid interleaving of printfn calls during async execution
  let private logger = MailboxProcessor.Start (fun inbox -> async {
    while true do
      let! message = inbox.Receive()

      printfn "DB: %s" message
  })

  let private log x =
    logger.Post(x)

  // Our data-set
  let private people =
    [
      { ID = "alice"; Name = "Alice" }, [ "bob"; "charlie"; "david"; "fred" ]
      { ID = "bob"; Name = "Bob" }, [ "charlie"; "david"; "emily" ]
      { ID = "charlie"; Name = "Charlie" }, [ "david" ]
      { ID = "david"; Name = "David" }, [ "emily"; "fred" ]
      { ID = "emily"; Name = "Emily" }, [ "fred" ]
      { ID = "fred"; Name = "Fred" }, []
    ]
    |> Seq.map (fun (p, fs) -> p.ID, (p, fs))
    |> Map.ofSeq

  let fetchPerson id =
    async {
      log $"fetchPerson {id}"

      match people |> Map.find id with
      | (x, _) -> return x
    }

  let fetchPersonBatch ids =
    async {
      let idsString = String.concat "; " ids
      log $"fetchPersonBatch [ {idsString} ]"

      return
        people
        |> Map.filter (fun k _ -> Set.contains k ids)
        |> Map.toSeq
        |> Seq.map (snd >> fst)
        |> Seq.toList
    }

  let fetchFollowers id =
    async {
      log $"fetchFollowers {id}"

      match people |> Map.tryFind id with
      | Some (_, followerIDs) -> return followerIDs
      | _ -> return []
    }





// GraphQL type definitions

open FSharp.Core
open FSharp.Data.GraphQL
open FSharp.Data.GraphQL.Types

#nowarn "40"

[<NoComparison>]
type Root =
  {
    FetchPerson : string -> Async<Person>
    FetchFollowers : string -> Async<string list>
  }

let rec personType =
  Define.Object(
    "Person",
    fun () -> [
      Define.Field("id", ID, fun ctx p -> p.ID)
      Define.Field("name", String, fun ctx p -> p.Name)
      Define.AsyncField("followers", ListOf personType, fun ctx p -> async {
        let root = ctx.Context.RootValue :?> Root

        let! followerIDs = root.FetchFollowers p.ID

        let! followers =
          followerIDs
          |> List.map root.FetchPerson
          |> Async.Parallel

        return Seq.toList followers
      })
    ])

let queryRoot = Define.Object("Query", [
  Define.AsyncField(
    "person",
    personType,
    "Fetches a person by ID",
    [
      Define.Input("id", ID)
    ],
    fun ctx root -> async {
      let id = ctx.Arg("id")

      return! root.FetchPerson id
    })
])

// Construct the schema once to cache it
let schema = Schema(queryRoot)




// Run an example query...
// Here we fetch the followers of the followers of the followers of `alice`
// This query offers many optimization opportunities to the data-loader

let query = """
  query Example {
    person(id: "alice") {
      id
      name
      followers {
        id
        name
        followers {
          id
          name
          followers {
            id
            name
          }
        }
      }
    }
  }
  """

let executor = Executor(schema)

async {
  // Construct a data-loader for fetch person requests
  let fetchPersonBatchFn (requests : Set<string>) =
    async {
      let! people =
        requests
        |> DB.fetchPersonBatch

      let responses =
        Seq.zip requests people
        |> Map.ofSeq

      return responses
    }

  let fetchPersonContext = DataLoader.context ()
  let fetchPersonLoader = DataLoader.create fetchPersonContext fetchPersonBatchFn

  // Construct a data-loader for fetch follower requests
  let fetchFollowersBatchFn (requests : Set<string>) =
    async {
      let! responses =
        requests
        |> Seq.map (fun id ->
          async {
            let! followerIDs = DB.fetchFollowers id

            return id, followerIDs
          })
        |> Async.Parallel

      return Map.ofSeq responses
    }

  let fetchFollowersContext = DataLoader.context ()
  let fetchFollowersLoader = 
    DataLoader.create fetchFollowersContext fetchFollowersBatchFn

  let root =
    {
      FetchPerson = fun id -> fetchPersonLoader.GetAsync(id)
      FetchFollowers = fun id -> fetchFollowersLoader.GetAsync(id)
    }

  // Uncomment this to see how sub-optimal the query is without the data-loader
  // let root =
  //   {
  //     FetchPerson = DB.fetchPerson
  //     FetchFollowers = DB.fetchFollowers
  //   }

  // See https://bartoszsypytkowski.com/data-loaders/
  do! Async.SwitchToContext fetchPersonContext
  do! Async.SwitchToContext fetchFollowersContext

  // Execute the query
  let! response = executor.AsyncExecute(query, root)

  printfn "%A" response
}
|> Async.RunSynchronously

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-09-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多