How to GraphQL with Ruby, Rails, Active Record, and No N+1
evilmartians.com
evilmartians.com
I think if GraphQL had existed in the early days of Rails, the natural assumption would have been that ActiveRecord model classes should double as GraphQL result objects, and there would be a nice DSL for specifying how to safely expose those objects to the API. But I haven't seen anyone try to build that - maybe the feeling is that Rails is _complete_ so new responsibilities need to live somewhere else
GraphQL creates the illusion for the consumers that arbitrary queries are cheap, possible, and transparent. None of those things are true. Whether that reality is exposed to the client through more restricted REST endpoints or whether the backend has to support this fiction by handling all these performance considerations in GraphQL in the backend, the fact is that some queries are cheap, others are near impossible to achieve efficiently.
So GraphQL doesn't really _reduce_ complexity; it merely pushes it into other places. Whether the right place is the client or the server depends on your organization and the acceptable engineering tradeoffs, but there's no free lunch when it comes to data locality.
"But my mobile app makes too many requests!" says the hopeful engineer, "and graphql means we can optimise the network!"
Well, yeah, it probably looks like that when you see one nice request from the browser dev-tools, rather than dozens, but that's not what it will look like on the server side, or in the database, when you can get into situations with quadratic queries, never mind N+1. And yes, I've seen that with my own eyes.
If you're sending a dozen or so requests to the server to render a screen in a mobile app, then the slowest thing might just be the stage where you spin up a connection and do the old TLS dance. Unless you're hitting Big Tech scale that's probably acceptable versus the insane amount of money you'll dump into building and maintaining a GraphQL-based architecture without any data proving that this approach would actually be an optimisation.
Just like Kafka, Kubernetes, etc. which have come out of incredibly large-scale projects and businesses...you don't have to and shouldn't use this stuff just because it's the popular thing to do. Startups and small businesses became successful without any of this.
Usually, any disagreement is seen as either contrarian or ignorant, and you end up building an over engineered monstrosity because that's what they want.
As someone who is nominally a decision maker (in that my job title has "architect" in it), I have the opposite experience, I guess: most of the pushes for GraphQL have come from devs desperate to have a magic bullet, rather than to rework an overly-granular API.
(Of course there are also the folks who want a tick on their CV but that's a whole other problem)
If you're building a single page app frontend where most of the business logic is on the client side, it makes sense to empower the client to define the types of queries it wants to make.
If anything, running a GraphQL architecture seems like less maintenance burden to me, because it makes the server side API less rigid, so the coupling between client and server becomes looser.
Beside that, I disagree in principle because, like the OP said, it's just hiding the maintenance burden elsewhere. It doesn't go away.
I’ve dealt with garbage REST APIs with tons of sequential calls and in low reliability scenarios 1 call is a massive improvement over a scenario where 4-5 sequential calls need to be made and if/when one of them fails you need to start over.
Now the client has to handle result caching and all the different error states because some server dev couldn't be bothered to batch a few calls and return all the required models.
If you're serving a public API and you really don't know up front what endpoints people will need, or like facebook you have tons of people with different needs, then maybe GraphQL makes sense.
As a rule of thumb I would use a GraphQL API if I (as a server developer) don't know my clients and their API access patterns and do not want them to assemble what they want on their own in n+1 requests, particularly if they are somewhere in the internet and would suffer from network latency in each request. Likewise, I (as a database developer) would prefer a SQL database if its clients (e.g. independently developed inhouse applications) and their access patterns are unknown.
Conversely, as, e.g., a backend microservice developer, you mostly know the access patterns between a database and its clients, mostly because there there is just one database client per database and both constitute the microservice. So you are safe to choose a NoSQL database here with, say, a document-oriented datamodel that is optimized for that single database client. With regard to the API of that microservice, you also know your clients (i.e. other microservices), so you can also optimize a RESTful (or even GRPC) API for them in order to avoid the over-/underfetching/n+1 problem. Even if you cannot optimize your internal RESTful API to satisfy each of your clients' requests in a single response, network latency is not such a big problem here because clients of a back-end microservice are often somewhat close to it.
This is simply recognizing the fact that many serialization patterns do not work in a black box. They inherently rely a coordination with SQL queries to load necessary data.
And IMO this is good. Now you have dedicated place (namely `app/graphql` folder) for API-specific data mapping and validations. And in general GraphQL-Ruby gently pushes you to decouple your API from your database schema so you will be able to refactor and optimize things under your application hood later.
GraphQL-Ruby Types _is_ that DSL. You specify what fields can be exposed to the world and this is also the place where you can hide too low-level specifics (e.g. join separate `amount` and `price` columns into specific `Money` type). Then you pass AR object to GraphQL-ruby and it does what you said in DSL.
These tools don't require explicit `includes`or `preload` calls. They can infer the tables that need to be eager loaded through usage (e.g. if you iterate through users and every iteration requests the user's posts, the first iteration will eager load all posts for all those users). This fits really well with GraphQL because different clients might trigger requests for different associations. Automatic eager loading allows you to optimize for all those different cases without writing a single line of code.
The other solutions are either way too explicit or might be eager loading too much. Things like graphql-batch don't even feel like ActiveRecord any more (and for a good reason - it's a different kind of abstraction, meant to batch all sorts of things, including HTTP requests).
There are more caveats when it comes to eager loading, mainly there are many things that can break eager loading. I recently wrote a blog post [3] about dealing with those issues.
[1] https://github.com/salsify/goldiloader
[2] https://sequel.jeremyevans.net/rdoc-plugins/classes/Sequel/P...
We‘re using Goldiloader in a medium sized Rails application with zero problems so far.
I have to say though, it's extremely un-railsy and the N+1 problem is hard to solve without creating heaps of extra loader types. The overarching issue is that from the front end perspective all queries are equal, where in fact some are much more expensive than others. I end up spending most of my time optimising the query resolvers to perform well for the set of queries the front end actually makes as it's not feasible to optimise for all possible outcomes.
The front end development experience was made marginally better, but it created far more problems than it was worth. It's harder to debug, it's harder to isolate performance problems, and the Rails documentation and battle-tested experience (not just todo apps) detailed in blogs and on stackoverflow is thin.
Unless you have a really good reason I would steer clear for now. Better to spend your time solving business problems than wrangling an immature framework.
The main idea was to feed the Active Record preloader with hints from the GraphQL queries.
Of course this only works well when your GraphQL schema matches your database schema (which is most cases anyway in my experience).
https://github.com/nettofarah/graphql-query-resolver
Here's a video of a talk I gave about the approach at GraphQL Europe in 2017:
Which is interesting, because every other comment here is saying it shouldn't.
I built this tool to help with existing codebases. At IFTTT specifically, we saw an instant 60% reduction in database IOPS. Not too bad for 1~2 days of coding.
The ruby graphql ecosystem is also lacking for decent loaders, but such a loader would likely not integrate with ActiveRecord.
Also, remember, GraphQL is a language, you're responsible for locating the data on the backend.
GraphQL provides one-way (server-bound) run-time type assertions.
I constantly see developers that should know better confuse validation with type checking. I've had developers, in earnest, ask how they can use TypeScript on run-time data inputs. I have to break the bad news to them and simply tell them: no, you can't do that. That's not how type systems work.
Out of the box, GraphQL provides no validation, that I know of, beyond the elementary data types. Anyone that knows a thing about XSS or SQL-injection can tell you that a string is not always just a string. Anyone using C/C++ that has dealt with stack overflows and code injection can tell you that an integer is not always an integer. Sometimes it's a pointer. Oops.
You're going to hand code validations, each and every time. Because no tool knows how many characters your DB is setup to handle for a username, for example. GraphQL and TypeScript won't save you.
GraphQL tooling cannot literally prevent JSON from having a string value where there's supposed to be a number value, but it can ensure that application code never has runtime type errors. Is that "validation rather than type checking"? Sure, I suppose so, but my application code that consumes GraphQL responses will either get some representation of an error, or a representation of a successful response that is guaranteed to have the types I expect.
And yes, there are plenty of other data formats and tools that provide the same sort of thing: some representation of a schema, some non-application code that checks runtime data against that schema, and a contract with the application code that each response will be either an error or a type-safe successful response.
It's not a replacement for SQL. It's a replacement for REST. And the intent is to provide a statically typed, holistic interface and encourage best practices in API design. It's also quite nice because there are clients that plug into it and offer substantial benefits as a result, like Relay.
There are lots of ways people write resolution methods for GraphQL, and this article provides a good number of them. You can think of them as query optimization approaches if it helps. But it's not quite like SQL, where in SQL you say whatever you like and it just gets figured out. GraphQL forces an amount of intention and asks schema writers to consider the most concrete use cases possible rather than just enabling super generic ones.
GraphQL lets you write queries for objects. This fundamentally changes how you architect your app and database.
In a REST API, you turn known a finite list of known query patterns (endpoints) into DB queries, which you can test, hand-write, and optimize.
If you let end users query your object model, it takes far more work to ensure they don't do something very expensive.
But, even should you have to support a complex schema, the fine article showcases a number of great mitigations that cover basically every possible issue.
The only issue that I don't think is covered here is that collecting all this data up and sending it all at once can sometimes be slow or even time out, and there's no mechanism really to allow GraphQL to defer the collection of some fields until they're ready. It's coming very soon (in the form of @defer; to the spec, to graphql-js, to Relay, and to others) but it's not quite here yet.
The article assumes simple schemas as well - a parent with many child relationships in this article could lead to some complex resolvers. Unless I misunderstand the article many of the strategies here are in ORMs (e.g. lazy subselects) which often don't perform as well as a crafted query given the still multiple roundtrip's to the db. They also require more code, and exhibit branching which means that performance is a function of the users query. If you know your user that's fine, but then again I don't see the advantage vs a straight REST or RPC call which would result in simpler code anyhow. It seems to work IMO in cases I guess when service isn't critical, your model/schema is simple, or you prefer flexibility of data access over reliability/predictability.
How so? There are existing tools that prevent both N+1 problems (Dataloader) and complex, recursive queries (depth and complexity limits).
Seems like it postpones premature optimisation... which is a good thing in my opinion
If your GraphQL schema requires expensive queries because it has a different schema than your database, is it really decoupled?
Queries over GraphQL often wind up being handled by custom resolvers and being about the same as remote procedure calls. It is kind of neat how it separates the queries from the mutations. Blitz does that, but with GraphQL being optional:
https://blitzjs.com/docs/query-resolvers
gRPC just provides an efficient transport and has the client library provide the interface. It works out pretty well for google APIs.
Would love to hear what's been missing / painful and factor that into the roadmap.
It's much easier to evolve from a very restrictive schema to a more open schema as the use-cases present themselves, than it is to move clients from an open schema to a more restrictive one because your backend is choking trying to support the cardinality of edge cases introduced by it.
Which is to say, a great user experience comes from the heart, not the skin, of your product.
My first recommendation in any design process is, assume that Conway's law will define your interfaces.
The FE team loves that they can easily look up what exactly the BE returns and include tools in their build pipeline to validate the queries they write against our production schema. Combined with TS then checking that you use the returned objects correctly this is a major win.
We actually rarely go an optimise on field level because often fetching unnecessary objects from the database is faster than creating two queries that 95% of the time will both be called (for example adding a join to the main query instead of fetching additional fields by id in a different resolver). However, if the FE really doesn't need the additonal data then at least it isn't sent over the network and the FE doesn't have to parse it from JSON.
https://github.com/ConsultingMD/graphql-preload
It is very sad that its creators are left and current owners aren't responding.
There's no reason to use Rails for that (or, well, use it for anything at all, given the terrible language it's based on and its outdated architecture).
In principle, I love the idea of Postgraphile but this is what turned us off.
There are definite downsides versus eg REST (notably performance, which becomes harder to reason about), but it’s an acceptable trade-off for us.
I’m also optimistic about the great tooling that is improving all the time - eg Hasura, Postgraphile, Graphene-SQLAlchemy all solve N+1 today.
If you are eagerloading multiple joins relationships, normally that would create a result set of n x m x k x ... rows. With json_agg each relation could be single column in a row etc.
Graph databases + simple API > GraphQL
Practically every important scenario uses related data. Graph databases are awesome at that.