SQLAlchemy versioning cares about class import order
我跟着导游走:
http://www.sqlacalchemy.org/docs/orm/examples.html?highlight=版本化版本化对象
并且遇到了一个问题。我定义了我的关系,比如:
1 | generic_ticker = relation('MyClass', backref=backref("stuffs")) |
它不关心我的模型模块的导入顺序。这一切正常工作,但当我使用版本控制元数据时,我会得到以下错误:
sqlalchemy.exc.InvalidRequestError: When initializing mapper Mapper|MyClass|stuffs, expression 'Trader' failed to locate a name ("name 'MyClass' is not defined"). If this is a class name, consider adding this relationship() to the class after both dependent classes have been defined.
号
我追踪到这个错误:
1 2 3 4 | 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() |
号
1 2 3 4 5 6 7 8 9 | 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中的东西,然后在所有的导入发生之后运行它们。这是可行的,但似乎有点垃圾,有什么想法如何解决这是一个更好的方法吗?
谢谢!
更新
问题不在于进口订单。版本化示例的设计使得映射器需要在每个版本化类的costructor中编译。当相关类尚未定义时,编译失败。对于循环关系,无法通过更改映射类的定义顺序使其工作。
更新2
由于上述更新状态(我不知道您可以在此处编辑其他人的帖子:),这可能是由于循环引用。在这种情况下,可能有人会发现我的黑客很有用(我正在使用它与涡轮齿轮)(替换版本meta和外接程序create_mappers global in history_meta)
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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()) |
。
然后你可以在你的模型中做如下的事情
1 2 3 4 5 6 7 8 9 10 11 | # 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] |
这样,它只在定义了所有类之后才创建映射器。
更新3有点不相关,但在某些情况下,我遇到了一个重复的主键错误(一次对同一对象提交两个更改)。我的解决方法是添加一个新的主自动递增键。当然,MySQL不能超过1个,所以我必须对用于创建历史表的现有内容取消主键。检查我的整体代码(包括历史ID和摆脱外键约束):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | """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) |
。
I fixed the problem by putting the try: except stuff in a lambda and
running them all after all the imports have happened.
号
伟大的!