2011-09-25 15 views
7

En mi aplicación SQLAlchemy tengo el siguiente modelo:SQLAlchemy: campo único del modelo de volver a guardar después de tratar de ahorrar no único valor

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() 

Más adelante en el código para cada nuevo MyModel objetos que desea generar label al azar , y solo regenere si el valor generado ya existe en DB.
estoy tratando de hacer lo siguiente:

# 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 

pero este error en caso de que generó por primera vez label no es única y save() llamados en el segundo (o posterior) de iteración:

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... 

cuando agrego DBSession.rollback() en la posición (*) me sale esto:

ResourceClosedError: The transaction is closed 

¿Qué debo hacer para manejar esta situación correctamente?
Gracias

+0

Debe asignar el valor de retorno de 'declarative_base()' a una variable. De lo contrario, experimentará problemas al crear más de un modelo, ya que podría tener diferentes clases base para ellos. – ThiefMaster

Respuesta

5

Si su objeto session retrocede esencialmente tiene que crear una nueva sesión y actualizar sus modelos antes de poder comenzar de nuevo. Y si usa zope.sqlalchemy, debe usar transaction.commit() y transaction.abort() para controlar cosas. Así que su bucle sería algo como esto:

# 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 

He tirado el uso del objeto de sesión de método del objeto save aquí. No estoy del todo seguro de cómo se actualiza el ScopedSession cuando se usa en el nivel de clase como lo ha hecho. Personalmente, creo que incrustar SqlAlchemy cosas dentro de sus modelos realmente no funciona bien con el enfoque de SqlAlchemy unit of work de ninguna manera.

Si su objeto de etiqueta es realmente un valor generado y único, entonces estaría de acuerdo con TokenMacGuy y simplemente use un valor de uuid.

Espero que ayude.

+0

ScopedSession utiliza un modelo de almacenamiento local de subprocesos; la sesión se invalida explícitamente (a través de 'ScopedSession.reset()'), pero eso generalmente es atendido por el marco que le proporciona la sesión, en el momento en que devuelve el control de la solicitud al marco. Es una conveniencia cuando el marco ayuda, pero un verdadero dolor de cabeza si no puede usar este tipo de modelo de hilo. A menos que esté diseñando un marco multiproceso, scopedesssion no es lo que quiere. – SingleNegationElimination

+0

@TokenMacGuy - Creo que lo que obtienes es que ScopedSession es esencialmente un objeto global en el hilo, por lo que se limpia explícitamente al final del ciclo de solicitud (por cierto, es 'ScopedSession.remove()' en 0.6/7) se convierte en una preocupación adicional. –

+0

Derecha; si tiene un hilo por unidad de trabajo, ScopedSession * podría * simplificar las cosas para los componentes que no pueden acoplarse fácilmente de otra manera; pero en muchos casos es bastante posible inyectar una sesión por medios que no sean un contenedor TLS global, o el hilo por vez simplemente no es posible, y ScopedSession no lo ayudará en absoluto. Esto parece ser un punto común de confusión para los principiantes; la sesión analizada hace que el UOW sea algo mágico, y la aplicación se vuelve difícil de depurar cuando el framework no administra la sesión de una manera que se alinea con la expectativa de los desarrolladores – SingleNegationElimination

2

bases de datos no tienen una forma consistente de que le dice qué una transacción falló, en una forma que sea accesible para la automatización. En general, no puede intentar la transacción y luego volver a intentarlo porque falló por algún motivo en particular.

Si conoce una condición que quiere evitar (como una restricción única), lo que tiene que hacer es verificar la restricción usted mismo. En sqlalchemy, eso va a ser algo como esto:

# 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() 

edición: Otra manera de responder a esto es que usted no debe intentar automáticamente la transacción; En su lugar, puede devolver un código de estado HTTP de 307 Temporary Redirect (con un poco de sal en la URL redirigida) para que la transacción se inicie realmente.

+0

Sí, pensé en verificar la restricción yo mismo, pero el problema es que no hay garantía de que el mismo valor que acabo de generar y que vaya a almacenar en DB no aparezca en ese DB entre los momentos de generación y almacenamiento. No pregunté, cómo saber por qué la transacción falló, estoy preguntando cómo "reparar" la sesión de la manera correcta. –

+0

Debería considerar usar una secuencia atómica o una clave global única; La mayoría de las bases de datos admiten algún tipo de secuencia (por ejemplo, MySQL tiene AUTOINCREMENT). Si esa no es una opción sensata para usted, puede usar una ID generada por el módulo 'uuid' para una alta probabilidad de una identificación única. – SingleNegationElimination

+0

Gracias, eche un vistazo al módulo 'uuid' –

2

Me enfrenté a un problema similar en mi aplicación web escrita en Pyramid framework. Encontré una solución un poco diferente para ese problema.

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) 

La parte de fusión es crucial si el my_model se almacenó anteriormente. Sin fusión, la sesión estaría vacía, por lo que Flush no tomaría ninguna medida.

+1

Solo una nota: si tienes una aplicación Pyramid que utiliza pyramid_tm es mejor usar transaction.abort() en lugar de DBSession.rollback() – Joril

Cuestiones relacionadas