Вопрос

There is an info table and it has relationship to car table or suvtable.

Itis specified in info.type field.

So how can I create association on fly based on the type data of the record?

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

Edit: I already have the tables filled with data, so I can not change db schema.

Это было полезно?

Решение

Since you're looking for a solution that doesn't require moving the foreign key to a different table, you can try this approach:

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

I renamed Info.car to Info._car since ._car will unavoidably be a bogus car object even if .type is 'suv'.

I've left the event listener stuff out to keep it simple, but you can definitely adapt what pieces you need from my other answer to avoid things getting into an inconsistent state.

Другие советы

In SQL, a foreign key must be mapped to one specific table, so you need to put the foreign key in the 'car' or 'suv' table pointing to 'info.id'.

This is probably overkill for what you need, but here's one way to solve it (assuming you do in fact want each Car to have only one Info):

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'

What the complexity of the event listeners gets you is that the type is automatically managed when you do something like:

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

or

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

If you want to keep Info.type, Info.car, and Info.suv consistent yourself, you can omit all the event listener functions.

It would also be a very reasonable option to have separate objects and tables for CarInfo and SuvInfo, and avoid all this complexity altogether.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top