SQLAlchemy版本控制关心类的导入顺序
我在这里遵循指导:
http://www.sqlalchemy.org/docs/orm/examples.html?highlight=versioning#versioned-objects
并遇到了一个问题。 我已经定义了我的关系,如:
generic_ticker = relation('MyClass', backref=backref("stuffs"))
与string,所以它不关心我的模型模块的import顺序。 这一切正常工作正常,但是当我使用版本元我得到以下错误:
sqlalchemy.exc.InvalidRequestError:当初始化映射器Mapper | MyClass | stuffs时,expression式'Trader'找不到名称(“name'MyClass'未定义”)。 如果这是一个类的名字,可以考虑在定义了两个相关类之后,将这个关系()添加到类中。
我跟踪了这个错误:
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
我把这个问题的解决办法放在了lambda上:除了lambda之外的东西,并且在全部导入发生之后运行它们。 这工作,但似乎有点垃圾,任何想法如何解决这是一个更好的办法?
谢谢!
更新
这个问题实际上并不涉及import订单。 版本控制的例子被devise成使得映射器需要在每个版本类的构造器中进行编译。 当相关类尚未定义时编译失败。 在循环关系的情况下,无法通过更改映射类的定义顺序来使其工作。
更新2
正如上面的更新状态(我不知道你可以在这里编辑其他人的post:))这可能是由于循环引用。 在这种情况下,有人会发现我的黑客有用(我用涡轮增压器)(replaceVersionedMeta并添加在history_meta中的create_mappers全局)
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())
那么你可以在你的模型__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]
这样,只有在所有类都被定义之后,才能创build映射器。
更新3略有不相关,但在某些情况下,我碰到了一个重复的主键错误(一次提交2个更改到同一个对象)。 我的解决方法是添加一个新的主自动递增密钥。 当然,你不能有超过1与MySQL,所以我不得不主要用于创build历史表的现有的东西。 检查我的整体代码(包括一个hist_id和摆脱外键约束):
"""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)
我把这个问题的解决办法放在了lambda上:除了lambda之外的东西,并且在全部导入发生之后运行它们。
大!