Bei der SQLAlchemy-Versionierung wird die Reihenfolge der Klassenimporte berücksichtigt

111

Ich folgte der Anleitung hier:

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

und sind auf ein Problem gestoßen. Ich habe meine Beziehungen wie folgt definiert:

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

mit Strings, so dass es nicht um die Importreihenfolge meiner Modellmodule geht. Dies alles funktioniert normalerweise einwandfrei, aber wenn ich das Versions-Meta verwende, wird der folgende Fehler angezeigt:

sqlalchemy.exc.InvalidRequestError: Beim Initialisieren von Mapper Mapper | MyClass | stuffs konnte der Ausdruck 'Trader' keinen Namen finden ("Name 'MyClass' ist nicht definiert"). Wenn dies ein Klassenname ist, sollten Sie diese Beziehung () zur Klasse hinzufügen, nachdem beide abhängigen Klassen definiert wurden.

Ich habe den Fehler aufgespürt, um:

  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

Ich habe das Problem behoben, indem ich den Versuch unternommen habe: außer Sachen in einem Lambda und sie alle auszuführen, nachdem alle Importe stattgefunden haben. Das funktioniert, scheint aber ein bisschen Unsinn zu sein. Irgendwelche Ideen, wie man das beheben kann, sind ein besserer Weg?

Vielen Dank!

Aktualisieren

Das Problem betrifft eigentlich nicht die Importreihenfolge. Das Versionsbeispiel ist so konzipiert, dass der Mapper eine Kompilierung im Costructor jeder versionierten Klasse erfordert. Die Kompilierung schlägt fehl, wenn verwandte Klassen noch nicht definiert sind. Bei zirkulären Beziehungen gibt es keine Möglichkeit, dies durch Ändern der Definitionsreihenfolge der zugeordneten Klassen zum Funktionieren zu bringen.

Update 2

Wie im obigen Update angegeben (ich wusste nicht, dass Sie die Beiträge anderer Leute hier bearbeiten können :)), ist dies wahrscheinlich auf Zirkelverweise zurückzuführen. In diesem Fall kann jemand meinen Hack nützlich finden (ich verwende ihn mit Turbogears) (Ersetzen Sie VersionedMeta und fügen Sie create_mappers global in history_meta hinzu.)

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

Dann können Sie in Ihren Modellen __init__.py wie folgt vorgehen

# 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]

Auf diese Weise werden die Mapper erst erstellt, nachdem alle Klassen definiert wurden.

Update 3 Etwas unabhängig, aber unter bestimmten Umständen ist ein doppelter Primärschlüsselfehler aufgetreten (zwei Änderungen an demselben Objekt auf einmal). Meine Problemumgehung bestand darin, einen neuen primären Schlüssel für die automatische Inkrementierung hinzuzufügen. Natürlich kann man mit MySQL nicht mehr als 1 haben, also musste ich den vorhandenen Schlüssel, der zum Erstellen der Verlaufstabelle verwendet wurde, vom Primärschlüssel trennen. Überprüfen Sie meinen Gesamtcode (einschließlich einer hist_id und der Beseitigung der Fremdschlüsseleinschränkung):

"""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
quelle
6
Verwenden Sie keine veralteten Namen. relation()sollte seinrelationship()
ThiefMaster
25
Fühlen Sie sich frei, einen Teil davon auf eine Antwort zu verschieben und diese zu akzeptieren.
Tobu
54
Kann jemand erklären, warum diese unbeantwortete Frage 48 positive Stimmen erhalten hat? Nur aus Interesse, da mir nicht klar ist, was hier los ist (kein Python-
Entwickler
4
@ Moak - Ich bin sicher, es liegt daran, dass es viele Leute gibt, die dem gleichen Leitfaden gefolgt sind. Kann jemand erklären, warum Ihr Kommentar 36 hat? Das ist die Magie von SO :)
alf
2
@alfonso und warum deine 1 haben :), das fängt an, wie Youtube's Daumen hoch zu sein :-)
Mouna Cheikhna

Antworten:

2

Ich habe das Problem behoben, indem ich den Versuch unternommen habe: außer Sachen in einem Lambda und sie alle auszuführen, nachdem alle Importe stattgefunden haben.

Toll!

l - '' '' '' - '' '' '' '' '' '' ''
quelle