How to implement overlapping joined table inheritance? #8320
-
Hi. I am implementing a supertype-subtype joined table inheritance as follows. That is, a person might be a employee and student at the same time. At first, I followed SQLAlchemy1.4 Documentation - Mapping Class Inheritance Hierarchies. That's great, but I found that it is only for disjoint subclasses because the subclass table records are fully dependent on base class table's discriminator, i.e. I cannot insert the same person to two sub classes table. I had googled to figure out that, I could use two discriminators, which are 'is_employee' and 'is_student' to solve the overlapping problem. But then I found a note stating that
I was not sure what is 'Cascading', but I got that I can only use one discriminator column or SQL expression. So I tried PostgreSQL's ARRAY and JSON column types, and also composite column as stated in documentation - Composite Column Types. All of which are failed as follows.
Then I dig deep into internet, found that #8089 are quite similar to my case. And I implemented an example below from typing import Tuple
from sqlalchemy import (
create_engine,
Column,
ForeignKey,
Integer,
select,
inspect,
cast,
event,
String,
Boolean,
)
from sqlalchemy.orm import declarative_base, sessionmaker
Base = declarative_base()
class ident_(str):
"""describe a composed identity.
Using a string for easy conversion to a string SQL composition.
"""
_tup: Tuple[bool, bool]
def __new__(cls, d1, d2):
self = super().__new__(cls, f"{d1}{d2}")
self._tup = d1, d2
return self
def _as_tuple(self):
return self._tup
class Person(Base):
__tablename__ = "person"
id: Column = Column(Integer, primary_key=True)
d1 = Column(Boolean, nullable=False) # this can be your FK to the other table etc.
d2 = Column(Boolean, nullable=True) # this is a "supplementary" discrim column
__mapper_args__ = {
"polymorphic_identity": ident_(False, False),
"polymorphic_on": cast(d1, String) + cast(d2, String),
}
@event.listens_for(Person, "init", propagate=True)
def _setup_poly(target, args, kw):
"""receive new AbstractQuestion objects when they are constructed and
set polymorphic identity"""
# this is the ident_() object
ident = inspect(target).mapper.polymorphic_identity
d1, d2 = ident._as_tuple()
kw["d1"] = d1
kw["d2"] = d2
class Employee(Person):
__tablename__ = "employee"
id: Column = Column(Integer, ForeignKey(Person.id), primary_key=True)
__mapper_args__ = {"polymorphic_identity": ident_(True, False)}
class Student(Person):
__tablename__ = "student"
id: Column = Column(Integer, ForeignKey(Person.id), primary_key=True)
__mapper_args__ = {"polymorphic_identity": ident_(False, True)}
engine = create_engine("sqlite://", echo=True, future=True)
Base.metadata.create_all(engine)
Session = sessionmaker(engine)
with Session() as session:
# adding a person to each table
id_ = 1
session.add(Employee(id=id_))
session.commit()
# search for inserted person
person_id = (
session.execute(select(Person.id).where(Person.id == id_))
.scalars()
.one_or_none()
)
employee_id = (
session.execute(select(Employee.id).where(Employee.id == id_))
.scalars()
.one_or_none()
)
session.commit()
# check if their ids equal
assert person_id == employee_id == id_
# try add the same person to Student subclass
session.add(Student(id=id_))
session.commit()
# Oops, IntegrityError: (sqlite3.IntegrityError) UNIQUE constraint failed: person.id But then I found no clues to continue implementing overlapping joined table inheritance due to IntegrityError. Could anybody give me some hints? Is there any possibility to implement this with the polymorphic functionality stated in documentation - Mapping Class Inheritance Hierarchies? Or is the following code the only way to implement overlapping joined table inheritance? i.e. without the polymorphic functionality and do joining and inserting manually? from sqlalchemy import create_engine, Column, ForeignKey, Integer, select
from sqlalchemy.orm import declarative_base, sessionmaker
Base = declarative_base()
class Person(Base):
__tablename__ = "person"
id: Column = Column(Integer, primary_key=True)
class Employee(Base):
__tablename__ = "employee"
id: Column = Column(Integer, ForeignKey(Person.id), primary_key=True)
class Student(Base):
__tablename__ = "student"
id: Column = Column(Integer, ForeignKey(Person.id), primary_key=True)
engine = create_engine("sqlite://", echo=True, future=True)
Base.metadata.create_all(engine)
Session = sessionmaker(engine)
with Session() as session:
# adding a person to each table
id_ = 1
session.add(Person(id=id_))
session.add(Employee(id=id_))
session.add(Student(id=id_))
session.commit()
# search for inserted person
person_id = (
session.execute(select(Person.id).where(Person.id == id_))
.scalars()
.one_or_none()
)
employee_id = (
session.execute(select(Employee.id).where(Employee.id == id_))
.scalars()
.one_or_none()
)
student_id = (
session.execute(select(Student.id).where(Student.id == id_))
.scalars()
.one_or_none()
)
session.commit()
# check if their ids equal
assert person_id == employee_id == student_id == id_
# No error, perfectly fine |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 9 replies
-
Hi. I may be wrong here, but I don't think you can do this without a 3rd model StudentEmployee here. Personally in this case I think I would model the data differently to have a Person table and separate Student and Employment tables that hold information of about a person |
Beta Was this translation helpful? Give feedback.
Hi.
I may be wrong here, but I don't think you can do this without a 3rd model StudentEmployee here.
How you are trying to model things does not seem correct to me, since by definition a person is either a student or an employee.
Personally in this case I think I would model the data differently to have a Person table and separate Student and Employment tables that hold information of about a person