我的Django项目中有下一个模型:
class Area(models.Model):
name = models.CharField(_('name'), max_length=100, unique=True)
...
class Zone(models.Model):
name = models.CharField(verbose_name=_('name'),
max_length=100,
unique=True)
area = models.ForeignKey(Area,
verbose_name=_('area'),
db_index=True)
polygon = PolygonField(srid=4326,
verbose_name=_('Polygon'),)
...
Area
就像一个城市,而Zone
就像一个地区。
因此,我想为每个区域高速缓存其区域中其他区域的顺序。像这样:
def store_zones_by_distance():
zones = {}
zone_qs = Zone.objects.all()
for zone in zone_qs:
by_distance = Zone.objects.filter(area=zone.area_id).distance(zone.polygon.centroid).order_by('distance').values('id', 'name', ...)
zones[zone.id] = [z for z in by_distance]
cache.set("zones_by_distance", zones, timeout=None)
但是问题在于它效率不高且不可扩展。我们有382个区域,此函数将383个查询发送到数据库,并且非常慢(SQL时间为3.80秒,全局时间为4.20秒)。
是否有任何有效且可扩展的方式来获取它。我曾经想到过这样的事情:
def store_zones_by_distance():
zones = {}
zone_qs = Zone.objects.all()
for zone in zone_qs.prefetch_related(Prefetch('area__zone_set', queryset=Zone.objects.all().distance(F('polygon__centroid')).order_by('distance'))):
by_distance = zone.area.zone_set.all().values('id', 'name', ...)
zones[zone.id] = [z for z in by_distance]
这显然不起作用,但是类似这样,在SQL(与预取相关)中对有序区域(area__zone_set)进行缓存。
编辑 store_zones_by_distance将返回(或在缓存中设置)以下内容:
{
1: [{"id": 1, "name": "Zone 1"}, {"id": 2, "name": "Zone 2"}, {"id": 2, "name": "Zone 4"}, {"id": 2, "name": "Zone 3"}],
2: [{"id": 2, "name": "Zone 2"}, {"id": 2, "name": "Zone 4"}, {"id": 2, "name": "Zone 1"}, {"id": 2, "name": "Zone 3"}],
...
}
答案 0 :(得分:3)
您可以进行嵌套的预取,导致3个查询。
def store_zones_by_distance():
area_qs = Area.objects.prefetch_related(Prefetch(
'zone_set',
queryset=Zone.objects.annotate(
distance=F('polygon__centroid')
).order_by('distance')
))
zones = Zone.objects.all().prefetch_related(Prefetch(
'area',
queryset=area_qs,
to_attr='prefetched_area'
))
zones_dict = {}
for zone in zones:
zones_dict[zone.id] = zone.prefetched_area.zone_set
更新,将 @JohnMoutafis 中的功能与django.forms.model_to_dict
结合使用,可在2个查询中完成预期的输出。
from django.db.models import F, Prefetch
from django.forms import model_to_dict
def store_zones_by_distance():
zones = {}
areas = Area.objects.prefetch_related(Prefetch(
'zone_set',
queryset=Zone.objects.annotate(
distance=Centroid('polygon')
).order_by('distance')
))
for area in areas:
for zone in area.zone_set.all():
zones[zone.id] = [
model_to_dict(zone, fields=['id', 'name'])
for zone in area.zone_set.all()
]
答案 1 :(得分:2)
更新:自从我们来回往返后,我相信我们可以找到解决该问题的可行方案。
您需要按区域之间的距离排列区域。据我了解,这不需要发生很多次(因此您正在使用缓存)。
本质上,您需要在服务器启动时以及每次在数据库上更新(添加,删除,打补丁等)新区域时设置一次此缓存。
我们可以使用AppConfig.ready()
函数来设置服务器启动时的缓存,然后为区域更新情况创建一个post_save
和一个post_delete
信号。
让我们编写在这两种情况下将使用的实用程序方法:
from django.db.models import Q
from django.forms import model_to_dict
def store_zones_by_distance():
zones = {}
areas = Area.objects.prefetch_related(`zone_set`).all()
for area in areas:
for zone in area.zone_set.all():
ordered_zones = area.zone_set.filter(~Q(id=zone.id)).distance(
zone.polygon.centroid
).order_by('distance')
zones[zone.id] = [
model_to_dict(ordered_zone, fields=['id', 'name'])
for ordered_zone in ordered_zones
]
cache.set("zones_by_distance", zones, timeout=None)
方法说明:
ordered_zones
将返回除我们当前正在检查的区域以外的所有区域(因此filter(~Q(id=zone.id))
转换为“过滤ID为 NOT 的ID的区域)当前区域”),按其质心到当前区域质心的距离排序。model_to_dict
的建议,我们以字典表示形式创建模型实例的列表。[{"id": 1, "name": "Zone 1"}, {"id": 2, "name": "Zone 2"}, ...]
现在,我们需要创建post_save
和post_delete
信号并将所有内容连接到AppConfig.ready()
函数(基本上,我们将按照此处描述的步骤进行操作:Django Create and Save Many instances of model when another object are created扭曲)。
我假设store_zones_by_distance
是在your_app/utils.py
中创建的(您可以在任意位置创建它):
在post_save
中创建post_delete
和your_app/signals.py
信号:
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from your_app.models import Zone
from your_app.utils import store_zones_by_distance
@receiver(post_save, sender=Zone)
def update_added_zone_cache(sender, instance, created, **kwargs):
store_zones_by_distance()
@receiver(post_delete, sender=Zone)
def update_removed_zone_cache(sender, instance, *args, **kwargs):
store_zones_by_distance()
在服务器启动时运行store_zones_by_distance
,并在your_app/app.py
中连接信号:
class YourAppConfig(AppConfig):
name = 'your_project.your_app'
def ready(self):
import your_project.your_app.signals
# Run it once at server start
store_zones_by_distance()
您将不会为此节省很多时间,但是您将准备好缓存,并且在更新之前不会阻塞任何端点。
我相信您已经接近一个好的解决方案。
正如您已经在尝试一种更优化的解决方案那样,you can access the foreign key related objects with the _set
notation。您可以使用Zones
从Area
访问zones_set
。
_set
允许您像往常一样在其上应用任何queryset方法。
现在,为了避免多个数据库命中,我们需要构造a custom Prefetch
,否则我们将添加polygon__centroid
距离作为注释。
这么说吧,让我们实现它:
def store_zones_by_distance():
zones = {}
areas = Area.objects.prefetch_related(
Prefetch(
`zone_set`,
queryset=Zone.object.all().annotate(
centroid_distance=Centroid('polygon')
).order_by('centroid_distance')
)
).all()
for area in areas:
for zone in area.zone_set.all():
zones[zone.id] = area.zone_set.all().values_list('id', 'name', ...)
这将导致对数据库的单个查询,该查询将获取方法所需的一切。
编辑: 正如@bdoubleu所述,values_list
将在每个区域引起一个额外的查询,因此您可能希望放弃该查询并将查询集保留在字典{{1}中} 。
请记住,尽管使用2 zones[zone.id] = area.zone_set.all()
可能仍然很耗时。
答案 2 :(得分:-2)
对不起,我不能发表评论,因为我很新,所以我必须在这里写下建议。 在您的第一个示例中:
def store_zones_by_distance(): zones = {} zone_qs = Zone.objects.all() for zone in zone_qs: by_distance = Zone.objects.filter(area=zone.area_id).distance(zone.polygon.centroid).order_by('distance').values('id', 'name', ...) zones[zone.id] = [z for z in by_distance] cache.set("zones_by_distance", zones, timeout=None)
更改时需要多长时间会很有趣:
zone_qs = Zone.objects.all()
zone_qs = Zone.objects.all().prefetch_related("area")
和
by_distance = Zone.objects.filter(area=zone.area_id).distance...
by_distance = zone_qs.objects.filter(area=zone.area_id).distance...
希望我可以为这个话题提供一些有用的信息。