【问题标题】:sqlalchemy generic foreign key (like in django ORM)sqlalchemy 通用外键(如在 django ORM 中)
【发布时间】:2013-07-16 05:02:07
【问题描述】:

sqlalchemy 有类似 django 的 GenericForeignKey 的东西吗?使用通用外部字段是否正确。

我的问题是:我有几个模型(例如,Post、Project、Vacancy,没有什么特别的),我想为每个模型添加 cmets。而且我只想使用一个评论模型。值得吗?或者我应该使用 PostComment、ProjectComment 等?两种方式的优缺点?

谢谢!

【问题讨论】:

    标签: python django sqlalchemy generic-foreign-key


    【解决方案1】:

    我最常使用的最简单的模式是,实际上每个关系都有单独的注释表。起初这可能看起来很吓人,但与使用任何其他方法相比,它不会产生任何额外的代码 - 表是自动创建的,并且使用模式 Post.CommentProject.Comment 等引用模型。评论保存在一处。从引用的角度来看,这种方法是最简单、最有效的,也是对 DBA 最友好的,因为不同类型的 Comments 保存在自己的表中,可以单独调整大小。

    要使用的另一种模式是单个 Comment 表,但不同的关联表。这种模式提供了一个用例,您可能希望一次将评论链接到一种以上的对象(例如同时链接到帖子和项目)。这种模式仍然相当有效。

    第三,多态关联表。此模式使用固定数量的表来表示集合和相关类,而不会牺牲引用完整性。这种模式试图最接近 Django 风格的“通用外键”,同时仍然保持引用完整性,尽管它不像前两种方法那么简单。

    也可以模仿 ROR/Django 使用的模式,其中没有使用真正的外键,并且使用应用程序逻辑匹配行。

    前三种模式以现代形式在 SQLAlchemy 发行版中的examples/generic_associations/ 下进行了说明。

    ROR/Django 模式,因为它经常被问到,我也会添加到 SQLAlchemy 示例中,尽管我不太喜欢它。我使用的方法与 Django 所做的并不完全相同,因为它们似乎使用“contenttypes”表来跟踪类型,这对我来说似乎有点多余,但整数列的一般概念指向基于鉴别器列的任意数量的表。这里是:

    from sqlalchemy.ext.declarative import declarative_base, declared_attr
    from sqlalchemy import create_engine, Integer, Column, \
                        String, and_
    from sqlalchemy.orm import Session, relationship, foreign, remote, backref
    from sqlalchemy import event
    
    
    class Base(object):
        """Base class which provides automated table name
        and surrogate primary key column.
    
        """
        @declared_attr
        def __tablename__(cls):
            return cls.__name__.lower()
        id = Column(Integer, primary_key=True)
    Base = declarative_base(cls=Base)
    
    class Address(Base):
        """The Address class.
    
        This represents all address records in a
        single table.
    
        """
        street = Column(String)
        city = Column(String)
        zip = Column(String)
    
        discriminator = Column(String)
        """Refers to the type of parent."""
    
        parent_id = Column(Integer)
        """Refers to the primary key of the parent.
    
        This could refer to any table.
        """
    
        @property
        def parent(self):
            """Provides in-Python access to the "parent" by choosing
            the appropriate relationship.
    
            """
            return getattr(self, "parent_%s" % self.discriminator)
    
        def __repr__(self):
            return "%s(street=%r, city=%r, zip=%r)" % \
                (self.__class__.__name__, self.street,
                self.city, self.zip)
    
    class HasAddresses(object):
        """HasAddresses mixin, creates a relationship to
        the address_association table for each parent.
    
        """
    
    @event.listens_for(HasAddresses, "mapper_configured", propagate=True)
    def setup_listener(mapper, class_):
        name = class_.__name__
        discriminator = name.lower()
        class_.addresses = relationship(Address,
                            primaryjoin=and_(
                                            class_.id == foreign(remote(Address.parent_id)),
                                            Address.discriminator == discriminator
                                        ),
                            backref=backref(
                                    "parent_%s" % discriminator,
                                    primaryjoin=remote(class_.id) == foreign(Address.parent_id)
                                    )
                            )
        @event.listens_for(class_.addresses, "append")
        def append_address(target, value, initiator):
            value.discriminator = discriminator
    
    class Customer(HasAddresses, Base):
        name = Column(String)
    
    class Supplier(HasAddresses, Base):
        company_name = Column(String)
    
    engine = create_engine('sqlite://', echo=True)
    Base.metadata.create_all(engine)
    
    session = Session(engine)
    
    session.add_all([
        Customer(
            name='customer 1',
            addresses=[
                Address(
                        street='123 anywhere street',
                        city="New York",
                        zip="10110"),
                Address(
                        street='40 main street',
                        city="San Francisco",
                        zip="95732")
            ]
        ),
        Supplier(
            company_name="Ace Hammers",
            addresses=[
                Address(
                        street='2569 west elm',
                        city="Detroit",
                        zip="56785")
            ]
        ),
    ])
    
    session.commit()
    
    for customer in session.query(Customer):
        for address in customer.addresses:
            print(address)
            print(address.parent)
    

    【讨论】:

    • 谢谢!我不喜欢第一个模式 - 我认为,它不是 DRY。如果我想为每个 cmets 添加一些信息(可能是 edited 标志),我应该对所有模型/表进行操作。我正在考虑我的Tag 模型的第二种模式。可以同时链接ProjectPost。第三个似乎是我的Comment 所需要的。 ROR/Django之类的好像没那么简单,所以我就“研究”一下吧。
    • 它完全干燥。 DRY 的意思是“不要重复自己”。如果你看看这个模式是如何工作的,你根本不会重复自己。仅仅因为数据库中有很多类似的表并不意味着你在重复自己;它们的创建是自动化的,添加了一些新列,如“已编辑”(使用 Alembic 之类的工具)。这是最节省空间/时间且对 DBA 友好的方法(因为可以独立配置不同表的存储)。太糟糕了,我很难说服前 djangoers。
    • 我喜欢您的第一个解决方案的概念,但我不清楚您如何实际实施它。你能举个简短的例子吗?
    • 这里所有模式的示例,包括或多或少执行 ROR/Django 通用外键的模式,都在 SQLA 发行版中的示例/通用关联下,从 0.9 开始,它们已经更新:@ 987654322@
    • @zzzeek 我想问题是当它们分散在多个表中时,运行 cleanup / stats / 任何其他试图分析所有 cmets 的东西都比较困难。不过,我想在顶部创建一个视图可以解决任何类似的只读问题。
    【解决方案2】:

    我知道这可能是一种糟糕的方法,但对我来说这是一个快速的解决方案。

    class GenericRelation(object):
        def __init__(self, object_id, object_type):
            self.object_id = object_id
            self.object_type = object_type
    
        def __composite_values__(self):
            return (self.object_id, self.object_type)
    
    
    class Permission(AbstractBase):
    
        #__abstract__ = True
    
        _object = None
    
        _generic = composite(
            GenericRelation,
            sql.Column('object_id', data_types.UUID, nullable=False),
            sql.Column('object_type', sql.String, nullable=False),
        )
    
        permission_type = sql.Column(sql.Integer)
    
        @property
        def object(self):
            session = object_session(self)
            if self._object or not session:
                return self._object
            else:
                object_class = eval(self.object_type)
                self._object = session.query(object_class).filter(object_class.id == self.object_id).first()
                return self._object
    
        @object.setter
        def object(self, value):
            self._object = value
            self.object_type = value.__class__.__name__
            self.object_id = value.id
    

    【讨论】:

      猜你喜欢
      • 2020-08-12
      • 2016-04-23
      • 1970-01-01
      • 1970-01-01
      • 2018-10-21
      • 2011-08-17
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多