Должен ли я обрабатывать номер заказа, используя идентификатор модели? - PullRequest
3 голосов
/ 13 января 2020

У меня есть личный сайт электронной коммерции.

Я использую идентификатор модели в качестве номера заказа. Просто потому, что казалось логика c, и я ожидал, что ID будет увеличиваться только на 1.

Однако я замечаю, что ID моих Орденов (моей модели Ордена) подскочил в два раза:

a) От 54 до 86 (32 различия).
b) От 99 до 132 (33 различия).

Не знаю почему, Не знаю, стоит ли мне использовать настраиваемое поле вместо идентификатора модели.

Я использую Django 3.0 и размещаю свой проект на Heroku.

models.py :

class Order(models.Model):
    ORDER_STATUS = (
        ('recibido_pagado', 'Recibido y pagado'),
        ('recibido_no_pagado', 'Recibido pero no pagado'),
        ('en_proceso', 'En proceso'),
        ('en_camino', 'En camino'),
        ('entregado', 'Entregado'),
        ('cancelado', 'Cancelado por no pagar' )
    )
    token = models.CharField(max_length=100, blank=True, null=True)
    first_name = models.CharField(max_length=50, blank=True, null=True)
    last_name = models.CharField(max_length=50, blank=True, null=True)
    phone_number = models.CharField(max_length=30, blank=True)
    total = models.DecimalField(max_digits=10, decimal_places=2)
    stickers_price = models.DecimalField(max_digits=10, decimal_places=2)
    discount = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal('0.00'))
    shipping_cost = models.DecimalField(max_digits=10, decimal_places=2)
    email = models.EmailField(max_length=250, blank = True, verbose_name= 'Correo electrónico')
    last_four = models.CharField(max_length=100, blank=True, null=True)
    created = models.DateTimeField(auto_now_add=True)
    shipping_address = models.CharField(max_length=100, blank=True, null=True)
    shipping_address1 = models.CharField(max_length=100, blank=True, null=True)
    reference = models.CharField(max_length=100, blank=True, null=True)
    shipping_department = models.CharField(max_length=100, blank=True, null=True)
    shipping_province = models.CharField(max_length=100, blank=True, null=True)
    shipping_district = models.CharField(max_length=100, blank=True, null=True)
    reason = models.CharField(max_length=400, blank=True, null=True, default='')
    status = models.CharField(max_length=20, choices=ORDER_STATUS, default='recibido_pagado')
    comments = models.CharField(max_length=400, blank=True, null=True, default='')
    cupon = models.ForeignKey('marketing.Cupons', blank=True, null=True, default=None, on_delete=models.SET_NULL)


    class Meta:
        db_table = 'Order'
        ordering = ['-created']

    def __str__(self):
        return str(self.id)

    def igv(self):
        igv = int(self.total) * 18/100
        return igv

    def shipping_date(self):
        shipping_date = self.created + datetime.timedelta(days=10)
        return shipping_date

    def deposit_payment_date(self):
        deposit_payment_date = self.created + datetime.timedelta(days=2)
        return 

Представление, создающее заказ:

@csrf_exempt
def cart_charge_deposit_payment(request):
    amount = request.POST.get('amount')
    email = request.user.email
    shipping_address = request.POST.get('shipping_address')
    shipping_cost = request.POST.get('shipping_cost')
    discount = request.POST.get('discount')
    stickers_price = request.POST.get('stickers_price')
    comments = request.POST.get('comments')
    last_four = 1111  
    transaction_amount = amount  

    first_name = request.user.first_name

    last_name = request.user.last_name

    phone_number = request.user.profile.phone_number

    current_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')

    shipping_address1 = request.user.profile.shipping_address1

    reference = request.user.profile.reference

    shipping_department = request.user.profile.shipping_department

    shipping_province = request.user.profile.shipping_province

    shipping_district = request.user.profile.shipping_district



    order_details = Order.objects.create(
        token='Random',
        first_name=first_name,
        last_name=last_name,
        phone_number=phone_number,
        email=email,  # Using email entered in Culqi module, NOT user.email. Could be diff.
        total=transaction_amount,
        stickers_price = stickers_price,
        discount = discount,
        shipping_cost=shipping_cost,
        last_four=last_four,
        created=current_time,
        shipping_address=shipping_address,
        shipping_address1=shipping_address1,
        reference=reference,
        shipping_department=shipping_department,
        shipping_province=shipping_province,
        shipping_district=shipping_district,
        status='recibido_no_pagado',
        cupon=cupon,
        comments=comments
    )

    ...

Ответы [ 4 ]

2 голосов
/ 21 января 2020

Это не будет отвечать на ваш вопрос напрямую, но все же может быть полезно для вас или кого-то с подобной проблемой.

С точки зрения целостности данных, удаление потенциально полезных данных, таких как заказ клиента в производстве, может быть действительно плохой идеей. Даже если вам не нужны эти данные в данный момент, вы можете прийти к определенному моменту в будущем, когда захотите проанализировать все свои заказы, даже не выполненные / отмененные.

То, что я хотел бы предложить здесь, это убедиться, что удаление не столь важных связанных моделей не приведет к удалению заказов. Этого легко добиться, передав аргумент PROTECT в поле ForeignKey. Это вызовет ProtectedError при попытке удалить связанную модель. Другие полезные опции: SET_NULL и SET_DEFAULT , чьи имена говорят сами за себя.

Следуя этому подходу, вам никогда не придется беспокоиться о сломанном счетчике идентификатора.

2 голосов
/ 22 января 2020

Давайте оставим Django, Python.

Это БД topi c. Скажем - вы начинаете транзакцию с новой строки в определенной таблице. Это означает новый идентификатор. Если вы совершите этот объем работы - новый идентификатор виден. В случае отката ID теряется. С точки зрения БД нет возможности повторно использовать этот номер.

Имейте в виду, что select max(id) + 1 - это плохая практика - что если две транзакции делают это одновременно?

Другой вариант - блокировка. Здесь я вижу 3 решения:

  1. Блокировка всех строк в таблице - это означает - время вставки зависит от размера таблицы :)

Как примечание. Если вы go блокируете одну за другой, обязательно отсортируйте все строки в таблице, чтобы убедиться в отсутствии тупиков. Допустим, вы используете Postgres, редактирование означает, что строка может быть перемещена в конце ... поэтому порядок зависит от того, что происходит с данными. В этом случае две транзакции могут блокировать строки в разном порядке, и взаимоблокировка - это вопрос времени. Во время тестов под малой нагрузкой - все идет нормально ...

Блокировка всей таблицы. Лучше, так как не зависит от строк, но вы также блокируете изменения.

Отдельная таблица для генераторов - у каждого генератора есть строка в этой таблице - вы блокируете эту строку, принимаете следующее значение, в конце строки транзакции освобождается.

На все точки. Это значит - вам нужны короткие транзакции. В веб-приложениях это общее правило. Просто убедитесь, что создание заказа легкое, а самые тяжелые вещи выполняются как отдельная транзакция. Почему? Блокировка снята в конце транзакции.

Надеюсь, это объясняет случай.

В Django. Давайте создадим модель:

class Custom_seq(models.Model):
    name = models.CharField(max_length=100, blank=False, null=False)
    last_number = models.IntegerField(default=0)

Запрос следующего идентификатора:

seq = Custom_seq.objects.filter(name='order sequence').select_for_update(no_wait=False).first()
new_order_id = seq.last_number + 1
seq.last_number = new_order_id
seq.save()

Почему это работает? Обратите внимание, что за один раз вы создаете один заказ. Он может быть зафиксирован - таким образом использован или отменен - ​​отменен ... оба случая поддерживаются.

2 голосов
/ 13 января 2020

Если вам нужна последовательная нумерация без дырок, вы не должны использовать автоматически генерируемое поле id Django в качестве номера заказа.

Чтобы гарантировать уникальность даже при параллельности Django создает последовательность базы данных который является объектом в базе данных, который производит новое значение каждый раз, когда к нему обращаются. Обратите внимание, что последовательность использует полученное значение, даже если оно нигде не сохранено в базе данных.

В таком случае происходит следующее: всякий раз, когда вы пытаетесь создать экземпляр, и эта операция завершается неудачно на уровне базы данных, число из последовательность потребляется в любом случае. Допустим, вы успешно создали свой первый ордер, он будет иметь идентификационный номер 1. Затем, допустим, вы пытаетесь создать второй ордер, но INSERT в базе данных не удается (например, для проверки целостности или чего-то еще) , После того, как вы успешно создадите третий ордер, вы ожидаете, что этот ордер имеет идентификационный номер 2, но на самом деле он будет иметь идентификационный номер 3, поскольку номер 2 был использован из последовательности, даже если он не был сохранен.

Так что нет, вы не можете использовать id, если вам нужно убедиться, что в ваших номерах заказов нет дыр.

Теперь, чтобы иметь последовательную нумерацию, вы можете просто добавить столбец

    order_number = models.PositiveIntegerField(unique=True, null=True)

Вопрос в том, как правильно установить его значение. Поэтому в идеальном мире, где нет параллелизма (два процесса выполняют запросы к одной и той же базе данных), вы можете просто получить максимальный номер заказа, добавить 1, а затем сохранить это значение в order_number. Дело в том, что если вы сделаете это наивно, у вас останутся дубликаты (на самом деле ошибки целостности, потому что unique=True предотвратит дубликаты).

Одним из способов решения этой проблемы будет , чтобы заблокировать вашу таблицу (см. Это ТАК вопрос) пока вы вычисляете и обновляете свой номер заказа.

Как я полагаю, вам все равно, что номер заказа точно отражает порядок, в котором были созданы заказы, а только то, что он является последовательным и без дыры, что вы можете сделать, это выполнить запрос, подобный следующему, внутри транзакции (при условии, что ваша Order модель находится в приложении orders django):

UPDATE orders_order SET order_number = (SELECT COALESCE(MAX(order_number), 0) FROM orders_order) + 1 WHERE id = [yourid] AND order_number IS NULL 

Теперь даже с этим запросом вы могут возникнуть проблемы с параллелизмом, , поскольку Django использует postgres уровень изоляции по умолчанию по умолчанию . Поэтому, чтобы сделать этот запрос безопасным, вам нужно изменить уровень изоляции. Обратитесь к этому вопросу SO , чтобы узнать, как получить два отдельных соединения с двумя разными уровнями изоляции. Чтобы сделать этот запрос безопасным, нужно установить для уровня изоляции значение SERIALIZABLE.

. Предполагая, что вы смогли решить проблему с уровнем изоляции, необходимо выполнить этот запрос

* 1036. *

В приведенном выше фрагменте предполагается, что у вас есть заказ, для которого вы хотите установить номер заказа в переменной с именем order. Если ваша изоляция правильная, тогда вы должны быть в безопасности.

Теперь есть третий вариант - - использовать select_for_update() в качестве механизма блокировки таблицы (хотя он не предназначен для этого, но для блокировки на уровне строк). Таким образом, идея проста, так же, как прежде, чем вы сначала создадите свой заказ, а затем обновите его, чтобы установить номер заказа. Таким образом, чтобы гарантировать, что у вас не будет дубликатов (aka IntegrityError) номеров заказов, вы должны выполнить запрос, который выберет все Заказы в вашей БД, а затем использовать select_for_update() следующим образом:

from django.db import transaction
with transaction.atomic():
    # This locks every row in orders_order until the end of the transaction
    Order.objects.all().select_for_update()  # pointless query just to lock the table
    max_on = Order.objects.aggregate(max_on=Max('order_number'))['max_on']
    Order.objects.filter(id=order.id).update(order_number=max_on + 1)

До тех пор, пока вы уверены, что у вас есть хотя бы 1 заказ, прежде чем вводить кодовый блок выше, И что вы всегда сначала делаете полный select_for_update(), тогда вы также должны быть в безопасности.

И вот способы, которыми я могу придумать, как решить последовательную нумерацию. Я бы хотел увидеть готовое решение для этого, но, к сожалению, я не знаю ни одного.

0 голосов
/ 22 января 2020

Это внутреннее поведение базы данных: https://www.postgresql.org/docs/current/functions-sequence.html

Важно

Чтобы не блокировать параллельные транзакции, которые получают числа из той же последовательности, операция nextval никогда не откатывается; то есть, как только значение было получено, оно считается использованным и больше не будет возвращено. Это верно, даже если окружающая транзакция позднее прерывается, или если вызывающий запрос заканчивается тем, что не использует значение. Например, команда INSERT с предложением ON CONFLICT будет вычислять кортеж для вставки, включая выполнение любых необходимых вызовов nextval, прежде чем обнаруживать любой конфликт, который заставил бы его следовать правилу ON CONFLICT. В таких случаях остаются неиспользованные «дыры» в последовательности назначенных значений. Таким образом, PostgreSQL объекты последовательностей не могут быть использованы для получения «бесщелевых» последовательностей.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...