SQLAlchemy: повторное сохранение уникального поля модели после попытки сохранить неуникальное значение - PullRequest
7 голосов
/ 25 сентября 2011

В моем приложении SQLAlchemy у меня есть следующая модель:

from sqlalchemy import Column, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import scoped_session, sessionmaker
from zope.sqlalchemy import ZopeTransactionExtension

DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension()))

class MyModel(declarative_base()):
    # ...
    label = Column(String(20), unique=True)

    def save(self, force=False):
        DBSession.add(self)
        if force:
            DBSession.flush()

Позже в коде для каждого нового MyModel объекта, который я хочу генерировать label случайным образом, и просто сгенерируйте его, если сгенерированное значениеуже существует в БД.
Я пытаюсь сделать следующее:

# my_model is an object of MyModel
while True:
    my_model.label = generate_label()
    try:
        my_model.save(force=True)
    except IntegrityError:
        # label is not unique - will do one more iteration
        # (*)
        pass
    else:
        # my_model saved successfully - exit the loop
        break

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

 InvalidRequestError: This Session's transaction has been rolled back due to a previous exception during flush. To begin a new transaction with this Session, first issue Session.rollback(). Original exception was: (IntegrityError) column url_label is not unique... 

Когда я добавляю DBSession.rollback() в позицию (*), я получаю это:

 ResourceClosedError: The transaction is closed

Что я должен сделать, чтобы правильно справиться с этой ситуацией?
Спасибо

Ответы [ 3 ]

5 голосов
/ 25 сентября 2011

Если ваш session объект откатывается по существу, вам нужно создать новый сеанс и обновить ваши модели, прежде чем вы сможете начать снова. И если вы используете zope.sqlalchemy, вы должны использовать transaction.commit() и transaction.abort() для управления вещами. Таким образом, ваш цикл будет выглядеть примерно так:

# you'll also need this import after your zope.sqlalchemy import statement
import transaction

while True:
    my_model.label = generate_label()
    try:
        transaction.commit()
    except IntegrityError:
        # need to use zope.sqlalchemy to clean things up
        transaction.abort()
        # recreate the session and re-add your object
        session = DBSession()
        session.add(my_model)
    else:
        break

Я вытащил использование объекта сеанса из метода save объекта здесь. Я не совсем уверен, как ScopedSession обновляется при использовании на уровне класса, как вы это сделали. Лично я думаю, что встраивание SqlAlchemy вещей в ваши модели не очень хорошо работает с подходом SqlAlchemy к unit of work.

Если ваш объект метки действительно является сгенерированным и уникальным значением, то я бы согласился с TokenMacGuy и просто использовал бы значение uuid.

Надеюсь, это поможет.

2 голосов
/ 07 августа 2012

Я столкнулся с подобной проблемой в моем веб-приложении, написанном на платформе Pyramid. Я нашел немного другое решение для этой проблемы.

while True:
    try:
        my_model.label = generate_label()
        DBSession.flush()
        break
    except IntegrityError:
        # Rollback will recreate session:
        DBSession.rollback()
        # if my_model was in db it must be merged:
        my_model = DBSession.merge(my_model)

Часть слияния имеет решающее значение, если my_model был сохранен ранее. Без слияния сессия будет пустой, поэтому flush не будет предпринимать никаких действий.

2 голосов
/ 25 сентября 2011

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

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

# Find a unique label
label = generate_label()
while DBsession.query(
        sqlalchemy.exists(sqlalchemy.orm.Query(Model)
                  .filter(Model.lable == label)
                  .statement)).scalar():
    label = generate_label()

# add that label to the model
my_model.label = label
DBSession.add(my_model)
DBSession.flush()

edit: еще один способ ответить на это - вы не должны автоматически повторять транзакцию;Вместо этого можно вернуть код состояния HTTP 307 Temporary Redirect (с небольшим количеством соли в перенаправленном URL), чтобы транзакция действительно началась заново.

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