In my specific case, I'm dealing with a stream of activities. There are some common fields that all activities have, like a timestamp but then there are fields specific to activities of a particular kind. For example a BookingActivity will have a relation to a booking object whereas TaskActivity will have a relation to a user (assignee). Sometimes I'm interested in looking at only BookingActivities or only at TaskActivities but sometimes I'm also interested in looking at all activities across types.
In Django, to achieve the above, usually you create a single model with a field that denotes the 'type' (an enum or something) and a bunch of nullable fields and then have various proxy classes that expose properties/methods that make sense to that particular kind of entity. That's exactly what I'm doing.
So how do I expose that in graphql? I was able to get pretty far by utilizing is_of_type. Take a look:
ActivityType = strawberry.enum(models.ActivityType)
@strawberry_django.interface(models.Activity)
class Activity(strawberry.relay.Node):
created_by: User
type: ActivityType
timestamp: datetime.datetime
@strawberry_django.type(models.Activity)
class BookingActivity(Activity):
booking: models.Booking
@classmethod
def is_type_of(cls, obj: Any, _) -> bool:
return isinstance(obj, models.Activity) and getattr(obj, "type") == 0
@strawberry_django.type(models.Activity)
class TaskActivity(Activity):
task: models.Task
@classmethod
def is_type_of(cls, obj: Any, _) -> bool:
return isinstance(obj, models.Activity) and getattr(obj, "type") == 1
Cool, now I can expose the connection field:
@strawberry_django.connection(
strawberry_django.relay.ListConnectionWithTotalCount[models.Activity]
)
def all_activities(self, info) -> list[Activity]:
# Can add filters/ordering to this field as needed
base_qs = floatist_models.Activity.objects.all()
return cast(list[Activity], base_qs)
The above mostly works. I can query the allActivities field and switch on the type and select appropriate subset of fields. I can also add ordering/filtering if desired.
The problem is that the optimizer doesn't seem to know what to do. There are 2 primary problems:
I need the field 'type' from Activity to determine the actual node type. If I do not select 'type' in my query, the optimizer will cause that field not to be retrieved and thus will lead to N+1 on every single row.
Optimizer only knows how to optimize the fields on the interface, but not on the implementing nodes. For example if I select 'user' from Activity, the optimizer is able to prefetch the users correctly. But if I select booking or task, I get N+1.
Problem (1) could be easily worked around, I suppose. I could inject the 'type' field selection in my resolver (probably) and be done with it.
Problem (2) is not so simple... I don't know how difficult it would be to make optimizer do the right thing in this particular scenario. Which leads me to another problem...
Given that optimizer falls apart in my use case, I wanted to disable the optimizer and optimize everything myself, based on the selections, in the field resolver. Not perfect, but this is a very specific use case and I'd be fine with it. The issue is that the optimizer canno be disabled on the interface. The argument is not exposed (I will submit a PR for that in a moment). Furthermore, annotating the all_activities field with disable_optimization=True doesn't seem to do anything. Lastly, this in my resolver:
with optimizer.DjangoOptimizerExtension.disabled():
return cast(list[models.Activity], base_qs)
doesn't do anything either.
For the time being the only option for me is to disable optimizer globally or return a list from my field resolver which robs me of the built in support for filtering and ordering.
Please advise what could be done better. I appreciate all feedback.
Upvote & Fund
We're using Polar.sh so you can upvote and help fund this issue.
We receive the funding once the issue is completed & confirmed by you.
Thank you in advance for helping prioritize & fund our backlog.
Hi! I'm trying to expose a field (paginated connection) which returns a list of interface implementations. Basically pattern shown here: https://strawberry.rocks/docs/types/interfaces
In my specific case, I'm dealing with a stream of activities. There are some common fields that all activities have, like a timestamp but then there are fields specific to activities of a particular kind. For example a BookingActivity will have a relation to a booking object whereas TaskActivity will have a relation to a user (assignee). Sometimes I'm interested in looking at only BookingActivities or only at TaskActivities but sometimes I'm also interested in looking at all activities across types.
In Django, to achieve the above, usually you create a single model with a field that denotes the 'type' (an enum or something) and a bunch of nullable fields and then have various proxy classes that expose properties/methods that make sense to that particular kind of entity. That's exactly what I'm doing.
So how do I expose that in graphql? I was able to get pretty far by utilizing
is_of_type
. Take a look:Cool, now I can expose the connection field:
The above mostly works. I can query the
allActivities
field and switch on the type and select appropriate subset of fields. I can also add ordering/filtering if desired.The problem is that the optimizer doesn't seem to know what to do. There are 2 primary problems:
Activity
to determine the actual node type. If I do not select 'type' in my query, the optimizer will cause that field not to be retrieved and thus will lead to N+1 on every single row.Problem (1) could be easily worked around, I suppose. I could inject the 'type' field selection in my resolver (probably) and be done with it.
Problem (2) is not so simple... I don't know how difficult it would be to make optimizer do the right thing in this particular scenario. Which leads me to another problem...
Given that optimizer falls apart in my use case, I wanted to disable the optimizer and optimize everything myself, based on the selections, in the field resolver. Not perfect, but this is a very specific use case and I'd be fine with it. The issue is that the optimizer canno be disabled on the interface. The argument is not exposed (I will submit a PR for that in a moment). Furthermore, annotating the
all_activities
field withdisable_optimization=True
doesn't seem to do anything. Lastly, this in my resolver:doesn't do anything either.
For the time being the only option for me is to disable optimizer globally or return a list from my field resolver which robs me of the built in support for filtering and ordering.
Please advise what could be done better. I appreciate all feedback.
Upvote & Fund