El control de versiones de SQLAlchemy se preocupa por el orden de importación de clases

111

Estaba siguiendo la guía aquí:

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

y se ha encontrado con un problema. He definido mis relaciones como:

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

con cadenas, por lo que no le importa el orden de importación de los módulos de mi modelo. Todo esto funciona bien normalmente, pero cuando uso el meta de control de versiones, aparece el siguiente error:

sqlalchemy.exc.InvalidRequestError: Al inicializar el asignador Mapper | MyClass | stuffs, la expresión 'Trader' no pudo localizar un nombre ("el nombre 'MyClass' no está definido"). Si se trata de un nombre de clase, considere agregar esta relación () a la clase después de que se hayan definido ambas clases dependientes.

Rastreé el error a:

  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

Solucioné el problema poniendo el try: except stuff en un lambda y ejecutándolos todos después de que se hayan realizado todas las importaciones. Esto funciona pero parece un poco basura, ¿alguna idea de cómo solucionarlo es una mejor manera?

¡Gracias!

Actualizar

El problema no se trata realmente de un pedido de importación. El ejemplo de control de versiones está diseñado de manera que el asignador requiera la compilación en el costructor de cada clase versionada. Y la compilación falla cuando las clases relacionadas aún no están definidas. En el caso de relaciones circulares, no hay forma de hacer que funcione cambiando el orden de definición de las clases mapeadas.

Actualización 2

Como dice la actualización anterior (no sabía que podía editar las publicaciones de otras personas aquí :)), esto probablemente se deba a referencias circulares. En cuyo caso, puede ser que alguien encuentre útil mi truco (lo estoy usando con turbogears) (Reemplace VersionedMeta y agregue create_mappers global en 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())

Entonces puedes hacer algo como lo siguiente en tus modelos __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 esa forma, crea los mapeadores solo después de que se hayan definido todas las clases.

Actualización 3 Ligeramente no relacionado, pero encontré un error de clave primaria duplicada en algunas circunstancias (cometiendo 2 cambios en el mismo objeto de una vez). Mi solución ha sido agregar una nueva clave primaria de incremento automático. Por supuesto, no puede tener más de 1 con mysql, así que tuve que eliminar la clave principal de las cosas existentes utilizadas para crear la tabla de historial. Consulte mi código general (incluido un hist_id y deshacerse de la restricción de clave externa):

"""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
fuente
6
No utilice nombres obsoletos; relation()debería serrelationship()
ThiefMaster
25
Siéntase libre de mover parte de esto a una respuesta y acéptela.
Tobu
54
¿Alguien puede explicar por qué esta pregunta sin respuesta obtuvo 48 votos a favor? Solo por interés, ya que no tengo claro qué está pasando aquí (no un desarrollador de Python)
Moak
4
@Moak: estoy seguro de que es porque hay muchas personas que siguieron la misma guía. ¿Alguien puede explicar por qué su comentario obtuvo 36? Esa es la magia de SO :)
alf
2
@alfonso y por qué el tuyo obtuvo 1 :), esto está comenzando a ser como el pulgar hacia arriba de youtube :-)
Mouna Cheikhna

Respuestas:

2

Solucioné el problema poniendo el try: except stuff en un lambda y ejecutándolos todos después de que se hayan realizado todas las importaciones.

¡Excelente!

l --''''''--------- '' '' '' '' '' '
fuente