Il controllo delle versioni di SQLAlchemy si preoccupa dell’ordine di importazione della classe

| | | | | | | | | |

Stavo seguendo la guida qui:

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

e ho riscontrato un problema. Ho definito le mie relazioni come:

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

con stringhe così non "Non mi interessa l'ordine di importazione dei moduli del mio modello. Funziona tutto normalmente, ma quando utilizzo il meta di controllo delle versioni ottengo il seguente errore:

sqlalchemy.exc.InvalidRequestError: durante l'inizializzazione del mapper Mapper|MyClass|stuffs, l'espressione "Trader" non è riuscita a individuare un nome ("il nome "MyClass" non è definito"). Se questo è il nome di una classe, considera di aggiungere questa relazione() alla classe dopo che entrambe le classi dipendenti sono state definite .

Ho rintracciato l'errore in:

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

class VersionedMeta(DeclarativeMeta): def __init__(cls, cl assname, bases, dict_): DeclarativeMeta.__init__(cls, classname, bases, dict_) try: mapper = class_mapper(cls) _history_mapper(mapper) eccetto UnmappedClassError: pass 

Ho risolto il problema provando: tranne le cose in una lambda ed eseguendole tutte dopo che tutte le importazioni sono avvenute. Funziona ma sembra un po' spazzatura, qualche idea su come risolverlo è un modo migliore?

Grazie!

Aggiorna

Il problema in realtà non riguarda l'ordine di importazione. L'esempio di controllo delle versioni è progettato in modo tale che il mapper richieda la compilazione nel costruttore di ciascuna classe con versione. E la compilazione non riesce quando le classi correlate non sono ancora definite. In caso di relazioni circolari non c'è modo di farlo funzionare cambiando l'ordine di definizione delle classi mappate.

Aggiornamento 2

Come afferma l'aggiornamento sopra ( Non sapevo che potevi modificare i post di altre persone qui :)) questo è probabilmente dovuto a riferimenti circolari. Nel qual caso qualcuno troverà utile il mio hack (lo sto usando con turbogears) (sostituisci VersionedMeta e aggiungi create_mappers global in history_meta)

create_mappers = [] class VersionedMeta(DeclarativeMeta) : def __init__(cls, classname, bases, dict_): DeclarativeMeta.__init__(cls, classname, bases, dict_) #Ho aggiunto questo codice perché si bloccava altrimenti def make_mapper(): try: mapper = class_mapper(cls) _history_mapper (mapper) eccetto UnmappedClassError: pass create_mappers.append(lambda: make_mapper()) 

Quindi puoi fare qualcosa di simile nei tuoi modelli __init__.py

# Importa qui i moduli del tuo modello da myproj.lib.history_meta import create_mappers da myproj.model.misc import * da myproj.model.actor import * da myproj.model.stuff1 import * da myproj.model.instrument import * da myproj.model.stuff import * #setup the history [func() for func in create_mappers] 

In questo modo vengono creati solo i mapper dopo che tutte le classi sono state definite.

Aggiornamento 3 Leggermente non correlato ma in alcune circostanze mi sono imbattuto in un errore di chiave primaria duplicato (commettendo 2 modifiche allo stesso oggetto in una volta sola) . La mia soluzione è stata quella di aggiungere una nuova chiave primaria a incremento automatico. Ovviamente non puoi averne più di 1 con mysql, quindi ho dovuto de-primary key le cose esistenti utilizzate per creare la tabella della cronologia. Controlla il mio codice generale (incluso hist_id e sbarazzarsi del vincolo della chiave esterna):

"""Rubato dalle ricette ufficiali di sqlalchemy """ da sqlalchemy.ext.declarative import DeclarativeMeta da sqlalchemy.orm import mapper, class_mapper, attributi, object_mapper da sqlalchemy.orm.exc import UnmappedClassError, UnmappedColumnError from sqlalchemy import Table, Column, ForeignKeyConstraint, Integer from sqlalchemy.orm.interfaces import SessionExtension da sqlalchemy.orm.properties import RelationshipProperty da sqlalchemy.types import DateTime import datetime da 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_ # imposta il flag "active_history" # su su mappato a colonne ttributes in modo che la vecchia versione # delle informazioni sia sempre caricata (attualmente la imposta su tutti gli attributi) per 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__", Nessuno) polymorphic_on = Nessuno super_fks = [] se non super_mapper o local_mapper.local_table non è super_mapper.local_table: cols = [] for column in local_mapper.local_table.c: if column.name == "versione": continua col = column.copy() col.unique = False #don"t incrementa automaticamente le cose dal db normale if col.autoincrement: col.autoincrement = False #sqllite cade con le chiavi di incremento automatico se abbiamo un chiave composita if col.primary_key: col.primary_key = False se super_mapper e 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) se la colonna è local_mapper.polymorphi c_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: # ereditarietà tabella singola. prendi tutte le colonne aggiuntive che potrebbero essere state # aggiunte e aggiungile alla tabella della cronologia. per la colonna in local_mapper.local_table.c: se column.key non in super_history_mapper.local_table.c: col = column.copy() super_history_mapper.local_table.append_column(col) table = Nessuno se super_history_mapper: basi = (super_history_mapper.class_,) else: basi = local_mapper.base_mapper.class_.__bases__ versiond_cls = type.__new__(type, "%sHistory" % cls.__name__, basi, {}) m = mapper( versiond_cls, tabella, inherits=super_history_mapper, polymorphic_on=polymorphic_on, polymorphic_identity =local_mapper.polymorphic_identity ) cls.__history_mapper__ = m se non super_history_mapper: cls.version = Column("version", Integer, default=1, nullable=False) create_mappers = [] class VersionedMeta(DeclarativeMeta): def __init__(cls, classname , basi, dict_): DeclarativeMeta.__init__(cls, classname, basi, dict_) #Ho aggiunto questo codice mentre si bloccava altrimenti def make_mapper(): try: mapper = class_mapper(cls) _history_mapper(mapper) eccetto UnmappedClassError: pass create_mappers.append(lambda: make_mapper()) def versiond_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 = attributi.instance_state(obj) attr = {} obj_changed = Falso per om, hm in zip(obj_mapper.iterate_to_root(), history_mapper.iterate_to_root()): if hm.single: continua per hist_col in hm .local_table.c: if hist_col.key == "versione" o hist_col.key == "modificato" o hist_col.key == "hist_id": continua obj_col = om.local_table.c[hist_col.key] # ottieni il valore dell'attributo # in base alla MapperProperty relativa alla colonna # mappata. ciò consentirà l'utilizzo di MapperProperties # che hanno un nome chiave diverso da quello della colonna mappata. try: prop = obj_mapper.get_property_by_column(obj_col) eccetto UnmappedColumnError: # nel caso di ereditarietà tabella singola, ci possono essere # colonne sulla tabella mappata destinate solo alla sottoclasse. # lo stato "non mappato" della colonna della sottoclasse sulla # classe base è una caratteristica del modulo dichiarativo a partire da sqla 0.5.2. continue # attributi dell'oggetto scaduti e anche le colonne differite potrebbero non essere nel # dict. forzalo a caricare indipendentemente da cosa usando getattr(). se prop.key non è in obj_state.dict: getattr(obj, prop.key) a, u, d = attributi.get_history(obj, prop.key) se d: attr[hist_col.key] = d[0] obj_changed = True elif u: attr[hist_col.key] = u[0] else: # se l'attributo non ha valore. attr[hist_col.key] = a[0] obj_changed = Vero se non obj_changed: # non modificato, ma abbiamo relazioni. OK # controlla anche quelli per prop in obj_mapper.iterate_properties: if isinstance(prop, RelationshipProperty) e attributi.get_history(obj, prop.key).has_changes(): obj_changed = True break if not obj_changed e non cancellato: 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 versiond_objects(session.dirty): create_version(obj, session) for obj in versiond_objects(session.deleted): create_version(obj, session, eliminato = True)