Связанное с фильтром поле против другой связанной модели M2M в Django
Итак, у меня есть система бронирования. Агенты (люди и организации, отправляющие заказы) разрешены только для бронирования в категориях, которые мы им назначаем. Многие агенты могут назначать одни и те же категории. Это простой для многих. Вот представление о том, как выглядят модели:
class Category(models.Model):
pass
class Agent(models.Model):
categories = models.ManyToManyField('Category')
class Booking(models.Model):
agent = models.ForeignKey('Agent')
category = models.ForeignKey('Category')
Итак, когда приходит заказ, мы динамически выделяем категорию, на основе которой доступны агенту. Агент обычно не указывает.
Можно ли выбрать "Заказы", в которых "Booking.kategory" не находится в Booking.agent.categories?
Мы только что заметили, что по милости глупой ошибки администратора некоторым агентам разрешалось отправлять заказы в любую категорию. Это оставило нас с тысячами заказов в неправильном месте.
Я могу исправить это, но я могу заставить его работать только с помощью поиска вложенности:
for agent in Agent.objects.all():
for booking in Booking.objects.filter(agent=agent):
if booking.category not in agent.categories.all():
# go through the automated allocation logic again
Это работает, но он очень медленный. Это много данных, которые летают между базой данных и Django. Это тоже не одноразовый. Я хочу периодически проверять новые заказы, чтобы убедиться, что они находятся в правильном месте. Не кажется невозможным, чтобы произошла другая проблема с администратором, поэтому после проверки базы данных агента я хочу запросить Заказы, которые не входят в их категории агентов.
Опять же, вложенные запросы будут работать не так, как только наши наборы данных вырастут до миллионов (и далее), я бы хотел сделать это более эффективно.
Мне кажется, что это можно сделать с помощью поиска F()
, примерно так:
from django.db.models import F
bad = Booking.objects.exclude(category__in=F('agent__categories'))
Но это не работает: TypeError: 'Col' object is not iterable
Я также пробовал .exclude(category=F('agent__categories'))
, и пока он более счастлив с синтаксисом, он не исключает "правильные" заказы.
Какова секретная формула для выполнения этого запроса F()
на M2M?
Чтобы скрыть то, что мне нужно, я создал репозиторий Github с этими моделями (и некоторые данные). Пожалуйста, используйте их для написания запроса. Нынешний единственный ответ на вопрос и проблема, которую я видел на моих "реальных" данных тоже.
git clone https://github.com/oliwarner/djangorelquerytest.git
cd djangorelquerytest
python3 -m venv venv
. ./venv/bin/activate
pip install ipython Django==1.9a1
./manage.py migrate
./manage.py shell
И в оболочке огонь в:
from django.db.models import F
from querytest.models import Category, Agent, Booking
Booking.objects.exclude(agent__categories=F('category'))
Это ошибка? Есть ли правильный способ достичь этого?
Ответы
Ответ 1
Есть вероятность, что я могу ошибаться, но я думаю, что делать это в обратном направлении должно быть трюком:
bad = Booking.objects.exclude(agent__categories=F('category'))
Edit
Если выше не будет работать, вот еще одна идея. Я пробовал аналогичную логику в настройке, которую у меня есть, и она работает. Попробуйте добавить промежуточную модель для ManyToManyField
:
class Category(models.Model):
pass
class Agent(models.Model):
categories = models.ManyToManyField('Category', through='AgentCategory')
class AgentCategory(models.Model):
agent = models.ForeignKey(Agent, related_name='agent_category_set')
category = models.ForeignKey(Category, related_name='agent_category_set')
class Booking(models.Model):
agent = models.ForeignKey('Agent')
category = models.ForeignKey('Category')
Затем вы можете сделать запрос:
bad = Booking.objects.exclude(agent_category_set__category=F('category'))
Конечно, указание промежуточной модели имеет свои собственные последствия, но я уверен, что вы сможете справиться с ними.
Ответ 2
Обычно при работе с отношениями m2m я беру гибридный подход. Я бы разбил проблему на две части: часть python и sql. Я нахожу, что это ускоряет запрос и не требует сложного запроса.
Первое, что вы хотите сделать, это получить сопоставление агента с категорией, а затем использовать это сопоставление для определения категории, которая не входит в присваивание.
def get_agent_to_cats():
# output { agent_id1: [ cat_id1, cat_id2, ], agent_id2: [] }
result = defaultdict(list)
# get the relation using the "through" model, it is more efficient
# this is the Agent.categories mapping
for rel in Agent.categories.through.objects.all():
result[rel.agent_id].append(rel.category_id)
return result
def find_bad_bookings(request):
agent_to_cats = get_agent_to_cats()
for (agent_id, cats) in agent_to_cats.items():
# this will get all the bookings that NOT belong to the agent category assignments
bad_bookings = Booking.objects.filter(agent_id=agent_id)
.exclude(category_id__in=cats)
# at this point you can do whatever you want to the list of bad bookings
bad_bookings.update(wrong_cat=True)
return HttpResponse('Bad Bookings: %s' % Booking.objects.filter(wrong_cat=True).count())
Вот небольшая статистика, когда я запускал тест на своем сервере: 10 000 агентов 500 Категории 2,479,839 Агент для назначения категорий 5 000 000 заказов
2,509,161 Плохие заказы. Общая продолжительность 149 секунд
Ответ 3
Решение 1:
Вы можете найти хорошие заказы, используя этот запрос
good = Booking.objects.filter(category=F('agent__categories'))
Вы можете проверить запрос sql для этого
print Booking.objects.filter(category=F('agent__categories')).query
Таким образом, вы можете исключить хорошие заказы из всех заказов.
Решение:
Booking.objects.exclude(id__in=Booking.objects.filter(category=F('agent__categories')).values('id'))
Он создаст MySql-вложенный запрос, который является самым оптимизированным запросом MySql для этой проблемы (насколько я знаю).
Этот MySql-запрос будет немного тяжелым, так как ваша база данных огромна, но она попадет в базу данных только один раз вместо вашей первой попытки циклов, которая будет срабатывать при бронировании * agent_categories times.
Кроме того, вы можете сделать набор данных меньше, используя фильтрацию по дате, если вы их сохраняете, и у вас есть аппроксимация, когда началось неправильное бронирование.
Вы можете использовать указанную выше команду, чтобы проверить наличие непоследовательных заказов.
Но я бы порекомендовал вам переместиться в админ-форму и проверить при заказе, если категория верна или нет.
Также вы можете использовать некоторый javascript для добавления только категорий в форме администратора, которые присутствуют для выбранного/зарегистрированного агента в то время.
Решение 2:
используйте prefetch_related, это значительно сократит ваше время, потому что очень мало ударов базы данных.
читайте об этом здесь: https://docs.djangoproject.com/en/1.8/ref/models/querysets/
for agent in Agent.objects.all().prefetch_related('bookings, categories'):
for booking in Booking.objects.filter(agent=agent):
if booking.category not in agent.categories.all():
Ответ 4
Это может ускорить его...
for agent in Agent.objects.iterator():
agent_categories = agent.categories.all()
for booking in agent.bookings.iterator():
if booking.category not in agent_categories:
# go through the automated allocation logic again
Ответ 5
Возможно, это не то, что вы ищете, но вы можете использовать необработанный запрос. Я не знаю, можно ли это сделать полностью в ORM, но это работает в вашем реестре github:
Booking.objects.raw("SELECT id \
FROM querytest_booking as booking \
WHERE category_id NOT IN ( \
SELECT category_id \
FROM querytest_agent_categories as agent_cats \
WHERE agent_cats.agent_id = booking.agent_id);")
Я предполагаю, что имена таблиц будут разными для вас, если ваше приложение не будет называться querytest
. Но в любом случае это можно повторить, чтобы вы могли подключить свою собственную логику.
Ответ 6
Ты был почти там. Сначала создайте два элемента бронирования:
# b1 has a "correct" agent
b1 = Booking.objects.create(agent=Agent.objects.create(), category=Category.objects.create())
b1.agent.categories.add(b1.category)
# b2 has an incorrect agent
b2 = Booking.objects.create(agent=Agent.objects.create(), category=Category.objects.create())
Вот список всех неправильных заказов (например: [b2]
):
# The following requires a single query because
# the Django ORM is pretty smart
[b.id for b in Booking.objects.exclude(
id__in=Booking.objects.filter(
category__in=F('agent__categories')
)
)]
[2]
Обратите внимание, что по моему опыту следующий запрос не вызывает ошибок, но по какой-то неизвестной причине результат также неверен:
Booking.objects.exclude(category__in=F('agent__categories'))
[]