Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to query a relationship on multiple polymorphic-inheritance tables?

Let's say you have the following simplified example schema, which uses SQLAlchemy joined table polymorphic inheritance. Engineer and Analyst models have a Role relationship. The Intern model does not.

class Role(db.Model):

    __tablename__ = 'role'

    id = db.Column(db.Integer, primary_key=True)

    name = db.Column(db.String(16), index=True)

class EmployeeBase(db.Model):

    __tablename__ = 'employee_base'

    id = db.Column(db.Integer, primary_key=True)

    some_attr = db.Column(db.String(16))
    another_attr = db.Column(db.String(16))

    type = db.Column(db.String(50), index=True)

    __mapper_args__ = {
        'polymorphic_identity': 'employee',
        'polymorphic_on': type
    }

class Engineer(EmployeeBase):

    __tablename__ = 'engineer'

    id = db.Column(db.Integer, db.ForeignKey('employee_base.id'), primary_key=True)

    role_id = db.Column(db.Integer, db.ForeignKey('role.id'), index=True)
    role = db.relationship('Role', backref='engineers')

    __mapper_args__ = {
        'polymorphic_identity': 'engineer',
    }

class Analyst(EmployeeBase):

    __tablename__ = 'analyst'

    id = db.Column(db.Integer, db.ForeignKey('employee_base.id'), primary_key=True)

    role_id = db.Column(db.Integer, db.ForeignKey('role.id'), index=True)
    role = db.relationship('Role', backref='analysts')

    __mapper_args__ = {
        'polymorphic_identity': 'analyst',
    }

class Intern(EmployeeBase):

    __tablename__ = 'intern'

    id = db.Column(db.Integer, db.ForeignKey('employee_base.id'), primary_key=True)

    term_ends = db.Column(db.DateTime, index=True, nullable=False)

    __mapper_args__ = {
        'polymorphic_identity': 'intern',
    }

If I want to find Employees with a Role name having "petroleum" somewhere in the name, how would I do that?

I've tried many, many approaches. The closest I've come is this, which only returns Analyst matches:

employee_role_join = with_polymorphic(EmployeeBase, 
    [Engineer, Analyst])
results = db.session.query(employee_role_join).join(Role).filter(Role.name.ilike('%petroleum%'))

If I try to do something like this, I get an AttributeError, because I'm searching on an attribute of the joined Role table:

employee_role_join = with_polymorphic(EmployeeBase, 
    [Engineer, Analyst])
results = db.session.query(employee_role_join).filter(or_(
    Engineer.role.name.ilike('%petroleum%'),
    Analyst.role.name.ilike('%petroleum%')))
like image 938
DeeDee Avatar asked Jul 12 '18 15:07

DeeDee


2 Answers

You can try specifying the join ON clause explicitly since the issue with your first query seems to be that Role is joining only on the analyst.role_id column:

employee_role_join = with_polymorphic(EmployeeBase, [Engineer, Analyst])
results = session.query(employee_role_join).join(Role).filter(Role.name.ilike('%petroleum%'))
print(str(results))

SELECT employee_base.id AS employee_base_id,
    employee_base.some_attr AS employee_base_some_attr,
    employee_base.another_attr AS employee_base_another_attr,
    employee_base.type AS employee_base_type,
    engineer.id AS engineer_id,
    engineer.role_id AS engineer_role_id,
    analyst.id AS analyst_id,
    analyst.role_id AS analyst_role_id
FROM employee_base
LEFT OUTER JOIN engineer ON employee_base.id = engineer.id
LEFT OUTER JOIN analyst ON employee_base.id = analyst.id
JOIN role ON role.id = analyst.role_id 
WHERE lower(role.name) LIKE lower(?)

employee_role_join is an AliasedClass that exposes both Analyst and Engineer, which we can then use to create a join-ON clause like so:

results = session.query(employee_role_join)\
        .join(Role, or_(  \
            employee_role_join.Engineer.role_id==Role.id,  \
            employee_role_join.Analyst.role_id==Role.id  \
        ))\
        .filter(Role.name.ilike('%petroleum%'))

which changes the resulting SQL to JOIN role ON engineer.role_id = role.id OR analyst.role_id = role.id

like image 58
nelfin Avatar answered Nov 16 '22 15:11

nelfin


Define the role_id on EmployeeBase. Even though Intern doesn't have the relationship back to the role table, the field can be null for that case.

I changed EmployeeBase to this:

class EmployeeBase(db.Model):

    __tablename__ = 'employee_base'

    id = db.Column(db.Integer, primary_key=True)
    role_id = db.Column(db.Integer, db.ForeignKey('role.id'), index=True)

    given_name = db.Column(db.String(16))
    surname = db.Column(db.String(16))

    type = db.Column(db.String(50), index=True)

    __mapper_args__ = {
        'polymorphic_identity': 'employee',
        'polymorphic_on': type
    }

And removed the role_id column definition from all other employee models.

db.create_all()

petrolium_engineer = Role(name='Petrolium Engineer')
geotech_engineer = Role(name='Geotech Engineer')
analyst_petrolium = Role(name='Analyst of Petrolium')

db.session.add(petrolium_engineer)
db.session.add(geotech_engineer)
db.session.add(analyst_petrolium)

db.session.add(
    Intern(given_name='Joe', surname='Blogs', term_ends=datetime.now())
)
db.session.add(
    Engineer(given_name='Mark', surname='Fume', role=petrolium_engineer)
)
db.session.add(
    Engineer(given_name='Steve', surname='Rocks', role=geotech_engineer)
)
db.session.add(
    Analyst(given_name='Cindy', surname='Booker', role=analyst_petrolium)
)

db.session.commit()

petrolium_roles = db.session.query(EmployeeBase).join(Role).\
                    filter(Role.name.contains('Petrolium')).all()

for emp in petrolium_roles:
    print(f'{emp.given_name} {emp.surname} is {emp.role.name}')

# Mark Fume is Petrolium Engineer
# Cindy Booker is Analyst of Petrolium
like image 2
SuperShoot Avatar answered Nov 16 '22 15:11

SuperShoot