12 ms·
How to GraphQL with Ruby, Rails, Active Record, and No N+1
- nettofarah 6y agoI built something slightly different when I was working at IFTTT. 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 https://github.com/nettofarah/graphql-query-resolver Here's a video of a talk I gave about the approach at GraphQL Europe in 2017: https://www.youtube.com/watch?v=TIzEZJuDpIQ https://www.youtube.com/watch?v=TIzEZJuDpIQ
- adwww 6y ago> which is most cases anyway in my experience Which is interesting, because every other comment here is saying it shouldn't.
- nettofarah 6y agoI agree with you. Not many things in life are done as they were supposed to be done. Hindsight is 20/20. 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.
- thom 6y agoUnless GraphQL implementations include some sort of query optimiser, it's very hard to see how they actually do anything, if you're left to constantly supervise the back end in this way.
- raspberry_fool 6y agoIn general I would not recommend bolting on GraphQL to an existing model unless you're willing to invest in either time to build out the backend query optimization or tolerance of suboptimal querying. 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.
- tshaddox 6y agoThe primary “thing GraphQL does” is provide a data shape contract between servers and clients, with type safety, introspection, and a fairly human-friendly query syntax.
- jamil7 6y agoI don’t disagree but can’t you kinda get a lot of this with swagger / openapi aswell?
- deckard1 6y ago> with type safety 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.
- tshaddox 6y agoI understand the distinction you're making, but I think it's fuzzier than you seem to think. Yes, GraphQL responses tend to go over the wire in a format without any type checking (like JSON). But GraphQL tooling and libraries can use GraphQL schemas to ensure that application code that consumes GraphQL responses will never see a value with a type that contradicts the GraphQL schema. 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.
- cactus2093 6y agoBuilding a Firebase-like direct frontend access to your database tables is far from the only use case of GraphQL. Many (probably most) apps inevitably need backend logic that does more than just pass through and format data for the frontend. GraphQL can still have a lot of other benefits in terms of enforcing data structure and eliminating other types of boilerplate.
- striking 6y agoI really wish GraphQL weren't named in a way that seems to cause confusion about what it's for and its relation to SQL. 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.
- jtmarmon 6y agoI don't think GP is confused. 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.
- Kaze404 6y ago> If you let end users query your object model, it takes far more work to ensure they don't do something very expensive. How so? There are existing tools that prevent both N+1 problems (Dataloader) and complex, recursive queries (depth and complexity limits).
- striking 6y agoThe amount of work it might take is generally proportional to the amount of flexibility you give the user. You don't have to (and in fact are generally discouraged to) offer end users a degree of flexibility that makes your life harder. To be especially clear about this: do not mirror your DB schema in your GQL schema; it's not worth it. 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.
- benatkin 6y agoThe discoverability and querying part of GraphQL seems to be a net loss, and the RPC part seems to be a net win. I think REST is going to lose some a big chunk of its popularity, but that it will be to gRPC, not GraphQL.
- valenterry 6y agoI found both the discoverability and querying in GraphQL much better compared to Rest. Can you explain what you mean in more detail?
- benatkin 6y agoDiscoverability with GraphiQL and Intellisense aims to make it self-documenting. It doesn't work out that well and you wind up needing documentation anyway. 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 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.
- valenterry 6y agoI agree that more comprehensive documentation is often necessary. But still, having GraphiQL that let's me discover all possible queries with their types and a small explanation for each field is a great win. I like that much better than for example generated swagger documentation. So how exactly is this a net loss? If anything it does not improve the situation - but making it worse?
- bdcravens 6y agoMy understanding is that Hasura more or less "compiles" SQL, rather than chaining models together like Rails does.
- thom 6y agoYeah, we use Hasura for some endpoints and it's great for rapid development, but still has a lot of sharp (or indeed dull) edges.
- tango12 6y agoHey! I'm from Hasura. Would love to hear what's been missing / painful and factor that into the roadmap.
- thom 6y agoA lot of random issues, some I think already fixed, like migrations being run out of order. Remotes being hit even for null foreign keys kills off some queries for us. Remotes not being supported in subscriptions (sort of understandable but still causes inconsistencies in user experience). We've had to be very strict with naming conventions for relationships etc or we find we're constantly dealing with merge conflicts, but that's as much our workflow issue. There's poor support for Postgres enums. That's the stuff I can remember off the top of my head, and none of it's really a killer. Without something like Hasura my interest in GraphQL drops close to zero, to be clear, so it's providing a lot of value.
- ilikehurdles 6y agoWhile it may be tempting to do so, don't create wide open "flexible" GraphQL queries that you don't know the use cases of yet, supporting arbitrarily deep levels of nesting, aggregation, and parameterization. That's how you paint yourself into a corner, fast. 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.
- dfee 6y agoThat’s kinda hard not to support. The second you have a relation modeled bidirectionally in GraphQL, you support infinite nesting (well, there are complexity guards, but alas).
- gsvclass 6y agoAnother way is to run the Super Graph servie alongside your Rails app. Super Graph can decode and use Rails cookies and it's in Go so very fast and lightweight. Super Graph automagically converts GraphQL queries into a single efficient SQL query. https://github.com/dosco/super-graph https://github.com/dosco/super-graph
- jes5199 6y agohuh. "Open your SQL database to the world" is the thing that Rails has carefully avoided... not immediately obvious to me how this tool deals with, like, security. or any kind of data hiding.
- Envek 6y agoThere is also abandoned graphql-preload gem with absolutely amazing API (it wraps graphql-batch under the hood): https://github.com/ConsultingMD/graphql-preload https://github.com/ConsultingMD/graphql-preload It is very sad that its creators are left and current owners aren't responding.
- inopinatus 6y agoCrystallising architecture around the front-end is one of those anti-patterns my grandmother warned me about.
- a-priori 6y agoWhy not? The front end is closer to your users. Shouldn't that be the focus?
- inopinatus 6y agoAnalogies about proximity are beguiling but misleading. They’re all hat and no cattle. Streamlining the ticket office won't make your trains run on time, and you can’t just put more lipstick on a dead pig, since ultimately it lets the tail wag the dog. Which is to say, a great user experience comes from the heart, not the skin, of your product.
- zionic 6y agoAs opposed to REST which, in practice, seemingly defines the network interface by what was convenient to the database layout?
- inopinatus 6y agoThe naive bijection of REST to table CRUD is both commonplace, and deeply flawed. Fortunately, databases do not have preferences; people do. That's one illustration of why I've found oppositional mindsets unhelpful to design processes. Heck, it's why they executed Socrates. My first recommendation in any design process is, assume that Conway's law will define your interfaces.
- mrpickels 6y agowhy would somebody use a chinese version of python?
- bdcravens 6y agoIf you're gonna go that direction, at least get the geography right. Ruby is Japanese, not Chinese.
- jes5199 6y agoGraphQL is so much heavier, code wise, than REST for Rails apps that it feels like a bad fit to me. Notice that these examples have created a completely parallel schema - new classes for every object in the GraphQL graph, which are analogous but separate from the Rails models, each one acting like a combination of controller and presenter for its Rails model, and breaking the ORM encapsulation to write efficient SQL queries. 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
- Daishiman 6y agoThe problem is GraphQL, fundamentally. 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.
- ljm 6y agoGraphQL, from what I see, is a solution designed by tech that is still best meant for big tech. Facebook had their reasons for coming up with it, because their user base is hitting the billions in scale. "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.
- bdcravens 6y agoHas anyone gone the route of leaving GraphQL out of Rails, and using a dedicated GraphQL server like Hasura?
- hirundo 6y agoWe looked at that, but with a large Rails app that already wraps the db in a ton of business logic, we needed to wrap the Rails app in GraphQL, not the db. To start with some other server would mean either recreating that business logic, or creating yet another API to wrap the Rails app. So we went with graphql-ruby and it's doing what we hoped.
- deleted 6y ago[deleted]
- BenGosub 6y agoEdgeDB [1] is an opinionated database and language built on top of Postgres that solves the N+1 problem. Using a framework on top of that is a stack that I definitely want to give a try for a simple project. A lot can be done within edgedb, the language offers powerful functions and expressive types. [1] https://www.edgedb.com/ https://www.edgedb.com/
- lipanski 6y agoIn my experience, automatic eager loading is the most elegant solution to this problem. For ActiveRecord, I'd recommend Goliloader [1] (which is a gem similar to ar_lazy_preload but with emphasis on doing that automatically) while for Sequel, you can enable the TacticalEagerLoading plugin [2]. 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 https://github.com/salsify/goldiloader [2] https://sequel.jeremyevans.net/rdoc-plugins/classes/Sequel/Plugins/TacticalEagerLoading.html https://sequel.jeremyevans.net/rdoc-plugins/classes/Sequel/P... [3] https://lipanski.com/posts/activerecord-eager-loading https://lipanski.com/posts/activerecord-eager-loading
- augstein 6y agoCame here to also recommend Goldiloader, but your comment and especially your blogpost summarizes it just fine. We‘re using Goldiloader in a medium sized Rails application with zero problems so far.
- dmitrytsepelev 6y agoGreat post! By the way, the reason why ar_lazy_preload exists is that neither me nor anyone else around me heard about goldiloader at the moment when ar_lazy_preload was created
- tehlike 6y agoPostgres supports json_agg which solves n+1 and also cartesian product problem. It's neat, hasura does it nicely for example.
- advance512 6y agoHow does json_agg solve the n+1 problem?
- tehlike 6y agoIt can eagerload more complex queries. 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.
- dmitrytsepelev 6y agoIn this case the row size can become huge. Also, how to make sure it's consistent? (I mean foreign keys/unique indexes/etc)
- tehlike 6y agoi was mostly talking about reading side, not mutation side.
- bot41 6y agoI don't get the need for GraphQL
- davidkell 6y agoIn my experience, GraphQL enables smaller, less experienced teams to build complex web apps faster. (As long as you use decent frameworks and tooling.) 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.
- devit 6y agoThe proper solution is using well-designed GraphQL-to-PostgreSQL software like Hasura or PostGraphile. 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).
- davidkell 6y agoHow do you feel about using SQL for everything, eg using Postgres policies for RBAC, or PL/pgSQL to generate passwords? In principle, I love the idea of Postgraphile but this is what turned us off.
- hiharryhere 6y agoI can speak from experience having built and supported a complex commercial React + Rails GraphQL app over the past year or so. The Evil Martians blog posts were really helpful in getting started. 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.
- joshspankit 6y agoOk, hear me out: Graph databases + simple API > GraphQL Practically every important scenario uses related data. Graph databases are awesome at that.
- Ozzie_osman 6y agoThe Python Graphql library Graphene-Django includes built-in support for dataloaders, and you can just tell it how to load a list of objects, given their id's (by default, use the foreign key id as the primary key). Works beautifully.
- yeswecatan 6y agoI haven't had a chance to dig into dataloaders much. Is the basic idea you perform one query per "layer?" Maybe not as great as just using something like A.objects.prefetch_related(related_objects__other_object), but better than N+1
- Ozzie_osman 6y ago(Sorry for the delay here) At face value, it can issue one more query per layer, but you get a lot of other benefits by breaking down your logic in this way. For example, you're less inclined to use database joins that may not scale as you grow. Another benefit is that you can very easily switch storage layers. For instance, maybe you fetch one layer from your SQL database, and another from a KV store database. Or maybe one layer comes from the cache, etc.
- valzam 6y agoWe use GraphQL heavily to power web apps and chrome extensions. To me the biggest benefit, as others have mentioned in this thread, is the strong contract it provides between FE and BE. We have a mix of CRUDy queries and queries tailored specificaly for certain pages. 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.
- gadys 6y agoGet most of the benefits of graphql while still using rest: https://www.graphiti.dev/ https://www.graphiti.dev/
- digitaltrees 6y agoso much this.