web-dev-qa-db-fra.com

SQLAlchemy DetachedInstanceError avec attribut régulier (pas une relation)

Je viens de commencer à utiliser SQLAlchemy et à obtenir un DetachedInstanceError et je ne trouve pas beaucoup d’informations à ce sujet où que ce soit. J'utilise l'instance en dehors d'une session, il est donc naturel que SQLAlchemy ne puisse pas charger de relations si elles ne sont pas déjà chargées. Cependant, l'attribut auquel j'accède n'est pas une relation. J'ai trouvé des solutions telles que le chargement avec impatience, mais je ne peux pas appliquer cela car ce n'est pas une relation. J'ai même essayé de "toucher" cet attribut avant de fermer la session, mais cela n'empêche pas l'exception. Qu'est-ce qui pourrait causer cette exception pour une propriété non relationnelle, même après un accès réussi auparavant? Toute aide au débogage de ce problème est appréciée. En attendant, je vais essayer d’obtenir un scénario reproductible et de le mettre à jour ici.

Mise à jour: il s'agit du message d'exception réel avec quelques piles:

  File "/home/hari/bin/lib/python2.6/site-packages/SQLAlchemy-0.6.1-py2.6.Egg/sqlalchemy/orm/attributes.py", line 159, in __get__
    return self.impl.get(instance_state(instance), instance_dict(instance))
  File "/home/hari/bin/lib/python2.6/site-packages/SQLAlchemy-0.6.1-py2.6.Egg/sqlalchemy/orm/attributes.py", line 377, in get
    value = callable_(passive=passive)
  File "/home/hari/bin/lib/python2.6/site-packages/SQLAlchemy-0.6.1-py2.6.Egg/sqlalchemy/orm/state.py", line 280, in __call__
    self.manager.deferred_scalar_loader(self, toload)
  File "/home/hari/bin/lib/python2.6/site-packages/SQLAlchemy-0.6.1-py2.6.Egg/sqlalchemy/orm/mapper.py", line 2323, in _load_scalar_attributes
    (state_str(state)))
DetachedInstanceError: Instance <ReportingJob at 0xa41cd8c> is not bound to a Session; attribute refresh operation cannot proceed

Le modèle partiel ressemble à ceci:

metadata = MetaData()
ModelBase = declarative_base(metadata=metadata)

class ReportingJob(ModelBase):
    __table= 'reporting_job'

    job_id         = Column(BigInteger, Sequence('job_id_sequence'), primary_key=True)
    client_id      = Column(BigInteger, nullable=True)

Et le champ client_id est ce qui cause cette exception avec une utilisation comme celle ci-dessous:

Question:

    jobs = session \
            .query(ReportingJob) \
            .filter(ReportingJob.job_id == job_id) \
            .all()
    if jobs:
        # FIXME(Hari): Workaround for the attribute getting lazy-loaded.
        jobs[0].client_id
        return jobs[0]

C'est ce qui déclenche l'exception plus tard en dehors de l'étendue de la session:

        msg = msg + ", client_id: %s" % job.client_id
38
haridsv

J'ai trouvé la cause première en essayant de réduire le code à l'origine de l'exception. J'ai placé le même code d'accès d'attribut à différents endroits après la fermeture de la session et j'ai constaté qu'il ne causait aucun problème, immédiatement après la fermeture de la session de requête. Il se trouve que le problème commence à apparaître après la fermeture d'une nouvelle session ouverte pour mettre à jour l'objet. Une fois que j'ai compris que l'état de l'objet est inutilisable après une fermeture de session, j'ai été capable de trouver ce thread qui a abordé ce même problème. Deux solutions qui sortent du fil sont:

  • Garder une session ouverte (ce qui est évident)
  • Spécifiez expire_on_commit=False à sessionmaker().

La 3ème option consiste à définir manuellement expire_on_commit sur False sur la session une fois créée, quelque chose comme: session.expire_on_commit = False. J'ai vérifié que cela résout mon problème.

56
haridsv

Nous obtenions des erreurs similaires, même avec expire_on_commit défini sur False. En fin de compte, le fait que deux sessionmakers soient utilisés pour créer des sessions dans différentes requêtes a en réalité été causé. Je ne comprends pas vraiment ce qui se passait, mais si vous voyez cette exception avec expire_on_commit=False, assurez-vous de ne pas avoir deux sessionmakers initialisés. 

9
glyphobet

Pour lancer ma cause et ma solution dans le ring, j'utilise flask et flask-sqlalchemy pour gérer tout le contenu de ma session. Cela me convient lorsque je fais des choses via le site, mais lorsque vous faites des choses via une ligne de commande et des scripts, vous devez vous assurer que tout ce qui fonctionne en mode flacon doit le faire avec le contexte flask.

Donc, dans ma situation, j'avais besoin de récupérer des éléments dans une base de données (en utilisant flask-sqlalchemy), puis de les restituer aux modèles (en utilisant render_template de flask), puis de les envoyer par courrier électronique (en utilisant flask-mail).

En code, ce que j'avais fait était quelque chose comme:

def render_obj(db_obj):
  with app.app_context():
    return render_template('template_for_my_db_obj.html', db_obj=db_obj

def get_renders():
  my_db_objs = MyDbObj.query.all()

  renders = []
  for day, _db_objs in itertools.groupby(my_db_objs, MyDbObj.get_date):
    renders.extend(list(map(render_obj, _db_obj)))

  return renders

def email_report():
  renders = get_renders()
  report = '\n'.join(renders)

  with app.app_context():
    mail.send(Message('Subject', ['[email protected]'], html=report))

(Il s’agit essentiellement d’un pseudo-code, je faisais autre chose dans la section de regroupement)

Et quand je courais, je passais à travers le premier_db_obj, mais ensuite j'obtenais l'erreur après chaque exécution.

Le coupable? with app.app_context().

En gros, cela fait quelques choses quand vous sortez de ce contexte, y compris un peu de rafraîchissement des connexions à la base de données. L’une des choses qui en découle est l’élimination de la dernière session, à savoir la session à laquelle tous les my_db_objs étaient associés.

Il y a quelques options différentes pour des solutions, mais je suis allé avec une variante de,

def render_obj(db_obj):
  return render_template('template_for_my_db_obj.html', db_obj=db_obj

def get_renders():
  my_db_objs = MyDbObj.query.all()

  renders = []
  for day, _db_objs in itertools.groupby(my_db_objs, MyDbObj.get_date):
    renders.extend(list(map(render_obj, _db_obj)))

  return renders

def email_report():
  with app.app_context():
    renders = get_renders()
    report = '\n'.join(renders)

    mail.send(Message('Subject', ['[email protected]'], html=report))

Seulement 1 with app.app_context() qui les enveloppe tous. La principale chose que vous devez faire (si vous avez une configuration comme la mienne) est d’assurer que tout objet dB que vous utilisez soit "à l’intérieur" de tout app_context que vous utilisez. Si vous faites ce que j'ai fait lors de la première itération, tous vos objets dB perdront leur session, se terminant par DetachedInstanceError comme moi.

0
seaders

J'ai eu un problème similaire avec le DetachedInstanceError: Instance <> is not bound to a Session; 

La situation était assez simple, je passe la session et l’enregistrement à mettre à jour pour ma fonction et cela fusionnerait l’enregistrement et le validerait dans la base de données. Dans le premier exemple, j'obtiendrais l'erreur, car j'étais paresseux et pensais que je pouvais simplement renvoyer l'objet fusionné pour que mon enregistrement d'opération soit mis à jour (c'est-à-dire que sa valeur is_modified serait fausse). Il a renvoyé l'enregistrement mis à jour et is_modified était maintenant faux, mais les utilisations suivantes renvoyaient l'erreur. Je pense que cela a été aggravé par des enregistrements enfants similaires, mais pas tout à fait sûr.

        def EditStaff(self, session, record):
            try:
                    r = session.merge(record)
                    session.commit()
                    return r
            except:
                    return False

Après avoir longuement recherché Google et lu sur les sessions, etc., je me suis rendu compte que, puisque j'avais capturé l'instance avant la validation et que je l'avais renvoyée, le même enregistrement avait été renvoyé à cette fonction pour une autre opération d'édition/de validation, il avait perdu sa session.

Donc, pour résoudre ce problème, je demande simplement à la base de données pour l'enregistrement qui vient d'être mis à jour et le retourne pour le garder en session et marque sa valeur is_modified à false.

        def EditStaff(self, session, record):
            try:
                    session.merge(record)
                    session.commit()
                    r = self.GetStaff(session, record)
                    return r
            except:
                    return False

Définir le expire_on_commit=False a également évité l’erreur mentionnée ci-dessus, mais je ne pense pas que cela corrige réellement l’erreur et pourrait entraîner de nombreux autres problèmes, à l’OMI.

0
Frank

Quant à moi (novice), j'ai commis une erreur sur le retrait et fermé la session dans ma boucle, dans laquelle je boucle chaque ligne, effectue une opération et valide chaque fois. 

Donc, pour les débutants comme moi, vérifiez votre code avant de régler des choses comme expire_on_commit=False, cela pourrait vous conduire à un autre piège.

0
Rick