Ограничение __in поиска в django
У меня есть вопрос о поиске "__in" в Django ORM.
Вот пример кода:
tags = [Tag.objects.get(name="sometag")]
servers = Server.objects.filter(tags__in=tags)[offset_from:offset_to]
server_infos = []
for server in servers:
server_infos.append(query.last())
Вот в чем проблема: мы делаем около 60-70 sql запросов для каждого сервера. Но я хочу сделать что-то вроде этого:
tags = [Tag.objects.get(name="sometag")]
servers = Server.objects.filter(tags__in=tags)[offset_from:offset_to]
server_infos = ServerInfo.objects.filter(contains__in=servers)
assert servers.count() == server_infos.count()
Могу ли я сделать это без необработанного sql-запроса? Все, что мне нужно понять, это как ограничить выражение "__in" в Django, чтобы получить только последнее значение, как в примере выше. Возможно ли это?
Обновление, мои модели:
class Tag(models.Model):
name = models.CharField(max_length=255, blank=True)
added_at = models.DateTimeField(auto_now_add=True, null=True)
def __str__(self):
return self.name
class Server(models.Model):
ip = models.CharField(max_length=255, blank=True)
port = models.IntegerField(blank=True)
name = models.CharField(max_length=255, blank=True)
tags = models.ManyToManyField(Tag)
added_at = models.DateTimeField(auto_now_add=True, null=True)
def __str__(self):
return self.name
def get_server_online(self):
query = ServerInfo.objects.filter(contains=self)
if query.exists():
return query.last().online
return 0
class ServerInfo(models.Model):
contains = models.ForeignKey(Server, \
on_delete=models.CASCADE, null=True, blank=True)
map = models.CharField(max_length=255, blank=True)
game = models.CharField(max_length=255, blank=True)
online = models.IntegerField(null=True)
max_players = models.IntegerField(null=True)
outdated = models.BooleanField(default=False)
tags = models.ManyToManyField(Tag)
ping = models.IntegerField(null=True)
def __str__(self):
return f"Current map {self.map} and current online {self.online}/{self.max_players}"
Я думаю, что проблема в том, что теги ManyToMany
и сервер может быть выбран дважды двумя разными тегами. Также сервер может иметь >1 serverinfos, потому что это ForeignKey
отношение, а не OneToOne
.
Возможности:
Убедитесь, что сервер возвращается только один раз в наборе запросов:
servers = Server.objects.filter(tags__in=tags).distinct()[offset_from:offset_to]
(или distinct('pk')
?)
Убедитесь, что только один ServerInfo
экземпляр возвращается на сервер:
server_infos = ServerInfo.objects.filter(contains__in=servers).distinct('contains')
Или использовать prefetch_related
в запросе Servers, а затем избежать последующих запросов, всегда ссылаясь на объекты serverinfo
через связанное имя (по умолчанию "serverinfo_set")
Я совершенно не люблю "магические" связанные имена по умолчанию, и всегда буду кодировать их явно: contains = models.ForeignKey(Server, ..., related_name='server_infos', ...)
servers = Server.objects.filter(tags__in=tags).distinct(
).prefetch_related('serverinfo')[offset_from:offset_to]
for server in servers:
server_info = server.serverinfo_set.first()
# or
for info in server.serverinfo_set:
NB не начинайте применять фильтры к serverinfo_set
, если вы не хотите обращаться к БД N раз. Фильтруйте путем итерации по тому, что предположительно является коротким списком, и который в любом случае уже находится в памяти в наборе запросов.
Ниже приведены данные, которые необходимо получить с помощью двух запросов: один - для получения всех серверов, а второй - с помощью prefetch_related()
. При фильтрации по ManyToManyField
необходимо использовать distinct()
, чтобы избежать дублирования результатов .
servers = Server.objects.filter(
tags__name="sometag").distinct().prefetch_related("server_info_set")
for server in servers:
print(server.name)
for info in server.server_info_set.all():
print(info)
Если вы хотите получать только одну определенную информацию на сервер, вам придется предоставить пользовательский набор запросов prefetch_related()
, используя Prefetch()
объекты.
from django.db.models import Prefetch
servers = Server.objects.filter(
tags__name="sometag").distinct().prefetch_related(Prefetch('server_info_set',
queryset=ServerInfo.objects.filter(outdated=False),
to_attr="current_infos"
)
)
for server in servers:
print(server.name)
for info in server.current_infos.all():
print(info)