Model Properties
Model properties allow you to add computed fields directly to your Django models while providing optimization hints for the GraphQL query optimizer. This feature is particularly useful when you want to expose derived data in your GraphQL schema without triggering N+1 queries or deferred attribute issues.
Overview
Strawberry Django provides two decorators for adding model properties:
-
@model_property: Similar to Pythonβs@propertybut with optimization hints -
@cached_model_property: Similar to Djangoβs@cached_propertybut with optimization hints
Both decorators accept the same optimization parameters as strawberry_django.field() , allowing the optimizer to properly prefetch or select related data.
Basic Usage
Basic Model Property
from decimal import Decimalfrom django.db import modelsfrom strawberry_django.descriptors import model_property
class OrderItem(models.Model): price = models.DecimalField(max_digits=10, decimal_places=2) quantity = models.IntegerField()
@model_property(only=["price", "quantity"]) def total(self) -> Decimal: """Calculate the total price for this order item.""" return self.price * self.quantity import strawberry_djangofrom strawberry import auto
@strawberry_django.type(models.OrderItem)class OrderItem: price: auto quantity: auto total: auto # Automatically resolved with optimization hints The only parameter tells the optimizer to ensure price and quantity are fetched from the database when total is requested.
Cached Model Property
For expensive computations that should only be calculated once per instance, use @cached_model_property :
from django.db import modelsfrom strawberry_django.descriptors import cached_model_property
class Product(models.Model): name = models.CharField(max_length=100)
@cached_model_property( prefetch_related=["reviews"] ) def average_rating(self) -> float: """Calculate average rating from all reviews.""" reviews = list(self.reviews.all()) if not reviews: return 0.0 return sum(r.rating for r in reviews) / len(reviews) The computed value is cached on the instance after the first access, avoiding redundant calculations.
Optimization Parameters
Model properties accept the same optimization hints as strawberry_django.field() . See the Query Optimizer guide for complete details.
Combining with GraphQL Types
Model properties integrate seamlessly with strawberry.auto :
from decimal import Decimalfrom django.db import modelsfrom django.db.models import Sumfrom strawberry_django.descriptors import model_property, cached_model_property
class Order(models.Model): customer = models.ForeignKey("Customer", on_delete=models.CASCADE) created_at = models.DateTimeField(auto_now_add=True) status = models.CharField(max_length=20)
@model_property( prefetch_related=["items"], annotate={"_total": Sum("items__total")} ) def total_amount(self) -> Decimal: """Calculate total order amount.""" return self._total or Decimal(0) # type: ignore
@cached_model_property(select_related=["customer"]) def customer_name(self) -> str: """Get the customer's full name.""" return f"{self.customer.first_name} {self.customer.last_name}" import strawberry_djangofrom strawberry import auto
@strawberry_django.type(models.Order)class Order: created_at: auto status: auto total_amount: auto # Uses model_property optimization hints customer_name: auto # Uses cached_model_property hints Best Practices
-
Always provide optimization hints: If your property accesses model fields or relations, specify them in the decorator parameters.
-
Use cached_model_property for expensive operations: If the calculation is expensive and doesnβt depend on mutable data, use caching.
-
Keep properties focused: Complex business logic should be in separate service classes, not in model properties.
-
Type annotations are required: Always provide return type annotations for model properties.
-
Document your properties: Add clear docstrings that will appear in your GraphQL schema.
-
Test with the optimizer: Ensure your optimization hints actually work by checking the generated SQL queries.
-
Use
len()instead of.count()with prefetch_related: When accessing prefetched relationships, uselen()to avoid bypassing the prefetch cache:
# β Bad: .count() bypasses prefetch cache and hits database@model_property(prefetch_related=["books"])def book_count(self) -> int: return self.books.count() # Issues a COUNT(*) query!
# β
Good: len() uses prefetch cache@model_property(prefetch_related=["books"])def book_count(self) -> int: return len(self.books.all()) # Uses prefetched data
# β
Best: Use database annotation when prefetch not needed@model_property(annotate={"_book_count": Count("books")})def book_count(self) -> int: return self._book_count # type: ignore Troubleshooting
Property triggers extra queries
If your model property is still causing N+1 queries:
- Check that optimization hints match the actual database access
- Ensure the Query Optimizer Extension is enabled
- Verify that
onlyincludes all accessed fields - Use
select_relatedfor foreign keys, notprefetch_related
Type resolution errors
If Strawberry canβt resolve the type:
# β Missing return type annotation@model_property(only=["name"])def display_name(self): return self.name.upper()
# β
With return type annotation@model_property(only=["name"])def display_name(self) -> str: return self.name.upper() See Also
- Query Optimizer - Understanding optimization hints
- Custom Resolvers - Alternative approaches for computed fields
- Fields - Basic field definition and customization