SQLAlchemy: повторное сохранение уникального поля модели после попытки сохранить не уникальное значение
в моем приложении 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 ответов
если 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
значение.
надеюсь, это поможет.
базы данных не имеют последовательного способа сообщить вам почему сбой транзакции в форме, доступной для автоматизации. Обычно вы не можете попробовать транзакцию, а затем повторить попытку, потому что она не удалась по какой-то особой причине.
Если вы знаете условие, которое хотите обойти (например, уникальное ограничение), вам нужно проверить ограничение самостоятельно. В 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) так, что сделка действительно начали.
я столкнулся с аналогичной проблемой в моем webapp, написанном в Pyramid framework. Я нашел немного другое решение этой проблемы.
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 не будет предпринимать никаких действий.