SqlAlchemy Модель ассоциации на основе данных записи - PullRequest
2 голосов
/ 04 февраля 2012

Существует таблица info, которая имеет отношение к таблице car или таблице suv.

Указывается в поле info.type.

Так, как я могу создать ассоциацию на лету, основываясь на данных типа записи?

class Info(Base):
    item_id = Column(ForeignKey('cars-or-suvs-table.id'))
    type = Column(String())

class Car(Base):
    - data - 

    info = relationship('Info', backref="car")

class Suv(Base):
    - data - 

    info = relationship('Info', backref="suv")

Редактировать: У меня уже есть таблицы, заполненные данными, поэтому я не могу изменить схему БД.

Ответы [ 2 ]

2 голосов
/ 05 февраля 2012

Поскольку вы ищете решение, которое не требует перемещения внешнего ключа в другую таблицу, вы можете попробовать этот подход:

import sqlalchemy
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, String, Integer
from sqlalchemy.orm import sessionmaker, relationship
from sqlalchemy import sql
Base = declarative_base()
engine = sqlalchemy.create_engine('sqlite:///:memory:')
Session = sessionmaker(bind=engine)
session = Session()

class Info(Base):
    __tablename__ = 'info'
    id = Column(Integer(), primary_key=True)
    type = Column(String())
    item_id = Column(Integer())

    @property
    def item(self):
        if self.type == 'car':
            return self._car
        elif self.type == 'suv':
            return self._suv
        return None

    @item.setter
    def item(self, value):
        if value is not None:
            self.item_id = value.id
            self.type = value.__tablename__
        else:
            self.item_id = None

class Car(Base):
    __tablename__ = 'car'
    id = Column(Integer(), primary_key=True)
    info = relationship(Info, primaryjoin=sql.and_(id == Info.item_id, Info.type == 'car'), foreign_keys=Info.item_id, uselist=False, backref='_car')

class Suv(Base):
    __tablename__ = 'suv'
    id = Column(Integer(), primary_key=True)
    info = relationship(Info, primaryjoin=sql.and_(id == Info.item_id, Info.type == 'suv'), foreign_keys=Info.item_id, uselist=False, backref='_suv')

Я переименовал Info.car в Info._car, так как._car неизбежно будет поддельным автомобильным объектом, даже если .type - это 'suv'.

Я упустил материал слушателя событий, чтобы он был простым, но вы определенно можете адаптировать то, что вам нужно, из моего другогоответьте, чтобы не попасть в противоречивое состояние.

1 голос
/ 04 февраля 2012

В SQL внешний ключ должен быть сопоставлен с одной конкретной таблицей, поэтому вам нужно поместить внешний ключ в таблицу 'car' или 'suv', указывающую на 'info.id'.

Это, вероятно, излишне для того, что вам нужно, но вот один из способов решить его (если вы действительно хотите, чтобы у каждого автомобиля была только одна информация):

import sqlalchemy
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, String, Integer, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy import event
Base = declarative_base()

class Info(Base):
    __tablename__ = 'info'
    id = Column(Integer(), primary_key=True)
    type = Column(String())
    # NOTE: can't use backref='info' because we need the attributes defined
    #   directly on both classes so we can attach event listeners
    car = relationship('Car', back_populates='info', uselist=False)
    suv = relationship('Suv', back_populates='info', uselist=False)

    @property
    def item(self):
        # could check self.type here if you wanted
        return self.car or self.suv

    @item.setter
    def item(self, value):
        if isinstance(value, Car):
            self.car = value
        elif isinstance(value, Suv):
            self.suv = value
        elif value is None:
            self.car = None
            self.suv = None
        else:
            raise ValueError("item must be Car or Suv")

@event.listens_for(Info.car, 'set')
def _car_set_event(target, value, oldvalue, initiator):
    if value is not None:
        target.type = 'car'
        if target.suv:
            target.suv = None
    elif target.type == 'car':
        target.type = None

@event.listens_for(Info.suv, 'set')
def _suv_set_event(target, value, oldvalue, initiator):
    if value is not None:
        target.type = 'suv'
        if target.car:
            target.car = None
    elif target.type == 'suv':
        target.type = None

class Car(Base):
    __tablename__ = 'car'
    id = Column(Integer(), primary_key=True)
    info_id = Column(Integer(), ForeignKey('info.id'))
    info = relationship(Info, back_populates='car')

@event.listens_for(Car.info, 'set')
def _car_info_set_event(target, value, oldvalue, initiator):
    if value is not None:
        value.type = 'car'

class Suv(Base):
    __tablename__ = 'suv'
    id = Column(Integer(), primary_key=True)
    info_id = Column(Integer(), ForeignKey('info.id'))
    info = relationship(Info, back_populates='suv')

@event.listens_for(Suv.info, 'set')
def _suv_info_set_event(target, value, oldvalue, initiator):
    if value is not None:
        value.type = 'suv'

Какова сложностьслушатели события получают, что тип автоматически управляется, когда вы делаете что-то вроде:

car1.info = Info()
assert (car1.info.type == 'car')

или

info1 = car1.info
info1.suv = suv1
assert (car1.info is None)
assert (info1.type == 'suv')

Если вы хотите сохранить Info.type, Info.car,и Info.suv согласуются, вы можете опустить все функции прослушивателя событий.

Было бы также очень разумно иметь отдельные объекты и таблицы для CarInfo и SuvInfo и вообще избежать всей этой сложности.

...