Sqlalchemy общий внешний ключ (например, в django ORM)

Есть ли sqlalchemy что-то вроде django GenericForeignKey? И правильно ли использовать общие внешние поля.

Моя проблема: у меня есть несколько моделей (например, Post, Project, Vacancy, ничего особенного), и я хочу добавить комментарии к каждому из них. И я хочу использовать только одну модель комментариев. Стоит ли это? Или я должен использовать PostComment, ProjectComment и т.д.? Плюсы/минусы обоих способов?

Спасибо!

Ответ 1

Самый простой шаблон, который я использую чаще всего, состоит в том, что у вас на самом деле есть отдельные таблицы комментариев для каждой связи. Это может показаться пугающим вначале, но он не требует дополнительного кода и не использует какой-либо другой подход - таблицы создаются автоматически, а модели относятся к шаблону Post.Comment, Project.Comment и т.д. Определение Комментарий сохраняется в одном месте. Этот подход с референтной точки зрения является самым простым и эффективным, а также большинством DBA, так как различные виды комментариев хранятся в собственных таблицах, размер которых может быть индивидуальным.

Еще один шаблон для использования - это одна таблица комментариев, но разные таблицы ассоциаций. Этот шаблон предлагает использовать случай, когда вам может понадобиться комментарий, связанный с более чем одним видом объекта за один раз (например, сообщение и проект одновременно). Этот шаблон по-прежнему достаточно эффективен.

В-третьих, существует таблица полиморфных ассоциаций. Этот шаблон использует фиксированное количество таблиц для представления коллекций и связанного с ними класса без ущерба для ссылочной целостности. Этот шаблон пытается приблизиться к "универсальному внешнему ключу" в стиле Django, сохраняя при этом ссылочную целостность, хотя и не так прост, как предыдущие два подхода.

Имитация шаблона, используемого ROR/Django, где нет реальных внешних ключей, и строки сопоставляются с использованием логики приложения, также возможно.

Первые три шаблона проиллюстрированы в современной форме в дистрибутиве SQLAlchemy в примерах /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)

Ответ 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