2011-09-25 3 views
7

Dans mon application SQLAlchemy je le modèle suivant:SQLAlchemy: Re sauver champ unique de modèle après avoir essayé de sauver la valeur non unique

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

plus tard dans le code pour tous les nouveaux MyModel objets que je veux générer label au hasard et juste le régénérer si la valeur générée existe déjà dans DB.
Je suis en train de faire ce qui suit:

# 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 

mais obtenir cette erreur dans le cas lors de la première généré label n'est pas unique et save() appelé au deuxième (ou version ultérieure) itération:

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

quand j'ajoute DBSession.rollback() dans la position (*) Je reçois ceci:

ResourceClosedError: The transaction is closed 

Que dois-je faire gérer cette situation correctement?
Merci

+0

Vous devez affecter la valeur de retour de 'declarative_base()' à une variable. Sinon, vous rencontrerez des problèmes lors de la création de plusieurs modèles car vous pourriez avoir différentes classes de base pour eux. – ThiefMaster

Répondre

5

Si votre objet session est annulé, vous devez créer une nouvelle session et actualiser vos modèles avant de pouvoir recommencer. Et si vous utilisez zope.sqlalchemy vous devriez utiliser transaction.commit() et transaction.abort() pour contrôler les choses. Donc, votre boucle ressemblerait à quelque chose comme ceci:

# 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 

J'ai tiré l'utilisation de l'objet de la session sur la méthode save de l'objet ici. Je ne suis pas tout à fait sûr comment le ScopedSession se rafraîchit lorsqu'il est utilisé au niveau de la classe comme vous l'avez fait. Personnellement, je pense que l'intégration de SqlAlchemy choses à l'intérieur de vos modèles ne fonctionne pas vraiment bien avec l'approche unit of work de SqlAlchemy aux choses de toute façon.

Si votre objet étiquette est vraiment une valeur générée et unique, alors je serais d'accord avec TokenMacGuy et utiliser simplement une valeur uuid.

Espérons que ça aide.

+0

ScopedSession utilise un modèle de stockage local de threads; la session est invalidée explicitement (via 'ScopedSession.reset()'), mais cela est généralement pris en charge par le framework qui vous donne la session, au moment où vous retournez le contrôle de la requête au framework. C'est une commodité lorsque le cadre vous aide, mais un vrai casse-tête si vous ne pouvez pas utiliser ce type de modèle de thread. Sauf si vous concevez un framework mulithread, scopedesssion n'est pas ce que vous voulez. – SingleNegationElimination

+0

@TokenMacGuy - Je pense que ce que vous obtenez est que ScopedSession est essentiellement un objet global sur le thread, le nettoyant ainsi explicitement à la fin du cycle de la requête (btw c'est ScopedSession.remove() 'dans 0.6/7) devient une préoccupation supplémentaire. –

+0

Droite; Si vous avez un thread par unité de travail, ScopedSession * pourrait * simplifier les choses pour les composants qui ne peuvent pas facilement être couplés d'une autre manière; mais dans de nombreux cas, il est tout à fait possible d'injecter une session par un moyen autre qu'un conteneur TLS global, ou bien le flux de threads n'est tout simplement pas possible, et ScopedSession ne vous aidera pas du tout. Cela semble être un point commun de confusion pour les débutants; la session étendue rend l'UOW quelque peu magique, et l'application devient difficile à déboguer lorsque le framework ne gère pas la session d'une manière qui correspond à l'attente des développeurs. – SingleNegationElimination

2

Les bases de données ne sont pas une manière cohérente de vous dire pourquoi une transaction a échoué, sous une forme accessible à l'automatisation. Vous ne pouvez généralement pas essayer la transaction, puis réessayez car il a échoué pour une raison particulière.

Si vous connaissez une condition que vous voulez contourner (comme une contrainte unique), vous devez vérifier la contrainte vous-même. Dans sqlalchemy, cela va ressembler à quelque chose comme ceci:

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

modifier: Une autre façon de répondre à cette question est que vous ne devriez pas réessayer automatiquement la transaction; Vous pouvez à la place renvoyer un code d'état HTTP de 307 Temporary Redirect (avec un peu de sel dans l'URL redirigée) afin que la transaction soit réellement lancée.

+0

Oui, j'ai pensé à vérifier la contrainte moi-même, mais le problème est qu'il n'y a aucune garantie que la même valeur que je viens de générer et va stocker dans DB n'apparaîtra pas dans cette DB entre les moments de génération et de stockage. Je n'ai pas demandé, comment savoir pourquoi la transaction a échoué, je demande comment "réparer" la session de la bonne façon. –

+0

Vous devriez envisager d'utiliser une séquence atomique ou une clé globalement unique; La plupart des bases de données supportent une sorte de séquence (par exemple, MySQL a AUTOINCREMENT). Si ce n'est pas une option raisonnable pour vous, vous pouvez utiliser un ID généré par le module 'uuid' pour une probabilité élevée d'un identifiant unique. – SingleNegationElimination

+0

Merci, jetterai un coup d'oeil sur le module 'uuid' –

2

J'ai rencontré un problème similaire dans ma webapp écrite dans Pyramid. J'ai trouvé une solution un peu différente pour ce problème.

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 partie de fusion est cruciale si le modèle my_model a été stocké auparavant. Sans fusionner la session serait vide donc flush ne prendrait aucune action.

+1

Juste une note: si vous avez une application Pyramid qui utilise pyramid_tm il est préférable d'utiliser transaction.abort() au lieu de DBSession.rollback() – Joril

Questions connexes