Как сделать выборку товаров, имеющих все комбинации характеристик?
Упрощенный вариант модели:
class Product():
name = CharField()
width = IntegerField()
height = IntegerField()
diameter = IntegerField()
Пользователь вводит 2 сета размеров, допустим:
w = 10
h = 20
d = 30
w2 = 15
h2 = 25
d2 = 35
Моя задача выбрать все товары первого и второго размера, только если и первый и второй размер присутствуют в базе одновременно.
Например, вот такая пара товаров должна попасть в выборку:
<'Модель1', 10, 20, 30>
<'Модель1', 15, 15, 35>
А вот такой товар в выборку попасть не должен, т.к. у него нет пары со вторым размером:
<'Модель2', 10, 20, 30>
Можно ли сделать такую выборку одним запросом и как? Или придется решать задачу, храня какие-то промежуточные результаты?
Ответы (3 шт):
Начал изучать django, решал эту задачу 3 дня. Пока решал, узнал много нового, так что спасибо огромное за вопрос!
Начал новый проект market и приложение goods.
models.py
from django.db import models
# Create your models here.
class Good(models.Model):
name = models.CharField(max_length = 50)
height = models.IntegerField()
width = models.IntegerField()
diameter = models.IntegerField()
def __str__(self):
return self.name
Через Админку создал 6 моделей товаров:
name = Shoes, height = 10, width = 20, diameter = 30
name = Shoes, height = 15, width = 15, diameter = 35
name = Shoes, height = 20, width = 40, diameter = 50
name = Sneakers, height = 10, width = 20, diameter = 30
name = Sneakers, height = 15, width = 15, diameter = 35
name = Slippers, height = 10, width = 20, diameter = 30
forms.py
from django import forms
from django.core import validators
from goods.models import Good
class DoubleCheckForm(forms.ModelForm):
class Meta:
model = Good
exclude = ['name'] # Exclude name
views.py
from django.shortcuts import render
from goods.models import Good
from . import forms
from django.contrib.auth.forms import UserCreationForm
# Create your views here.
def search(request):
first_form = forms.DoubleCheckForm(prefix="first_form")
second_form = forms.DoubleCheckForm(prefix="second_form")
context = {'first_form':first_form,
'second_form':second_form,
'result':None}
if request.method == 'POST':
first_form = forms.DoubleCheckForm(request.POST, prefix="first_form")
second_form = forms.DoubleCheckForm(request.POST, prefix="second_form")
if first_form.is_valid() and second_form.is_valid():
print('VALIDATION SUCCESS!')
first_user_height = first_form.cleaned_data.get('height')
first_user_width = first_form.cleaned_data.get('width')
first_user_diameter = first_form.cleaned_data.get('diameter')
second_user_height = second_form.cleaned_data.get('height')
second_user_width = second_form.cleaned_data.get('width')
second_user_diameter = second_form.cleaned_data.get('diameter')
#QuerySet of corresponding to first_form's parameters goods
g1 = Good.objects.all().filter(height = first_user_height, width = first_user_width, diameter = first_user_diameter)
#QuerySet of corresponding to second_form's parameters goods
g2 = Good.objects.all().filter(height = second_user_height, width = second_user_width, diameter = second_user_diameter)
l1 = [i.name for i in g1] #List that consists from good's name from first QuerySet
l2 = [c.name for c in g2] #List that consists from good's name from second QuerySet
cur_res = [name for name in l1 if name in l2] #Good's names that correspon both to first and second form's parameters
result = ''
for i in cur_res:
result += f'{i}, '
context['result'] = result[0:-2]
return render(request, 'goods/search.html', context)
ШАБЛОН search.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Search</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
</head>
<body>
<style>
h3{
font-size: 38px;
background: -webkit-linear-gradient(rgb(128, 128, 128), #3cdede); /*#eee, #333*/
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
</style>
<div style="background-image: linear-gradient(rgb(128, 128, 128), #3cdede)">
<div class="container">
<div class="jumbotron">
<h1 class="display-4">Hello, world!</h1>
<p class="lead">You can type two kind of parametres to look for goods that relate to this parametres and have the same name</p>
<hr class="my-4">
<form method="POST">
{% csrf_token %}
<div class="row">
<div class="col-lg-5 col-5">
{{first_form.as_ul}}
</div>
<div class="col-lg-5 col-5">
{{second_form.as_ul}}
</div>
</div>
<hr class="my-4">
<div style="text-align: center;" class="row">
<div class="col-lg-9 col-9">
<input type="submit" class="btn btn-primary btn-lg" value="Submit" style="height: 2.4em; width: 7em">
</div>
</div>
</form>
<br>
<div style="text-align: center;" class="row">
<div class="col-lg-9 col-9">
{% if result %}
<h3><em>Result: {{result}}</em></h3>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</body>
</html>
Нажимаем на кнопку и получаем результат!

P.S Надеюсь Я ответил на ваш вопрос =))
P.P.S 2 вопроса, которые я не смог нагуглить и задал так же на stackOverflow :D Первый Второй
P.P.P.S Как говорится: Feel free to post any questions in the comments below :D
P.P.P.P.S Слишко много ПэЭсов.. Так вообще можно :D
снова я. Думаю тот скрипт, который я написал в прошлом ответе очень даже работоспособный и быстрый.
я немного обновил модель: добавил ещё одно поле, которое хранит количество товара, ведь это реалистично/
models.py
from django.db import models
# Create your models here.
class Good(models.Model):
name = models.CharField(max_length = 50)
height = models.IntegerField()
width = models.IntegerField()
diameter = models.IntegerField()
number = models.IntegerField() # NEW
def __str__(self):
return self.name
а в forms.py в exclude добавил number, чтоб этого поля в выводе не было
Создал незамысловатый скрипт фэйкер, который наполняет бд, чтоб не делать это в ручную populate_goods.py
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'market.settings')
import django
django.setup()
## FAKE POP SCRIPT
import random
from goods.models import Good
from faker import Faker
fakegen = Faker()
fake_name_list = ['Apres-ski', 'Waders', 'Combat boots', 'Trekking boots', 'Brogues',
'Bast shoes', 'kamik', 'Boots', 'Clog', 'Crackowes',
'Bedroom', 'Monquis', 'Moccasins', 'Slip-ons shoes', 'Athletic shoe',
'Cowboy boots', 'Keds', 'Derby', 'Gibson', 'Bucks',
'Geta', 'Galoshes', 'Flip-flops', 'jandals', 'Japanese sandals']
fake_height_list = [10, 15, 20, 30, 35, 40]
fake_width_list = [10, 15, 20, 30, 35, 40]
fake_diameter_list = [10, 15, 20, 30, 35, 40]
def populate(N = 5):
global number_of_created_models
number_of_created_models = N
for entry in range(N):
# Create the fake data for that entry
fake_name = random.choice(fake_name_list)
fake_height = random.choice(fake_height_list)
fake_width = random.choice(fake_width_list)
fake_diameter = random.choice(fake_diameter_list)
fake_number = random.randint(1, 99)
# Create the new good entry
fake_good = Good.objects.get_or_create(name = fake_name,
height = fake_height,
width = fake_width,
diameter = fake_diameter,
number = fake_number)[0]
if __name__ == '__main__':
print('Populating script!')
populate(100)
print('Populating completed!')
print('Создано {} Моделей'.format(number_of_created_models))
Далее во views.py если форма валидная импортирую time, чтобы засечь, сколько выполняется скрипт. И одну строчку, которая вычисляет сколько элементов в модели.
def search(request):
first_form = forms.DoubleCheckForm(prefix="first_form")
second_form = forms.DoubleCheckForm(prefix="second_form")
context = {'first_form':first_form,
'second_form':second_form,
'result':None}
if request.method == 'POST':
first_form = forms.DoubleCheckForm(request.POST, prefix="first_form")
second_form = forms.DoubleCheckForm(request.POST, prefix="second_form")
if first_form.is_valid() and second_form.is_valid():
import time
start_time = time.time()
print('VALIDATION SUCCESS!')
first_user_height = first_form.cleaned_data.get('height')
first_user_width = first_form.cleaned_data.get('width')
first_user_diameter = first_form.cleaned_data.get('diameter')
second_user_height = second_form.cleaned_data.get('height')
second_user_width = second_form.cleaned_data.get('width')
second_user_diameter = second_form.cleaned_data.get('diameter')
#QuerySet of corresponding to first_form's parameters goods
g1 = Good.objects.all().filter(height = first_user_height, width = first_user_width, diameter = first_user_diameter)
#QuerySet of corresponding to second_form's parameters goods
g2 = Good.objects.all().filter(height = second_user_height, width = second_user_width, diameter = second_user_diameter)
l1 = [i.name for i in g1] #List that consists from good's name from first QuerySet
l2 = [c.name for c in g2] #List that consists from good's name from second QuerySet
cur_res = [name for name in l1 if name in l2] #Good's names that correspon both to first and second form's parameters
result = ''
for i in cur_res:
result += f'{i}, '
context['result'] = result[0:-2]
execution_time = time.time() - start_time
context['execution_time'] = execution_time
context['number_of_models'] = len(Good.objects.all())
return render(request, 'goods/search.html', context)
А в шаблоне добавил 2 template teg'а, чтобы вывести, сколько времени заняло вычисление, и сколько элементов в модели. search.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Search</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
</head>
<body>
<style>
h3{
font-size: 38px;
background: -webkit-linear-gradient(rgb(128, 128, 128), #3cdede); /*#eee, #333*/
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
h4{
font-size: 28px;
background: -webkit-linear-gradient(red, blue);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
</style>
<div style="background-image: linear-gradient(rgb(128, 128, 128), #3cdede)">
<div class="container">
<div class="jumbotron">
<h1 class="display-4">Hello, world!</h1>
<p class="lead">You can type two kind of parametres to look for goods that relate to this parametres and have the same name</p>
<hr class="my-4">
<form method="POST">
{% csrf_token %}
<div class="row">
<div class="col-lg-5 col-5">
{{first_form.as_ul}}
</div>
<div class="col-lg-5 col-5">
{{second_form.as_ul}}
</div>
</div>
<hr class="my-4">
<div style="text-align: center;" class="row">
<div class="col-lg-9 col-9">
<input type="submit" class="btn btn-primary btn-lg" value="Submit" style="height: 2.4em; width: 7em">
</div>
</div>
</form>
<br>
<div style="text-align: center;" class="row">
<div class="col-lg-9 col-9">
{% if result %}
<h3><em>Result: {{result}}</em></h3>
<h4>Execution takes {{execution_time}} seconds</h4>
<h4>we have {{number_of_models}} Different Goods in the storage</h4>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</body>
</html>
С 200, 500, 1000, 2000 элементов вычисление выполнялось от Пяти тысячных секунд до Пятнадцати сотых секунд

Далее решил затестить с 10000 элементов модели
Самое медленное это 23 Тысячных секунды

За пять дней регулярной работы над эти вопросом не нашёл как это можно сделать просто запросом без какой-либо обработки. To my mind это логично ведь ситуация нестандартная(используется не так часто), я думаю от 0,005 до 0,02 секунды, это очень быстро, а в обычном магазине будет максимум 100-150 различных товаров (по моему мнению) и это будет выполнятся ещё быстрей. 10000 товаров может быть только в таких магазинах как AliExpress, и всё равно всё выполняется очень быстро. Так вот хочу у вас спросить 0,005 или 0,02 секунды это много?
Я продолжаю изучать Веб-разработку, python, django, если найду какое-нибудь Омега-Интересное решение напишу. =)
Модель:
class Product(models.Model):
name = models.CharField(max_length=100)
width = models.IntegerField()
height = models.IntegerField()
diameter = models.IntegerField()
def __str__(self):
return f'{self.name} - {self.width}x{self.height}x{self.diameter}'
Пример данных в бд и список размеров, по которым будем искать:
In [61]: Product.objects.all()
Out[61]: <QuerySet [<Product: Product_1 - 10x20x30>, <Product: Product_1 - 15x25x35>, <Product: Product_2 - 15x25x35>, <Product: Product_3 - 15x25x35>, <Product: Product_3 - 10x20x30>, <Product: Product_4 - 1x1x1>]>
In [62]: sizes
Out[62]: ['10x20x30', '15x25x35']
Инструменты, чтобы отслеживать количество запросов к бд:
In [63]: from django.db import connection, reset_queries
In [64]: reset_queries()
In [65]: connection.queries
Out[65]: []
Параметры продукта находятся в разных полях модели, поэтому сгенерируем новое поле с размером продукта.
Для этого используем annotate() и функцию Concat(). И сразу отсекаем объекты, у которых значение product_size не в списке sizes:
In [66]: from django.db.models import CharField, Count, Value
In [67]: from django.db.models.functions import Concat
In [68]: products = Product.objects.annotate(
...: product_size=Concat('width', Value('x'), 'height', Value('x'), 'diameter',
...: output_field=CharField()
...: )
...: ).filter(product_size__in=sizes)
Теперь посчитаем сколько раз встречается каждое имя в products и оставим только те имена, количество которых равно длине списка sizes:
In [69]: names = [
...: p['name']
...: for p in products.values('name').annotate(count=Count('name'))
...: if p['count'] == len(sizes)
...: ]
In [70]: names
Out[70]: ['Product_1', 'Product_3']
И оставляем только те продукты имена которых есть в списке names:
In [71]: products.filter(name__in=names)
Out[71]: <QuerySet [<Product: Product_1 - 10x20x30>, <Product: Product_1 - 15x25x35>, <Product: Product_3 - 15x25x35>, <Product: Product_3 - 10x20x30>]>
Смотрим сколько сделано запросов к бд:
In [72]: connection.queries
Out[72]:
[{'sql': 'SELECT "products_product"."name", COUNT("products_product"."name") AS "count" FROM "products_product" WHERE COALESCE("products_product"."width", \'\') || COALESCE(COALESCE(\'x\', \'\') || COALESCE(COALESCE("products_product"."height", \'\') || COALESCE(COALESCE(\'x\', \'\') || COALESCE("products_product"."diameter", \'\'), \'\'), \'\'), \'\') IN (\'10x20x30\', \'15x25x35\') GROUP BY "products_product"."name"',
'time': '0.000'},
{'sql': 'SELECT "products_product"."id", "products_product"."name", "products_product"."width", "products_product"."height", "products_product"."diameter", COALESCE("products_product"."width", \'\') || COALESCE(COALESCE(\'x\', \'\') || COALESCE(COALESCE("products_product"."height", \'\') || COALESCE(COALESCE(\'x\', \'\') || COALESCE("products_product"."diameter", \'\'), \'\'), \'\'), \'\') AS "product_size" FROM "products_product" WHERE (COALESCE("products_product"."width", \'\') || COALESCE(COALESCE(\'x\', \'\') || COALESCE(COALESCE("products_product"."height", \'\') || COALESCE(COALESCE(\'x\', \'\') || COALESCE("products_product"."diameter", \'\'), \'\'), \'\'), \'\') IN (\'10x20x30\', \'15x25x35\') AND "products_product"."name" IN (\'Product_1\', \'Product_3\')) LIMIT 21',
'time': '0.000'}]
In [73]: len(connection.queries)
Out[73]: 2
Я не нашел способа избавиться от names = [p['name'] for p in ...].
Если придумаете как сделать все одним запросом в ORM, напишите комментарий к моему ответу, любопытно узнать есть ли решение.


