如何在django_tables2中预取关联模型以避免大量数据库查询?
问题
我可能忽略了一些简单的实现方法,目前缺乏相关操作知识。我定义了三个模型:Site、SiteField和SiteFieldValue,希望为Site创建一个Django表格,将SiteFieldValue的值作为特定站点行中对应列的内容。但每个站点有数十个此类字段,每个render_方法都会触发一次查询,乘以站点数量后会产生大量数据库查询,我希望避免这种情况。
我的问题是:能否在SiteListTable类中预取每个站点的所有SiteFieldValue值,将其存入数组,然后在render_方法中通过字段ID直接获取对应值,而非每次查询数据库?
模型代码
class Site(models.Model): name = models.CharField(max_length=100) class SiteField(models.Model): name = models.CharField(max_length=100) description = models.CharField(max_length=500, null=True, blank=True) def __str__(self): return self.name class SiteFieldValue(models.Model): site = models.ForeignKey(Site, on_delete=models.CASCADE) field = models.ForeignKey(SiteField, on_delete=models.CASCADE) value = models.CharField(max_length=500)
表格视图代码
class SiteListTable(tables.Table): name = tables.Column() importance = tables.Column(verbose_name='Importance',empty_values=()) vertical = tables.Column(verbose_name='Vertical',empty_values=()) #... 还有更多类似字段,都基于SiteFieldValue def render_importance(self, value, record): q = SiteFieldValue.objects.filter(site=record, field=1).first() # ^^ 我不想这么做!我希望提前预取该模型的所有SiteFieldValue,然后在这里直接从数组中通过字段ID查找对应值。 if q: return q.value else: return None def render_vertical(self, value, record): q = SiteFieldValue.objects.filter(site=record, field=2).first() # ^^ 我不想这么做!我希望提前预取该模型的所有SiteFieldValue,然后在这里直接从数组中通过字段ID查找对应值。 if q: return q.value else: return None class Meta: model = Site attrs = { "class": "table table-striped", "thead" : {'class': 'thead-light',}} template_name = "django_tables2/bootstrap.html" fields = ("name", "importance", "vertical",)
解决方案
完全可以,通过预取关联数据并转换为字典映射,就能彻底避免重复查询,具体分两步操作:
1. 在视图中预取SiteFieldValue数据
在传递给表格的Site查询集里,用prefetch_related预取所有关联的SiteFieldValue,同时用Prefetch对象优化查询逻辑:
from django.db.models import Prefetch # 在视图中获取Site数据时 sites = Site.objects.prefetch_related( Prefetch( 'sitefieldvalue_set', queryset=SiteFieldValue.objects.select_related('field'), to_attr='prefetched_values' ) ) # 将预取后的查询集传给表格 table = SiteListTable(sites)
这里to_attr='prefetched_values'会把预取的数据存到每个Site实例的prefetched_values属性中,方便后续直接调用。
2. 在表格类中处理预取的数据
修改表格的render_方法,直接从预取的字典映射中取值,无需再发起数据库查询:
class SiteListTable(tables.Table): name = tables.Column() importance = tables.Column(verbose_name='Importance', empty_values=()) vertical = tables.Column(verbose_name='Vertical', empty_values=()) # 其他字段... def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # 为每个站点生成「字段ID→值」的映射字典 for record in self.data: record.field_values = { val.field_id: val.value for val in record.prefetched_values } def render_importance(self, value, record): return record.field_values.get(1) # 直接通过字段ID取值,无数据库查询 def render_vertical(self, value, record): return record.field_values.get(2) class Meta: model = Site attrs = { "class": "table table-striped", "thead": {'class': 'thead-light'} } template_name = "django_tables2/bootstrap.html" fields = ("name", "importance", "vertical",)
额外优化:动态生成字段(可选)
如果字段数量很多,没必要手动编写每个render_方法,可以动态生成列,提升效率:
class SiteListTable(tables.Table): name = tables.Column() def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # 先获取需要展示的SiteField(可根据业务逻辑筛选) target_fields = SiteField.objects.filter(id__in=[1,2]) for field in target_fields: # 动态添加列 self.columns.add( tables.Column( verbose_name=field.name, empty_values=(), render=lambda value, record, fid=field.id: record.field_values.get(fid) ), field.name.lower() # 设置列名,比如importance、vertical ) # 生成每个站点的字段映射字典 for record in self.data: record.field_values = { val.field_id: val.value for val in record.prefetched_values } class Meta: model = Site attrs = { "class": "table table-striped", "thead": {'class': 'thead-light'} } template_name = "django_tables2/bootstrap.html" fields = ("name",)
这样不管有多少个字段,都能自动生成列,全程仅2次数据库查询(一次查Site,一次查关联的SiteFieldValue),彻底解决N+1查询问题。
内容的提问来源于stack exchange,提问作者rass
相关产品推荐
相关产品推荐

