3 ms·
You can use regular functions, but there are several things you lose: - intermediate keys are not recalculated if they're used across different resolvers. This
by geokon 3mo ago
You can use regular functions, but there are several things you lose:
- intermediate keys are not recalculated if they're used across different resolvers. This means you basically never need to manage caches of precomputed results. So if you're calling `my-func` on `input-a` everywhere, you don't need to do all the ceremony of computing it once, storing it somewhere, and then passing it around to everyone that needs it. It's all just handled automatically. Code simplifies greatly
- It's much easier to "inject" lower-level steps b/c resolvers are essentially declaring an interface. If you suddenly don't like your interface and want a new interface, then you make a new interface and a bridging resolver. Refactoring is much easier. If you want to introduce an entirely new input format that usually just involves adding a single new resolver that outputs the inputs to your system (at whatever part of the pipeline you want). While with a pipeline of function calls it's generally more messy. It hard to make a generalization here b/c it depends on how your functions are organized.
- With the async engine you can automatically resolve branches concurrently without having to manage or think about it. You get a lot less stalls in the code.
I haven't really hit an "exploding their fetches" scenario personally. Things like optional inputs and resolvers that rely on precedence rules are generally a bit of a code smell and are usually points where I start to think about how to reorganize my code
- Capricorn2481 3mo ago> intermediate keys are not recalculated if they're used across different resolvers. This means you basically never need to manage caches of precomputed results. So if you're calling `my-func` on `input-a` everywhere, you don't need to do all the ceremony of computing it once, storing it somewhere, and then passing it around to everyone that needs it. It's all just handled automatically. Code simplifies greatly I think what I'm suggesting is, if you can, avoiding intermediate keys at all can be helpful for performance, and graph querying encourages people to break their queries into small, atomic units that can fire in any order. Which is good for composability, but you don't want to run multiple queries if you don't have to. Using functions encourages writing what happens explicitly. I'm coming at that not as someone who uses graph querying a lot, but someone who has worked on code bases where a single api call was dozens of DB calls. If people do that with regular function convenience, I imagine it happens even more often with libraries like Pathom, but that is speculation. > It's much easier to "inject" lower-level steps b/c resolvers are essentially declaring an interface. If you suddenly don't like your interface and want a new interface, then you make a new interface and a bridging resolver Is that exceptionally harder to do with regular functions though? I feel the same way about this as I do above. It sounds like this is only useful when your data topology is unknown and you can't be sure of the access pattern to begin with. I'm used to codebases where access patterns need to be documented and flexibility is not a huge concern. We just have repository interfaces, and we substitute the ones that we need.
- geokon 3mo agoI'll be honest, I work in a very different area (scientific computing) so my code is a lot more exploratory and I don't ever deal with DB access for instance. If you have a very stable interface and clear objectives then coupling isn't really a concern b/c there won't be anything to refactor and extend. > avoiding intermediate keys at all can be helpful for performance, and graph querying encourages people to break their queries into small, atomic units that can fire in any order EDIT: Reading the other comments, I realize here query is a DB query and not a EQL query.. so nevermind :) I'm a bit fuzzy on what you're saying, but I think you may be misunderstanding an aspect (I could be wrong here). You typically have one complex top-level query and the engine builds the sequence/graph of resolvers that need to be run to derive the requested query. In that graph key values can be reused and branches can be run in parallel. You don't run a series of small queries and manually build up anything. In my limited experience the order in which the resolvers are run is pretty clear (unless they're independent branches of the graph being run concurrently) and if you have a non-branching pipeline there isn't really any incentive to break it up. From a performance perspective I'm guessing you mean in terms of DB access? Because calling a series of functions or a series of resolvers is going to be quite similar performance wise - though you have some engine and destructuring overhead (can be significant in tight loop situations). > Is that exceptionally harder to do with regular functions though? It's hard to make a general statement here b/c it really depends on how you've set up your functions. But yes, generally if you are just playing with functions it can be harder to plug in a different "backend" or step in the middle unless you've somehow planned for it. Is it very hard? Generally not super difficult - but you generally need to refactor code to make it happen and explicitly handle the branching logic - so the code usually gets uglier If you want to do a mock or try injecting some step, with resolvers you can do that without touching your code
- Capricorn2481 3mo ago> Reading the other comments, I realize here query is a DB query and not a EQL query.. so nevermind :) I have been using them interchangeably and it's confusing. I am talking about how differing queries to the Pathom environment may trigger different resolvers, but the resolvers themselves also have DB queries. In Pathom, as far as I have seen, their query planner will try to fulfill the requested keys with the least amount of resolvers. That means if you have the following resolvers, each their own DB query - GetEmployee - GetCompany - GetEmployeesAndTheirCompanies Then querying Pathom for a users general information + their company data should only trigger the third resolver, preventing a redundant DB fetch from happening. So as your schema evolves and new entities emerge, when you find your routes do not have optimal resolvers, you can try to make a new one that fulfills a previously unexpected combination of keys. But that, to me, feels like undoing the things that make graphs appealing. Instead of just querying whatever you want, you now have to remember if the resolvers you've written up to this point can meet the query efficiently, or if it's an non-optimal combination of resolvers. That feels kinda leaky to me, and I'd rather just explicitly code the flow for each route than write Pathom queries and hope the key combination is performant enough. Caching absolutely helps, but doesn't eliminate this. I also don't like that your only tool to guide which resolver is chosen is just priority. You don't know which two resolvers might be competing against each other, so giving any one resolver a single number for its priority feels very wrong. I can't guarantee how it shakes out without tracing the wrong code. When I am writing explicit procedural code, I do have to write a lot more, but I can just go to definition and see where everything is being used. I do think this would be less important if your resolvers aren't particularly expensive, or if they are in memory DB calls. > It's hard to make a general statement here b/c it really depends on how you've set up your functions. But yes, generally if you are just playing with functions it can be harder to plug in a different "backend" or step in the middle unless you've somehow planned for it. Is it very hard? Generally not super difficult - but you generally need to refactor code to make it happen and explicitly handle the branching logic - so the code usually gets uglier I think I see what you're saying. You compared resolvers to an interface. I would use an interface in procedural code, like for a repository that gets users, and I can plug in a different implementation if I want to replace it. But if I wanted to change the interface itself, that would require refactoring code. You're saying this would be as simple as making the resolver in Pathom, and it can plug in anywhere now.