tfoxy/graphene-django-optimizer

Support for mutations

wodCZ opened this issue · 2 comments

wodCZ commented

Hi,
we're successfully using the optimizer for queries and it works perfectly.

But now I've noticed that mutation result is not optimized, which generates tons of sql queries.

I've tried tricking the resolver by calling MyNode.get_optimized_node directly, but without success:

class MyMutation:
    #...

    my_node = graphene.Field(nodes.MyNode)
    
    @classmethod
    def mutate_and_get_payload(cls, root, info, **kwargs):
        # ...
        return MyMutation(my_node=nodes.MyNode.get_optimized_node(info, nodes.MyNode._meta.model.objects, my_id))

Result is returned properly, but without optimization.

I've noticed a mention about mutations here: #18, but it didn't lead me anywhere.

Is there some workaround?

Thank you!

@wodCZ this is somewhat related to your problem so I thought I'd share to see if it helps (tl;dr at the bottom):

I've been implementing subscriptions using django-channels-graphql-ws which uses a class very similar to graphene's Mutation type, and I was having an issue where the optimizer wouldn't optimize my query at all. My subscription class looked very similar to your mutation:

class MySubscription(channels_graphql_ws.Subscription):
	# ...

	my_node = graphene.Field(nodes.MyNode)

	@staticmethod
	def publish(payload, info, **kwargs):
		return MySubscription(my_node=gql_optimizer.query(MyNode.objects.filter(**kwargs).first(), info))

After some digging I found that, under the hood, the problem boils down to a mismatch between the info object (an instance of ResolveInfo) and the QuerySet being optimized. It uses the info object as a starting point and tries to get the _meta.model of the current field and optimize from there. It can't do that, because the current field is the result of MyMutation.Field(), which by default is MyMutation, which doesn't have a backing model for the optimizer to act on.

The solution (or so I thought) was to find a way to make the optimizer "think" that it was optimizing the my_node field, and not the field produced by MyMutation.Field().

What I did originally was a bit of a hack. I basically copied and tweaked the info object before optimizing so that the optimizer would "think" it was operating at the level of my my_node field:

class MySubscription(channels_graphql_ws.Subscription):
	# ...

	my_node = graphene.Field(nodes.MyNode)

	@staticmethod
	def publish(payload, info, **kwargs):
        # In order for the query optimizer to function as expected, we must adjust the ResolverInfo
        # object such that the root of the resolver is the `my_node` field. Effectively changing
        # this `publish` method into a resolver for the `my_node` field.
        new_info = copy.copy(info)
        new_info.field_name = 'my_node'
        new_info.return_type = info.schema.get_graphql_type(MyNode)
        new_info.parent_type = info.schema.get_graphql_type(MySubscription)
        # The selected fields should be whatever subfields of `my_node` were selected in the query.
        new_info.field_asts = info.field_asts[0].selection_set.selection

		return MySubscription(my_node=gql_optimizer.query(MyNode.objects.filter(**kwargs).first(), info))

This was a fairly unsatisfying -- albeit working -- hack.

Later, as I was looking at the source for mutations, I found that you can specify a node type for Meta.output that points to a model-backed ObjectType, and Mutation.Field() will use it instead of generating its own type (which doesn't have a backing model and breaks the optimizer).

This allows the optimizer to match up the current field type (a MyNode) with its backing model and start optimizing from there.

tl;dr Try this:

import graphene_django_optimizer as gql_optimizer

class MyNode(gql_optimizer.OptimizedDjangoObjectType):
	class Meta:
		model = MyNodeModel
	# ...

class MyMutation:
    #...
	class Meta:
		# Tell MyMutation.Field() that it returns a `MyNode` instead of a 
        # `MyMutation` with a set of fields.
		output = MyNode
    
    @classmethod
    def mutate_and_get_payload(cls, root, info, **kwargs):
        # ...
        return gql_optimizer.query(MyNode._meta.model.objects.filter(pk=my_id), info)
wodCZ commented

Hi @eabruzzese, thank you for extensive response!

I've tried a quick test of the tl;dr solution, and I was immediately stopped by this assert on relay mutation.
Also I'm afraid it would require a lot of changes both on backend and frontend, as the return type changes significantly (we oftentimes have multiple output attributes, not just one node). I'll try when I come around non-relay mutation to check the approach.

Somehow the mutation I'm testing is now optimized without any extra code 🤔 I'll have to further investigate what's the difference between our two similar projects.

Maybe it's already magically resolved 🎉

I'll keep this issue open and I'll come back with what I've found.