邻接列表+抽象基类继承中使用的关系

时间:2014-11-03 23:05:12

标签: sqlalchemy

以下是Adjacency List + Inheritance的示例。这可以按预期工作,但如果我尝试在另一个模型Mammut中使用它作为一种关系,它会抛出这个错误:

Traceback (most recent call last):
  File "bin/py", line 73, in <module>
    exec(compile(__file__f.read(), __file__, "exec"))
  File "../adjacency_list.py", line 206, in <module>
    create_entries(IntTreeNode)
  File "../adjacency_list.py", line 170, in create_entries
    mut.nodes.append(node)
  File "/home/xxx/.buildout/eggs/SQLAlchemy-0.9.8-py3.4-linux-x86_64.egg/sqlalchemy/orm/dynamic.py", line 304, in append
    attributes.instance_dict(self.instance), item, None)
  File "/home/xxx/.buildout/eggs/SQLAlchemy-0.9.8-py3.4-linux-x86_64.egg/sqlalchemy/orm/dynamic.py", line 202, in append
    self.fire_append_event(state, dict_, value, initiator)
  File "/home/xxx/.buildout/eggs/SQLAlchemy-0.9.8-py3.4-linux-x86_64.egg/sqlalchemy/orm/dynamic.py", line 99, in fire_append_event
    value = fn(state, value, initiator or self._append_token)
  File "/home/xxx/.buildout/eggs/SQLAlchemy-0.9.8-py3.4-linux-x86_64.egg/sqlalchemy/orm/attributes.py", line 1164, in emit_backref_from_collection_append_event
    child_impl.append(
AttributeError: '_ProxyImpl' object has no attribute 'append'

守则:

from sqlalchemy import (Column, ForeignKey, Integer, String, create_engine,
                        Float)
from sqlalchemy.orm import (Session, relationship, backref, joinedload_all)
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm.collections import attribute_mapped_collection
from sqlalchemy.ext.declarative import declared_attr, AbstractConcreteBase


Base = declarative_base()


class Mammut(Base):
    __tablename__ = "mammut"

    id = Column(Integer, primary_key=True)
    nodes = relationship(
        'TreeNode',
        backref='mammut',
        lazy='dynamic',
        cascade="all, delete-orphan",
        #viewonly=True
    )


class TreeNode(AbstractConcreteBase, Base):
    id = Column(Integer, primary_key=True)
    name = Column(String(50), nullable=False)
    depth = Column(Integer, default=0)
    data_type = Column(String(50))

    @declared_attr
    def mammut_id(cls):
        return Column(Integer, ForeignKey('mammut.id'))

    @declared_attr
    def __tablename__(cls):
        return cls.__name__.lower()

    @declared_attr
    def __mapper_args__(cls):
        ret = {}
        if cls.__name__ != "TreeNode":
            ret = {'polymorphic_identity': cls.__name__,
                   'concrete': True,
                   # XXX redundant makes only sense if we use one table
                   'polymorphic_on': cls.data_type}
        return ret

    @declared_attr
    def parent_id(cls):
        _fid = '%s.id' % cls.__name__.lower()
        return Column(Integer, ForeignKey(_fid))

    @declared_attr
    def children(cls):
        _fid = '%s.id' % cls.__name__
        return relationship(cls.__name__,
                            # cascade deletions
                            cascade="all, delete-orphan",
                            # many to one + adjacency list - remote_side
                            # is required to reference the 'remote'
                            # column in the join condition.
                            backref=backref("parent", remote_side=_fid),
                            # children will be represented as a dictionary
                            # on the "name" attribute.
                            collection_class=attribute_mapped_collection(
                                'name'),
                            )

    def get_path(self, field):
        if self.parent:
            return self.parent.get_path(field) + [getattr(self, field)]
        else:
            return [getattr(self, field)]

    @property
    def name_path(self):
        # XXX there is no way to query for it except we add a function with a
        # cte (recursive query) to our database see [1] for it
        # https://stackoverflow.com/questions/14487386/sqlalchemy-recursive-hybrid-property-in-a-tree-node
        return '/'.join(self.get_path(field='name'))

    def __init__(self, name, value=None, parent=None):
        self.name = name
        self.parent = parent
        self.depth = 0
        self.value = value
        if self.parent:
            self.depth = self.parent.depth + 1

    def __repr__(self):
        ret = "%s(name=%r, id=%r, parent_id=%r, value=%r, depth=%r, " \
            "name_path=%s data_type=%s)" % (
                self.__class__.__name__,
                self.name,
                self.id,
                self.parent_id,
                self.value,
                self.depth,
                self.name_path,
                self.data_type
            )
        return ret

    def dump(self, _indent=0):
        return "   " * _indent + repr(self) + \
            "\n" + \
            "".join([
                c.dump(_indent + 1)
                for c in self.children.values()]
        )


class IntTreeNode(TreeNode):
    value = Column(Integer)


class FloatTreeNode(TreeNode):
    value = Column(Float)
    miau = Column(String(50), default='zuff')

    def __repr__(self):
        ret = "%s(name=%r, id=%r, parent_id=%r, value=%r, depth=%r, " \
            "name_path=%s data_type=%s miau=%s)" % (
                self.__class__.__name__,
                self.name,
                self.id,
                self.parent_id,
                self.value,
                self.depth,
                self.name_path,
                self.data_type,
                self.miau
            )
        return ret


if __name__ == '__main__':
    engine = create_engine('sqlite:///', echo=True)

    def msg(msg, *args):
        msg = msg % args
        print("\n\n\n" + "-" * len(msg.split("\n")[0]))
        print(msg)
        print("-" * len(msg.split("\n")[0]))

    msg("Creating Tree Table:")

    Base.metadata.create_all(engine)

    session = Session(engine)

    def create_entries(Cls):
        node = Cls('rootnode', value=2)
        Cls('node1', parent=node)
        Cls('node3', parent=node)

        node2 = Cls('node2')
        Cls('subnode1', parent=node2)
        node.children['node2'] = node2
        Cls('subnode2', parent=node.children['node2'])

        msg("Created new tree structure:\n%s", node.dump())

        msg("flush + commit:")
        # XXX this throws the error
        mut = Mammut()
        mut.nodes.append(node)
        session.add(mut)
        session.add(node)
        session.commit()

        msg("Tree After Save:\n %s", node.dump())

        Cls('node4', parent=node)
        Cls('subnode3', parent=node.children['node4'])
        Cls('subnode4', parent=node.children['node4'])
        Cls('subsubnode1', parent=node.children['node4'].children['subnode3'])

        # remove node1 from the parent, which will trigger a delete
        # via the delete-orphan cascade.
        del node.children['node1']

        msg("Removed node1. flush + commit:")
        session.commit()

        msg("Tree after save:\n %s", node.dump())

        msg("Emptying out the session entirely, "
            "selecting tree on root, using eager loading to join four levels deep.")
        session.expunge_all()
        node = session.query(Cls).\
            options(joinedload_all("children", "children",
                                   "children", "children")).\
            filter(Cls.name == "rootnode").\
            first()

        msg("Full Tree:\n%s", node.dump())

        # msg("Marking root node as deleted, flush + commit:")
        # session.delete(node)
        # session.commit()

    create_entries(IntTreeNode)
    create_entries(FloatTreeNode)

    nodes = session.query(TreeNode).filter(
        TreeNode.name == "rootnode").all()
    for idx, n in enumerate(nodes):
        msg("Full (%s) Tree:\n%s" % (idx, n.dump()))

1 个答案:

答案 0 :(得分:1)

具体的继承可能非常困难,而AbstractConcreteBase本身也存在0.9中的错误,这些错误会妨碍使用这种精心设计的映射。

使用1.0(未发布,使用git master),我可以得到如下主要元素:

from sqlalchemy import Column, String, Integer, create_engine, ForeignKey, Float
from sqlalchemy.orm import Session, relationship
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm.collections import attribute_mapped_collection
from sqlalchemy.ext.declarative import declared_attr, AbstractConcreteBase


Base = declarative_base()


class Mammut(Base):
    __tablename__ = "mammut"

    id = Column(Integer, primary_key=True)
    nodes = relationship(
        'TreeNode',
        lazy='dynamic',
        back_populates='mammut',
    )


class TreeNode(AbstractConcreteBase, Base):
    id = Column(Integer, primary_key=True)
    name = Column(String)

    @declared_attr
    def __tablename__(cls):
        if cls.__name__ == 'TreeNode':
            return None
        else:
            return cls.__name__.lower()

    @declared_attr
    def __mapper_args__(cls):
        return {'polymorphic_identity': cls.__name__, 'concrete': True}

    @declared_attr
    def parent_id(cls):
        return Column(Integer, ForeignKey(cls.id))

    @declared_attr
    def mammut_id(cls):
        return Column(Integer, ForeignKey('mammut.id'))

    @declared_attr
    def mammut(cls):
        return relationship("Mammut", back_populates="nodes")

    @declared_attr
    def children(cls):
        return relationship(
            cls,
            back_populates="parent",
            collection_class=attribute_mapped_collection('name'),
        )

    @declared_attr
    def parent(cls):
        return relationship(
            cls, remote_side="%s.id" % cls.__name__,
            back_populates='children')


class IntTreeNode(TreeNode):
    value = Column(Integer)


class FloatTreeNode(TreeNode):
    value = Column(Float)
    miau = Column(String(50), default='zuff')

e = create_engine("sqlite://", echo=True)
Base.metadata.create_all(e)

session = Session(e)

root = IntTreeNode(name='root')
IntTreeNode(name='n1', parent=root)
n2 = IntTreeNode(name='n2', parent=root)
IntTreeNode(name='n2n1', parent=n2)

m1 = Mammut()
m1.nodes.append(n2)
m1.nodes.append(root)

session.add(root)
session.commit()


session.close()

root = session.query(TreeNode).filter_by(name='root').one()
print root.children