La gestion des versions de SQLAlchemy se soucie de l'ordre d'importation des classes

111

Je suivais le guide ici:

http://www.sqlalchemy.org/docs/orm/examples.html?highlight=versioning#versioned-objects

et ont rencontré un problème. J'ai défini mes relations comme:

generic_ticker = relation('MyClass', backref=backref("stuffs"))

avec des chaînes de sorte qu'il ne se soucie pas de l'ordre d'importation de mes modules de modèle. Tout cela fonctionne bien normalement, mais lorsque j'utilise la méta de contrôle de version, j'obtiens l'erreur suivante:

sqlalchemy.exc.InvalidRequestError: Lors de l'initialisation du mappeur Mapper | MyClass | stuffs, l'expression 'Trader' n'a pas réussi à localiser un nom ("le nom 'MyClass' n'est pas défini"). S'il s'agit d'un nom de classe, envisagez d'ajouter cette relation () à la classe une fois que les deux classes dépendantes ont été définies.

J'ai retracé l'erreur pour:

  File "/home/nick/workspace/gm3/gm3/lib/history_meta.py", line 90, in __init__
    mapper = class_mapper(cls)
  File "/home/nick/venv/tg2env/lib/python2.6/site-packages/sqlalchemy/orm/util.py", line 622, in class_mapper
    mapper = mapper.compile()

class VersionedMeta(DeclarativeMeta):
    def __init__(cls, classname, bases, dict_):
        DeclarativeMeta.__init__(cls, classname, bases, dict_)

        try:
            mapper = class_mapper(cls)
            _history_mapper(mapper)
        except UnmappedClassError:
            pass

J'ai résolu le problème en mettant le try: sauf des trucs dans un lambda et en les exécutant tous après que toutes les importations aient eu lieu. Cela fonctionne mais semble un peu nul, des idées sur la façon de résoudre ce problème sont-elles une meilleure façon?

Merci!

Mettre à jour

Le problème ne concerne pas réellement l'ordre d'importation. L'exemple de gestion des versions est conçu de telle sorte que mapper nécessite la compilation dans costructor de chaque classe versionnée. Et la compilation échoue lorsque les classes associées ne sont pas encore définies. Dans le cas de relations circulaires, il n'y a aucun moyen de le faire fonctionner en changeant l'ordre de définition des classes mappées.

Mise à jour 2

Comme l'indique la mise à jour ci-dessus (je ne savais pas que vous pouviez modifier les messages d'autres personnes ici :)), cela est probablement dû à des références circulaires. Dans ce cas, quelqu'un trouvera peut-être mon hack utile (je l'utilise avec turbogears) (Remplacez VersionedMeta et ajoutez create_mappers global dans history_meta)

create_mappers = []
class VersionedMeta(DeclarativeMeta):
    def __init__(cls, classname, bases, dict_):
        DeclarativeMeta.__init__(cls, classname, bases, dict_)
        #I added this code in as it was crashing otherwise
        def make_mapper():
            try:
                mapper = class_mapper(cls)
                _history_mapper(mapper)
            except UnmappedClassError:
                pass

        create_mappers.append(lambda: make_mapper())

Ensuite, vous pouvez faire quelque chose comme ce qui suit dans vos modèles __init__.py

# Import your model modules here.
from myproj.lib.history_meta import create_mappers

from myproj.model.misc import *
from myproj.model.actor import *
from myproj.model.stuff1 import *
from myproj.model.instrument import *
from myproj.model.stuff import *

#setup the history
[func() for func in create_mappers]

De cette façon, il crée les mappeurs seulement après que toutes les classes ont été définies.

Mise à jour 3 Légèrement sans rapport mais je suis tombé sur une erreur de clé primaire en double dans certaines circonstances (commettre 2 modifications sur le même objet en une seule fois). Ma solution de contournement a été d'ajouter une nouvelle clé primaire à incrémentation automatique. Bien sûr, vous ne pouvez pas en avoir plus d'un avec mysql, j'ai donc dû supprimer la clé primaire des éléments existants utilisés pour créer la table d'historique. Consultez mon code global (y compris un hist_id et se débarrasser de la contrainte de clé étrangère):

"""Stolen from the offical sqlalchemy recpies
"""
from sqlalchemy.ext.declarative import DeclarativeMeta
from sqlalchemy.orm import mapper, class_mapper, attributes, object_mapper
from sqlalchemy.orm.exc import UnmappedClassError, UnmappedColumnError
from sqlalchemy import Table, Column, ForeignKeyConstraint, Integer
from sqlalchemy.orm.interfaces import SessionExtension
from sqlalchemy.orm.properties import RelationshipProperty
from sqlalchemy.types import DateTime
import datetime
from sqlalchemy.orm.session import Session

def col_references_table(col, table):
    for fk in col.foreign_keys:
        if fk.references(table):
            return True
    return False

def _history_mapper(local_mapper):
    cls = local_mapper.class_

    # set the "active_history" flag
    # on on column-mapped attributes so that the old version
    # of the info is always loaded (currently sets it on all attributes)
    for prop in local_mapper.iterate_properties:
        getattr(local_mapper.class_, prop.key).impl.active_history = True

    super_mapper = local_mapper.inherits
    super_history_mapper = getattr(cls, '__history_mapper__', None)

    polymorphic_on = None
    super_fks = []
    if not super_mapper or local_mapper.local_table is not super_mapper.local_table:
        cols = []
        for column in local_mapper.local_table.c:
            if column.name == 'version':
                continue

            col = column.copy()
            col.unique = False

            #don't auto increment stuff from the normal db
            if col.autoincrement:
                col.autoincrement = False
            #sqllite falls over with auto incrementing keys if we have a composite key
            if col.primary_key:
                col.primary_key = False

            if super_mapper and col_references_table(column, super_mapper.local_table):
                super_fks.append((col.key, list(super_history_mapper.base_mapper.local_table.primary_key)[0]))

            cols.append(col)

            if column is local_mapper.polymorphic_on:
                polymorphic_on = col

        #if super_mapper:
        #    super_fks.append(('version', super_history_mapper.base_mapper.local_table.c.version))

        cols.append(Column('hist_id', Integer, primary_key=True, autoincrement=True))
        cols.append(Column('version', Integer))
        cols.append(Column('changed', DateTime, default=datetime.datetime.now))

        if super_fks:
            cols.append(ForeignKeyConstraint(*zip(*super_fks)))

        table = Table(local_mapper.local_table.name + '_history', local_mapper.local_table.metadata,
                      *cols, mysql_engine='InnoDB')
    else:
        # single table inheritance.  take any additional columns that may have
        # been added and add them to the history table.
        for column in local_mapper.local_table.c:
            if column.key not in super_history_mapper.local_table.c:
                col = column.copy()
                super_history_mapper.local_table.append_column(col)
        table = None

    if super_history_mapper:
        bases = (super_history_mapper.class_,)
    else:
        bases = local_mapper.base_mapper.class_.__bases__
    versioned_cls = type.__new__(type, "%sHistory" % cls.__name__, bases, {})

    m = mapper(
            versioned_cls, 
            table, 
            inherits=super_history_mapper, 
            polymorphic_on=polymorphic_on,
            polymorphic_identity=local_mapper.polymorphic_identity
            )
    cls.__history_mapper__ = m

    if not super_history_mapper:
        cls.version = Column('version', Integer, default=1, nullable=False)

create_mappers = []

class VersionedMeta(DeclarativeMeta):
    def __init__(cls, classname, bases, dict_):
        DeclarativeMeta.__init__(cls, classname, bases, dict_)
        #I added this code in as it was crashing otherwise
        def make_mapper():
            try:
                mapper = class_mapper(cls)
                _history_mapper(mapper)
            except UnmappedClassError:
                pass

        create_mappers.append(lambda: make_mapper())

def versioned_objects(iter):
    for obj in iter:
        if hasattr(obj, '__history_mapper__'):
            yield obj

def create_version(obj, session, deleted = False):
    obj_mapper = object_mapper(obj)
    history_mapper = obj.__history_mapper__
    history_cls = history_mapper.class_

    obj_state = attributes.instance_state(obj)

    attr = {}

    obj_changed = False

    for om, hm in zip(obj_mapper.iterate_to_root(), history_mapper.iterate_to_root()):
        if hm.single:
            continue

        for hist_col in hm.local_table.c:
            if hist_col.key == 'version' or hist_col.key == 'changed' or hist_col.key == 'hist_id':
                continue

            obj_col = om.local_table.c[hist_col.key]

            # get the value of the
            # attribute based on the MapperProperty related to the
            # mapped column.  this will allow usage of MapperProperties
            # that have a different keyname than that of the mapped column.
            try:
                prop = obj_mapper.get_property_by_column(obj_col)
            except UnmappedColumnError:
                # in the case of single table inheritance, there may be 
                # columns on the mapped table intended for the subclass only.
                # the "unmapped" status of the subclass column on the 
                # base class is a feature of the declarative module as of sqla 0.5.2.
                continue

            # expired object attributes and also deferred cols might not be in the
            # dict.  force it to load no matter what by using getattr().
            if prop.key not in obj_state.dict:
                getattr(obj, prop.key)

            a, u, d = attributes.get_history(obj, prop.key)

            if d:
                attr[hist_col.key] = d[0]
                obj_changed = True
            elif u:
                attr[hist_col.key] = u[0]
            else:
                # if the attribute had no value.
                attr[hist_col.key] = a[0]
                obj_changed = True

    if not obj_changed:
        # not changed, but we have relationships.  OK
        # check those too
        for prop in obj_mapper.iterate_properties:
            if isinstance(prop, RelationshipProperty) and \
                attributes.get_history(obj, prop.key).has_changes():
                obj_changed = True
                break

    if not obj_changed and not deleted:
        return

    attr['version'] = obj.version
    hist = history_cls()
    for key, value in attr.iteritems():
        setattr(hist, key, value)

    obj.version += 1
    session.add(hist)

class VersionedListener(SessionExtension):
    def before_flush(self, session, flush_context, instances):
        for obj in versioned_objects(session.dirty):
            create_version(obj, session)
        for obj in versioned_objects(session.deleted):
            create_version(obj, session, deleted = True)
Nick Holden
la source
6
N'utilisez pas de noms obsolètes; relation()devrait êtrerelationship()
ThiefMaster
25
N'hésitez pas à en déplacer une partie vers une réponse et à l'accepter.
Tobu
54
Quelqu'un peut-il expliquer pourquoi cette question sans réponse a obtenu 48 votes positifs? Juste par intérêt car je ne sais pas ce qui se passe ici (pas un développeur python)
Moak
4
@Moak - Je suis sûr que c'est parce qu'il y a beaucoup de gens qui ont suivi le même guide. Quelqu'un peut-il expliquer pourquoi votre commentaire a eu 36? C'est la magie de SO :)
alf
2
@alfonso et pourquoi le vôtre en a 1 :), cela commence à être comme le pouce de YouTube :-)
Mouna Cheikhna

Réponses:

2

J'ai résolu le problème en mettant le try: sauf des trucs dans un lambda et en les exécutant tous après que toutes les importations aient eu lieu.

Génial!

l --''''''--------- '' '' '' '' '' '
la source