Phiên bản SQLAlchemy quan tâm đến thứ tự nhập lớp


111

Tôi đã làm theo hướng dẫn ở đây:

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

và đã gặp phải một vấn đề. Tôi đã xác định các mối quan hệ của mình như:

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

với các chuỗi nên nó không quan tâm đến thứ tự nhập của các mô-đun mô hình của tôi. Tất cả điều này đều hoạt động tốt bình thường, nhưng khi tôi sử dụng meta lập phiên bản, tôi gặp lỗi sau:

sqlalchemy.exc.InvalidRequestError: Khi khởi tạo trình liên kết Mapper | MyClass | thứ, biểu thức 'Trader' không xác định được tên ("tên 'MyClass' không được định nghĩa"). Nếu đây là tên lớp, hãy xem xét thêm mối quan hệ này () vào lớp sau khi cả hai lớp phụ thuộc đã được xác định.

Tôi đã tìm ra lỗi để:

  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

Tôi đã khắc phục sự cố bằng cách thử: ngoại trừ những thứ trong lambda và chạy tất cả chúng sau khi tất cả các lần nhập đã xảy ra. Điều này hoạt động nhưng có vẻ hơi rác, bất kỳ ý tưởng về cách khắc phục điều này là một cách tốt hơn?

Cảm ơn!

Cập nhật

Vấn đề không thực sự là về thứ tự nhập khẩu. Ví dụ lập phiên bản được thiết kế sao cho người lập bản đồ yêu cầu biên dịch trong cấu trúc chi phí của mỗi lớp được tạo phiên bản. Và quá trình biên dịch không thành công khi các lớp liên quan chưa được xác định. Trong trường hợp quan hệ vòng tròn, không có cách nào làm cho nó hoạt động bằng cách thay đổi thứ tự định nghĩa của các lớp được ánh xạ.

Cập nhật 2

Như bản cập nhật ở trên nêu rõ (tôi không biết bạn có thể chỉnh sửa bài đăng của người khác ở đây :)), điều này có thể là do tham chiếu vòng tròn. Trong trường hợp đó, có thể ai đó sẽ thấy bản hack của tôi hữu ích (Tôi đang sử dụng nó với turbogears) (Thay thế phiên bản đã được cập nhật và thêm vào create_mappers global trong history_meta)

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

Sau đó, bạn có thể làm điều gì đó như sau trong các mô hình __init__.py của bạn

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

Bằng cách đó, nó chỉ tạo các ánh xạ sau khi tất cả các lớp đã được xác định.

Bản cập nhật 3 Hơi không liên quan nhưng tôi đã gặp lỗi khóa chính trùng lặp trong một số trường hợp (thực hiện 2 thay đổi cho cùng một đối tượng trong một lần). Cách giải quyết của tôi là thêm một khóa tăng tự động chính mới. Tất nhiên bạn không thể có nhiều hơn 1 với mysql vì vậy tôi đã phải bỏ khóa chính những thứ hiện có được sử dụng để tạo bảng lịch sử. Kiểm tra mã tổng thể của tôi (bao gồm hist_id và loại bỏ ràng buộc khóa ngoại):

"""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
Không sử dụng các tên không dùng nữa; relation()nên đượcrelationship()
ThiefMaster

25
Hãy chuyển một phần của điều này thành câu trả lời và chấp nhận nó.
Tobu

54
Ai đó có thể giải thích tại sao câu hỏi chưa được trả lời này nhận được 48 lượt ủng hộ không? Chỉ cần ra khỏi sự quan tâm như tôi không rõ ràng những gì đang xảy ra ở đây (không phải là một dev python)
Moak

4
@Moak - Tôi chắc chắn đó là vì có rất nhiều người đã làm theo cùng một hướng dẫn. Ai đó có thể giải thích tại sao nhận xét của bạn nhận được 36? Đó là điều kỳ diệu của SO :)
alf

2
@alfonso và lý do tại sao bạn có 1 :), điều này đang bắt đầu trở nên giống như ngón tay cái youtube của lên :-)
Mouna Cheikhna

Câu trả lời:


2

Tôi đã khắc phục sự cố bằng cách thử: ngoại trừ những thứ trong lambda và chạy tất cả chúng sau khi tất cả các lần nhập đã xảy ra.

Tuyệt quá!

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.