Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using polymorphic classes in classic mappings style

Tags:

sqlalchemy

The example about joined inheritance in the docs uses the declarative mapping. I'm trying to adapt it to use "classic mappings", but is not working as it should.

I've read and used the docs from https://docs.sqlalchemy.org/en/14/orm/inheritance.html as a guide.

I have some simple classes using attrs:

class Person:
    pass


@attr.s(auto_attribs=True)
class Manager(Person):
    name: str
    data: str


@attr.s(auto_attribs=True)
class Engineer(Person):
    name: str
    info: int


@attr.s(auto_attribs=True)
class Company:
    people: list[Person]

And I'm declaring the mappings and tables as follows:

persons_table = Table(
    "person",
    metadata,
    Column("id", Integer, primary_key=True),
)

managers_table = Table(
    "manager",
    metadata,
    Column("id", Integer, primary_key=True),
    Column("name", String(50)),
    Column("data", String(50)),
)

engineers_table = Table(
    "engineer",
    metadata,
    Column("id", Integer, primary_key=True),
    Column("name", String(50)),
    Column("info", Integer),
)


company_table = Table(
    "company",
    metadata,
    Column("id", Integer, primary_key=True),
)

pjoin = polymorphic_union(
    {"person": persons_table, "manager": managers_table, "engineer": engineers_table},
    "type",
    "pjoin",
)


company_2_people_table = Table(
    "company_2_people",
    metadata,
    Column("id", Integer, primary_key=True, autoincrement=True),
    Column("company_id", ForeignKey("company.id")),
    Column("person_id", ForeignKey("person.id")),
)


person_mapper = mapper(
    Person,
    pjoin,
    with_polymorphic=("*", pjoin),
    polymorphic_on=pjoin.c.type,
)
manager_mapper = mapper(
    Manager,
    managers_table,
    inherits=person_mapper,
    concrete=True,
    polymorphic_identity="manager",
)
engineer_mapper = mapper(
    Engineer,
    engineers_table,
    inherits=person_mapper,
    concrete=True,
    polymorphic_identity="engineer",
)

company_mapper = mapper(
    Company,
    company_table,
    properties={
        "people": relationship(
            person_mapper,
            secondary=company_2_people_table,
            collection_class=list,
        ),
    },
)

A simple test:

fn = Path(__file__).with_suffix(".db")
fn.unlink(missing_ok=True)
engine = create_engine(f"sqlite:///{fn}", echo=True)
metadata.create_all(engine)

Session = sessionmaker(bind=engine)
with Session() as session:
    m1 = Manager(name="Manager 1", data="Manager Data")
    m2 = Manager(name="Manager 2", data="Manager Data")
    e1 = Engineer(name="Eng", info=10)

    company = Company([m1, e1, m2])
    session.add(company)
    session.commit()

with Session() as session:
    print(session.query(Company).get(1))

This runs, however I get this output:

Company(people=[Engineer(name='Eng', info=10), Manager(name='Manager 1', data='Manager Data'), Manager(name='Manager 2', data='Manager Data')])

Notice that although the instances are correct, the order is not: it should be Manager, Engineer, Manager.

Comparing my database file with the one generated from the example from the docs:

  • In the table from the docs, the person table contains all people, and a type column with the type of the person.
  • In mine, the person table is empty, and contains only an id column (no type).

I have debugged the runtime classes generated by the example and tried to mimic the structures there (for example explicitly passing the internal _polymorphic_map, but to no avail).

I've also changed the primary key definition for Manager and Engineer to Column('id', ForeignKey("person.id"), primary_key=True), however I get an exception:

sqlalchemy.orm.exc.FlushError: Instance <Engineer at 0x198e43cd280> has a NULL identity key.  If this is an auto-generated value, check that the database table allows generation of new primary key values, and that the mapped Column object is configured to expect these generated values.  Ensure also that this flush() is not occurring at an inappropriate time, such as within a load() event.

Any other suggestions or hints that might point me in the right direction?

Thanks.

I've posted the full source code at https://gist.github.com/nicoddemus/26de7bbcdfa9ed4b14fcfdde72b1d63f.

like image 655
Bruno Oliveira Avatar asked Apr 02 '21 15:04

Bruno Oliveira


Video Answer


1 Answers

After reading the examples more carefully I found what I was doing wrong: I was mixing concepts from joined inheritance with concrete inheritance.

I want joined inheritance, so:

  1. Each table subclass needs a to define its primary key as a foreign key to the base table:
engineers_table = Table(
    "engineer",
    metadata,
    Column('id', ForeignKey("person.id"), primary_key=True),
    Column("name", String(50)),
    Column("info", Integer),
)
  1. The base mapper needs to specify which column to use as denominator:
person_mapper = mapper_registry.map_imperatively(
    Person,
    persons_table,
    polymorphic_identity="person",
    polymorphic_on=persons_table.c.type,
)
  1. Every subclass also needs to specify their polimorphic identity:
manager_mapper = mapper_registry.map_imperatively(
    Manager,
    managers_table,
    inherits=person_mapper,
    polymorphic_identity="manager",
)

And that's it, SQLA takes care of the rest. I've updated the Gist link with the full and now working code, in case it might help others.

https://gist.github.com/nicoddemus/26de7bbcdfa9ed4b14fcfdde72b1d63f

like image 130
Bruno Oliveira Avatar answered Oct 06 '22 14:10

Bruno Oliveira