-
-
Notifications
You must be signed in to change notification settings - Fork 22
Improving use_query #113
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
In my opinion, (1) and (3) are linked. Our present attempts to pre-fetch as many fields and relationships as possible are related to the fact that we're trying to protect users from experiencing a |
The issue is those exceptions are happening in the context of component renders. We could possibly mutate all ORM objects that pass through IDOM to provide custom exception handling. But there's no realistic way to fix this for users who attempt to directly perform ORM queries in a component without |
Since errors that happen in the context of a render function get logged. I think the best option might be to use a custom log handler to amend the reported log record to include a more helpful message. Another option might be to change |
Something for us to keep in mind is that these Django exceptions may not be a permanent feature of Django. In fact, with the last revision of Django the exceptions technically are no longer necessary. With every update Django has been adding more async ORM compatibility, I would expect these exceptions to be removed from Django core at some point. However, this may not change what we need to do today. But it is likely to complicate our solution further down the line. |
As for (2), I think there may be a way to get rid of the use_query(lambda: my_query(*a, **kw), other_options=...) The problem here is that since the identity of the query function will change on every render, we'll need to reach into the lambda to find def f(x, y):
return x + y
g = lambda: f(1, 2)
globals_referenced_by_g = [g.__globals__[name] for name in g.__code__.co_names]
assert f in globals_referenced_by_g There's probably a bit of work to do here in terms of hardening this heuristic, but it seems like a plausible solution to avoiding |
I honestly dislike the idea of heuristics, sounds like it's going to be a fairly error prone process. I'd rather concede to a slightly uglier API (for example, the |
On thinking further, I agree, cases like |
There's two more things I'd like to nail down:
In the case of (1), my intuition is that In the case of (2), I think |
Also I agree, if we can find a graceful way to handle error messages for SynchronousOnlyOperation exceptions, then prefetching should be disabled by default. However, I do think |
The proposal I submitted for
|
Ok, I have another idea. What if this is the interface: # default options
use_query(my_query, *args, **kwargs)
# explicit options
use_query(Options(fetch_policy="cache-first"), my_query, *args, **kwargs) MyPy seems to be ok with this: from typing import overload, ParamSpec, TypeVar, Callable, Any
from django_idom import Query
P = ParamSpec("P")
R = TypeVar("R")
@overload
def use_query(options: Options, query: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Query[R]: ...
@overload
def use_query(query: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Query[R]: ...
def use_query(*args: Any, **kwargs: Any) -> Query[Any]: ...
class Options: ... |
Works, but wouldn't provide any useful type errors if the user accidentally provides too many The type checker would just assume those unneeded args get lumped into the final definition of |
So the type checker doesn't actually use the final definition. It only looks at the ones in the |
A slight issue with this implementation is it would be fairly convoluted to support passing In terms of KISS, I think the decorator wins here. If we go with your type-hint based solution, we would end up needing a lot of boilerplate code for every new argument we add in the future. I've rewritten We can also consider removing the hooks.use_query(
QueryOptions(my_func, {"many_to_many": True}),
) |
I can't really think of a reason why a user would really want to pass those as kwargs. Plus being able to do so eats into the allowed kwargs for
I'm not really convinced of that. There's very little difference between passing an options object as the first arg vs using normal kwargs in use_query(my_func, *args, fetch_policy=..., **kwargs)`
# vs
use_query(Options(fetch_policy=...), my_func, *args, **kwargs) And in some ways this is preferable since it's clear to the reader which options belong to |
Regardless of whether they want to or not, the current type hint hack we're relying on would suggest I already drafted this change and can commit it to demonstrate the differences. |
Ok, I think we can probably go with both solutions. We can have a |
We can also make django-idom 3.8 only in the next release too to take advantage of pos-only parameters. I'll can do the same with IDOM-core too. |
Django-IDOM is already 3.8+, we only need to worry about core when this hook gets ported over. |
I've committed the changes. This is how it looks in practice. |
What I meant to suggest is that we could address post-processing (i.e. pre-fetching lazy model fields) and query options (i.e. ones analogous to Apollo's) with two different approaches. For post-processing, this could be accomplished with a standard decorator, and for the query options, we could add an optional positional-only argument to The decorator would be "standard" in that it doesn't return any special object, but rather just wraps the query function: def prefetch(many_to_many=False):
def decorator(query_func):
def wrapper(*args, **kwargs):
data = query_func(*args, **kwargs)
if many_to_many:
# do pre-fetching logic
return data
return wrapper
return decorator The positional-only options object (i.e. the one described earlier) is something that we can add later since we haven't implemented any caching or other behaviors that would require it. With that said, if you put both approaches together, usage would look like: @prefetch(many_to_many=True)
def get_user(*a, **kw):
...
@component
def example():
user = use_query(QueryOptions(fetch_policy="cache-first"), get_user, *a, **kw)
# QueryOptions could also be defined as a TypedDict so you could optionally do
user = use_query({"fetch_policy": "cache-first"}, get_user, *a, **kw) If that's still unclear I'm happy to meet to discuss further to try and nail this down. |
Right now my thought process is it feels more natural for all "configuration" to be in one place, regardless of whether that's a decorator or positional arg. |
The main reason I'm suggesting a decorator for configuration prefetch behavior is because it seems like, given a particular query function, users would always use the same settings? That is, for a query function that returns an object with a many-to-many relationship, users would always want to prefetch that. Thus, only allowing configuration at the call sight would require passing the same settings on every usage of that query when it could have been configured once at the definition sight. However, if prefetching behavior is more likely to vary from usage to usage, then only defining prefetch behavior at the call sight makes more sense. |
Also I think that the location where the |
Current Situation
There are a number of usability problems with
use_query
:use_query
does not properly pre-fetch some model fields #110Model
/QuerySet
restrictions onuse_query
#104, Addfetch_policy=...
touse_query
#103SynchronousOnlyOperation
error they get from Django does not communicate this fact and how to resolve it.Proposed Actions
Discuss and arrive at a solution to each of the above problems. I'll update this section as we reach consensus on how to proceed.
The text was updated successfully, but these errors were encountered: