Функция значения по умолчанию для SQL Alchemy для имитации автоинкремента в уникальной группе записей типа "родители-потомки"

У меня есть небольшая проблема, которую я думаю, что она должна быть легко решена с помощью SQL Alchemy, но, похоже, я не могу понять ее правильно. У меня есть две таблицы, одна из которых является родительской, а другая дочерней. Для каждой дочерней записи требуется уникальный идентификатор, но только в контексте уникальной родительской записи.

Я использую подход декларативной базы.

Я устанавливаю отношения родитель-потомок, используя FK и функцию отношений. Я пытаюсь добиться чего-то вроде псевдо-автоинкрементной функции, которая будет искать максимальное значение CategoryID в группе уникальных имен Category и увеличивать его на единицу. Я пытался использовать различные функции по умолчанию, но проблема, с которой я сталкиваюсь, заключается в невозможности указать CategoryUniqueName во время вставки. Я не могу найти способ передать текущее значение CategoryItems.CategoryUniqueName, чтобы к поисковому запросу применялся правильный фильтр при попытке выбрать что-то вроде func.max(CategoryItems.CategoryID). Если я жестко закодирую запрос, он работает просто отлично. Это то, что я думаю, должно работать, но, опять же, я не могу найти способ указать уникальное значение для фильтра.

unique_group='my_group'
result=con.query(func.max(CategoryItems.CategoryID)).filter(and_(
        CategoryItems.CategoryUniqueName==unique_group, 
    )).one()

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

class Category(Base):
    __tablename__ = 'parent_table'
    __table_args__ = {'mysql_engine':'InnoDB', 'useexisting':True}

    CategoryUniqueName = Column(Unicode(255), primary_key=True)
    CategoryGroupName = Column(Unicode(255), nullable=False)
    CategoryGroupMemo = Column(UnicodeText)
    SortOrder = Column(Integer, index=True)
    IsLocked = Column(Boolean, default=0)

class CategoryItems(Base):
    __tablename__ = 'child_table'
    __table_args__ = {'mysql_engine':'InnoDB', 'useexisting':True}

    CategoryUniqueName = Column(Unicode(255), ForeignKey(Category.CategoryUniqueName), primary_key=True)
    CategoryID = Column(Integer, primary_key=True, autoincrement=False)
    CategoryName = Column(Unicode(255), nullable=False, index=True)
    CategoryMemo = Column(UnicodeText)
    CategoryImage = Column(Unicode(255))
    CategoryFlex1 = Column(Unicode(255), index=True)
    CategoryFlex2 = Column(Unicode(255), index=True)
    CategoryFlex3 = Column(Unicode(255), index=True)
    SortOrder = Column(Integer, index=True)

    category_group = relation(
        Category, 
        backref=backref(
            'items', 
            order_by=SortOrder, 
            collection_class=ordering_list('SortOrder'), 
            cascade="all, delete, delete-orphan"
    ))

3 ответа

Решение

Я вижу 3 пути:

  1. Самый очевидный и хорошо документированный. Создайте расширение картографа с before_insert() хук, заменяющий вставленный параметр.
  2. Передать функцию как default аргумент. Эта функция вызывается с context Параметр со всеми необходимыми данными: context.compiled_parameters[0]['CategoryUniqueName'], context.connection,
  3. Проходить FetchedValue() в server_default параметр и использовать триггер, чтобы сделать работу на стороне сервера.

Все эти решения имеют состояние гонки, упомянутое ddaa. В случае состояния гонки ваш код не будет нарушать состояние базы данных, но сбоит, за исключением случаев, когда первичный ключ определен правильно (это не так для вашего кода!). В некоторых редких случаях может быть допустимо сбои в работе некоторых приложений (показывать 500 страниц в веб-приложении).

Обратите внимание, что вы определили CategoryID в качестве первичного ключа. Это не позволит повторно использовать один и тот же номер для разных значений CategoryUniqueName колонка. Вы должны изменить его на составной первичный индекс для 2 столбцов.

Спасибо за понимание Денис, вы на месте. Я играл с обоими вариантами 1 и 2, и они работают очень хорошо. Параметр context был ключом для варианта 2. Я не знал, что он передается автоматически. Одна вещь, которую я заметил, была в том, что вариант 1 вводит условие гонки, даже если один пользователь отправляет несколько записей. Я думаю, что это как-то связано с флешем и экономит время. Однако вариант 2 работает блестяще.

Эта маленькая функция теперь вызывается из параметра по умолчанию:

def getNextId(context):
    unique_name=context.compiled_parameters[0]['CategoryUniqueName']
    sql = """
        SELECT MAX(CategoryID)
        FROM child_table
        WHERE CategoryUniqueName='%s'""" % (unique_name, )

    result = context.connection.execute(sql).fetchone()[0]
    if result > 0:
         return result + 1
    else:
        return 1 

Итак, чего вы пытаетесь добиться, чтобы каждый набор CategoryItems с разными CategoryUniqueName имел свой CategoryId с автоматическим увеличением отдельно?

Если это правильно, ваш текущий подход (получить текущий максимум CategoryId в подмножестве CategoryItems, к которому вы добавляете) не работает. Он имеет внутреннее состояние гонки: одновременные вставки будут использовать один и тот же CategoryId.

Вы действительно нуждаетесь в увеличении вашего CategoryId отдельно? Почему бы просто не использовать обычную функцию автоинкремента. Последовательность CategoryId для данного CategoryUniqueName будет иметь дыры, но действительно ли это проблема?

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

Другие вопросы по тегам