SQLAlchemyのバージョン管理はクラスのインポート順序を考慮します


111

私はここでガイドに従っていました:

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

と問題に遭遇しました。私は私の関係を次のように定義しました:

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

文字列を使用するため、モデルモジュールのインポート順序は関係ありません。これはすべて正常に機能しますが、バージョン管理メタを使用すると、次のエラーが発生します。

sqlalchemy.exc.InvalidRequestError:マッパーMapper | MyClass | stuffsを初期化するときに、式 'Trader'が名前を見つけられませんでした( "名前 '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

私は試して問題を修正しました:ラムダにあるものを除いて、すべてのインポートが行われた後にそれらをすべて実行しました。これは機能しますが少しごみのようです、これを修正する方法のアイデアはより良い方法ですか?

ありがとう!

更新

問題は実際には輸入注文に関するものではありません。バージョン管理の例は、マッパーがバージョン管理された各クラスのコンストラクターでコンパイルする必要があるように設計されています。また、関連するクラスがまだ定義されていない場合、コンパイルは失敗します。循環リレーションの場合、マップされたクラスの定義順序を変更して機能させる方法はありません。

アップデート2

上記の更新のように(他の人の投稿をここで編集できることを知りませんでした:))これは循環参照が原因である可能性があります。その場合、誰かが私のハックを便利にするでしょう(私はターボギアでそれを使用しています)(VersionedMetaを置き換えて、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]

このようにして、すべてのクラスが定義された後にのみマッパーを作成します。

Update 3 わずかに無関係ですが、状況によっては、重複する主キーエラーに遭遇しました(同じオブジェクトに2つの変更を一度にコミットする)。私の回避策は、新しい自動増分キーを追加することです。もちろん、mysqlでは1を超えることはできないため、履歴テーブルを作成するために使用されている既存のものを主キーから外す必要がありました。私の全体的なコードを確認してください(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)

6
非推奨の名前は使用しないでください。relation()する必要がありますrelationship()
ThiefMaster

25
これの一部を回答に移動して受け入れてください。
東武

54
誰かがこの未回答の質問が48票を獲得した理由を説明できますか?ここで何が起こっているのかわからないので興味がない(Python
開発者で

4
@Moak-同じガイドに従った人がたくさんいるからだと思います。コメントが36になった理由を誰かが説明できますか?それがSOの魔法です:)
2011

2
@alfonsoと、なぜあなたが1を獲得したのか:)、これはyoutubeのいいね:-)
Mouna Cheikhna '19

回答:


2

私は試して問題を修正しました:ラムダにあるものを除いて、すべてのインポートが行われた後にそれらをすべて実行しました。

すごい!

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.