Federation Mutation Example
cheepo2109 opened this issue · comments
It's an awesome example for Federation!
I have 1 question on my mind, after trying it out.
How do I write resolver for federated mutations?
Let's say I want to create or update reviews and products. They are on 2 different services?
What would be the best approach?
It would be nice to have a mutation example in the demo :)
Thanks!
I think this is extremely important to demo. It's actually one of the main drawbacks that I'm seeing right now. In a monolithic GraphQL server, it's easy to perform complicated logic in a single mutation, but right now the only solution I can seem to find when using a federated graph is to push that logic onto the client, which IMO is not a good solution at all.
Federation seems like a great technology for querying data from multiple sources, but doesn't seem to hold up too well for anything more complex.
Would be really good to get some input from the Apollo team, although this repo seems stale and the only contributor looks like they're not a dev anymore judging from their GitHub status...
any suggestions on this?
Yeps would be great to have an explanation on how mutations can be accomplished when the federation schemas is being used.
While the story is well defined for queries, having the feeling that there is no good example - and maybe implementation - for mutations.
For example, regarding the reviews and product use cases, two separates schemas that got federated and allow the user to make optimal queries that touch both services with still providing a good way of isolating both areas. How for example the addition of a new review would need to be expressed as a mutation?
The naive approx would be adding to the reviews the schema the following mutation:
type Mutation {
addReview(productId: String, review: ReviewInput)
}
But the first question that you could ask for yourself is where the productId
should be really retrieved - and implicitly validated? With the current proposal would be the responsibility of the user of providing a valid id. Besides not being the most optimal thing - you would need two round trips, one for checking the existence of the product and another one for the mutation - you would be on the mercy of the caller, so if the caller would use invalid ids you would be populating invalid ids to the database.
I'm wondering if there is any way of moving that query to the Apollo GW, so Apollo GW would continue with the mutation if and only if the query for retrieving the production would not return an error.
bumping this. would really love an example of this as well
I was wondering the same thing. Best answer i could find was this: https://spectrum.chat/apollo/apollo-federation/mutations-for-federated-services~b45f6d65-2785-4e26-87ce-03ce23676fbf
and for a more in depth explanation: https://spectrum.chat/apollo/apollo-federation/graphql-microservices-what-is-the-accepted-approach-for-inter-service-communication~ffc5fb60-639f-4d11-af54-ea3fcb4a447b?m=MTU3NTk0Njk3NjIyNQ==
I understand that there is currently no real support in GraphQL Federation for mutations... and I know why: it's much harder (if not impossible) to find an as elegant generic solution as we have for queries, and still be fast enough.
The best option is probably to 'stich' your mutations, i.e. program the full orchestration yourself. It will be hard to balance between eventual consistency, retries, timeouts, two-phase commits, etc. A generic example would not help a lot, as there are too many use-case specific parameters that have to be taken into account.
Just my 2¢
This doesn't solve the whole issue of needing a more in-depth example, but there's another thread here with some info on mutations in Apollo Federation: apollographql/apollo-server#4194 (comment)
In case the link breaks later, here is the gist of the linked comment:
Yes, they [mutations] are supported. In the same/similar way that services/subgraphs should extend type Query to expose top-level entry-points into the API schema (that's exposed to the client), the subgraphs/services need to extend type Mutation with the fields they want to expose:
type Result { success: Boolean } extend type Mutation { review(date: String review: String): Result }
While technically mutations are supported exactly like queries, it's not explicitly mentioned in the spec, so all the interesting stuff is not specified: e.g. what happens if one of the backend services fails? Maybe it would be okay for an example where these things don't matter, but in real life, there has to be a solution that matches the business requirements. And as there is AFAIK no support to specify these details, it's generally not usable in the real world. I think it's better to just manually write an orchestrator.
Still it would be interesting to see how you can split the input data among the different services. E.g. how do I update one review and add a new one in the same request? How do I update one review to be about a different product? How do I update, e.g., the price of the product of two reviews if they refer to the same product?
I'm considering using graphql federation and this is my only issue with it, I feel like I need to rely on other microservice/event-driven architecture patterns to communicate between the microservices within the mutations to properly update the databases using some message queue or kafka via something like a Saga pattern, but ofcourse managing rollbacks and errors are much more complex than a normal setup, so its pushing me towards an event sourcing architecture with database replication/cqrs which is obviously quite complicated
Is there any update?
This concept is also block me of using federated graphql, so an example how to do this properly would benefit me greatly.
The benefits of a federated approach far outweigh the disadvantages. What would be the alternative? There's nothing else on the market (that I'm aware of) that offers what you presumably propose Federated Migrations would support. I'm sticking with this architecture and I'm confident it will mature.
Any new update or idea? 😥
Any new update or idea? 😥
unfortunately I don't think its a graphql or apollo problem, but a microservice architecture one, more specifically with database per service, the problem wouldn't exist with a shared database.
microservices.io has great explanation of these architecture patterns and their pros & cons https://microservices.io/patterns/data/database-per-service.html , microservices are difficult unfortunately and there's no easy solution, stay away from them unless you really need them. for smaller apps you could maybe rely on message queues and deal with eventual consistency.
unfortunately I don't think its a graphql or apollo problem, but a microservice architecture one, more specifically with database per service, the problem wouldn't exist with a shared database.
a single shared database is often not possible, or even an antipattern in some cases at this point. Think active directory.
That said, I'm giving my +1 for some kind of example. I need to ask another set of services that I don't control for input and at least validate the id for the database we do control.
Any updates here?
If you're interested in how Apollo Federation can handle cross-service mutations, check out this repository Apollo-Federation-Mutation-Demo. It provides a practical example of accessing external data in a seamless way.
If you're interested in how Apollo Federation can handle cross-service mutations, check out this repository Apollo-Federation-Mutation-Demo. It provides a practical example of accessing external data in a seamless way.
It's an approach but the whole point of querying a subgraph from another subgraph breaks the point of federation is not it?
The document Contributing computed entity fields describes how to query data from external subgraphs and contribute a new computed field to the entity.
I have adopted this approach to help ensure consistency in mutations, and I believe that this does not go against the principles of federation. @tummalah
I guess that with one InputType or the parent TypeReference in the subgraph could will resolve (I don't know if the spec enable this). I mean: imagine us that we have two uncouple microservices: User & Product (for this example I'll use Strawberry Python as a reference).
### User
@strawberry.federation.type(keys=["id"])
class UserType:
id: strawberry.ID
name: str
@classmethod
def resolve_reference(cls, **representation) -> "UserType":
id = strawberry.ID(representation["id"])
user = db.session.get(User, id)
return cls(id=user.id, name=user.name)
### Product
@strawberry.federation.type(keys=["id"])
class UserType:
id: strawberry.ID = strawberry.federation.field
@classmethod
def resolve_reference(cls, id: strawberry.ID):
return UserType(id)
@strawberry.federation.type(keys=["id"], description="Product Type definition")
class ProductType:
id: strawberry.ID
name: str
@strawberry.field
def created_by(self) -> typing.Optional[UserType]: ### Store the User.id value
return UserType(id=self.created_by)
@strawberry.type
class Mutation:
@strawberry.mutation
def product_create(self, name: str, created_by: UserType) -> ProductType:
product = Product(name=name, created_by=created_by.id) ### And be able to use the parent value here!
db.session.add(product)
db.session.commit()
return product
The problem with this approach now is that:
TypeError: Mutation fields cannot be resolved. Argument type must be a GraphQL input type.
Why with the UserType in the Product microservice? Because the value already resolve in the resolve reference in the User microservice.