Skip to content

Commit 3506f55

Browse files
committed
fixed N+1 problem on ClassFeatureItems
1 parent 8a3f15a commit 3506f55

File tree

5 files changed

+83
-21
lines changed

5 files changed

+83
-21
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Generated by Django 5.1.2 on 2025-03-24 14:10
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('api_v2', '0026_delete_searchresult'),
11+
]
12+
13+
operations = [
14+
migrations.AlterField(
15+
model_name='classfeatureitem',
16+
name='parent',
17+
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feature_items', to='api_v2.classfeature'),
18+
),
19+
]
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Generated by Django 5.1.2 on 2025-03-24 14:14
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('api_v2', '0027_alter_classfeatureitem_parent'),
11+
]
12+
13+
operations = [
14+
migrations.AlterField(
15+
model_name='classfeature',
16+
name='parent',
17+
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='features', to='api_v2.characterclass'),
18+
),
19+
]

api_v2/models/characterclass.py

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class ClassFeatureItem(models.Model):
2323
# Somewhere in here is where you'd define a field that would eventually display as "Rage Damage +2"
2424
# Also spell slots...?
2525

26-
parent = models.ForeignKey('ClassFeature', on_delete=models.CASCADE)
26+
parent = models.ForeignKey('ClassFeature', on_delete=models.CASCADE, related_name="feature_items")
2727
level = models.IntegerField(validators=[MinValueValidator(0),MaxValueValidator(20)])
2828
detail = models.CharField(
2929
null=True,
@@ -48,17 +48,16 @@ class ClassFeature(HasName, HasDescription, FromDocument):
4848
"""This class represents an individual class feature, such as Rage, or Extra
4949
Attack."""
5050

51-
parent = models.ForeignKey('CharacterClass',
52-
on_delete=models.CASCADE)
51+
parent = models.ForeignKey('CharacterClass', on_delete=models.CASCADE, related_name="features")
5352

5453
def gained_at(self):
55-
return self.classfeatureitem_set.exclude(column_value__isnull=False)
54+
return self.feature_items.filter(column_value__isnull=True)
5655

5756
def table_data(self):
5857
"""Returns an array of tabular data relating to the feature. Each
5958
array element is a table-row of data. Not needed for most features."""
6059

61-
return self.classfeatureitem_set.exclude(column_value__isnull=True)
60+
return self.feature_items.filter(column_value__isnull=False)
6261

6362
# Infer the type of this feature based on the `key`
6463
@property
@@ -134,11 +133,6 @@ def is_subclass(self):
134133
"""Returns whether the object is a subclass."""
135134
return self.subclass_of is not None
136135

137-
@property
138-
def features(self):
139-
"""Returns the set of features that are related to this class."""
140-
return self.classfeature_set
141-
142136
@extend_schema_field(serializers.DictField(
143137
child=inline_serializer(
144138
name="levels",
@@ -228,7 +222,7 @@ def __str__(self):
228222
def as_text(self):
229223
text = self.name + '\n'
230224

231-
for feature in self.classfeature_set.all():
225+
for feature in self.features.all():
232226
text+='\n' + feature.as_text()
233227

234228
return text

api_v2/serializers/characterclass.py

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,55 @@ class Meta:
1616
class ClassFeatureColumnItemSerializer(GameContentSerializer):
1717
class Meta:
1818
model = models.ClassFeatureItem
19-
fields = ['level','column_value']
19+
fields = ['level', 'column_value']
20+
21+
class ClassFeaturePrefetchSerializer(GameContentSerializer):
22+
class Meta:
23+
model = models.ClassFeatureItem
24+
fields = ['level', 'detail', 'column_value']
2025

2126
class ClassFeatureSerializer(GameContentSerializer):
2227
key = serializers.ReadOnlyField()
23-
gained_at = ClassFeatureItemSerializer(
24-
many=True
25-
)
28+
feature_items = ClassFeaturePrefetchSerializer(many=True, read_only=True)
29+
30+
def to_representation(self, instance):
31+
# run 'to_representation' on super-class (GameContentSerializer)
32+
representation = super().to_representation(instance)
33+
34+
# Filters non-table data from FeatureItems
35+
gained_at = [
36+
ClassFeatureItemSerializer(item).data
37+
for item in instance.feature_items.all()
38+
if item.column_value is None
39+
]
40+
41+
# Filters table data from FeatureItems
42+
column_data = [
43+
ClassFeatureColumnItemSerializer(item).data
44+
for item in instance.feature_items.all()
45+
if item.column_value is not None
46+
]
47+
48+
# replace 'feature_items' with 'gained_at' and 'column_data' in representation
49+
representation['gained_at'] = gained_at
50+
representation['column_data'] = column_data
51+
del representation['feature_items']
2652

27-
table_data = ClassFeatureColumnItemSerializer(
28-
many=True
29-
)
53+
return representation
3054

3155
class Meta:
3256
model = models.ClassFeature
33-
fields = ['key', 'name', 'desc','gained_at','table_data', 'feature_type']
57+
fields = [
58+
'key',
59+
'name',
60+
'desc',
61+
'feature_type',
62+
'feature_items'
63+
]
3464

3565
class CharacterClassSerializer(GameContentSerializer):
3666
key = serializers.ReadOnlyField()
37-
features = ClassFeatureSerializer(many=True)
67+
features = ClassFeatureSerializer(many=True, read_only=True)
3868
hit_points = serializers.ReadOnlyField()
3969
document = DocumentSummarySerializer()
4070

api_v2/views/characterclass.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,4 @@ class CharacterClassViewSet(EagerLoadingMixin, viewsets.ReadOnlyModelViewSet):
3333
filterset_class = CharacterClassFilterSet
3434

3535
select_related_fields = []
36-
prefetch_related_fields = ['document', 'saving_throws', 'features', 'subclass_of']
36+
prefetch_related_fields = ['document', 'saving_throws', 'features', 'subclass_of', 'features__feature_items']

0 commit comments

Comments
 (0)