Question

I have three tables: UserTypeMapper, User, and SystemAdmin. In my get_user method, depending on the UserTypeMapper.is_admin row, I then query either the User or SystemAdmin table. The user_id row correlates to the primary key id in the User and SystemAdmin tables.

class UserTypeMapper(Base):
    __tablename__ = 'user_type_mapper'

    id = Column(BigInteger, primary_key=True)
    is_admin = Column(Boolean, default=False)
    user_id = Column(BigInteger, nullable=False)

class SystemAdmin(Base):
    __tablename__ = 'system_admin'

    id = Column(BigInteger, primary_key=True)
    name = Column(Unicode)
    email = Column(Unicode)

class User(Base):
    __tablename__ = 'user'

    id = Column(BigInteger, primary_key=True)
    name = Column(Unicode)
    email = Column(Unicode)

I want to be able to get any user – system admin or regular user – from one query, so I do a join, on either User or SystemAdmin depending on the is_admin row. For example:

DBSession.query(UserTypeMapper, SystemAdmin).join(SystemAdmin, UserTypeMapper.user_id==SystemAdmin.id).first()

and

DBSession.query(UserTypeMapper, User).join(User, UserTypeMapper.user_id==User.id).first()

This works fine; however, I then would like to be access these, like so:

>>> my_admin_obj.is_admin
True
>>> my_admin_obj.name
Bob Smith

versus

>>> my_user_obj.is_admin
False
>>> my_user_obj.name
Bob Stevens

Currently, I have to specify: my_user_obj.UserTypeMapper.is_admin and my_user_obj.User.name. From what I've been reading, I need to map the tables so that I don't need to specify which table the attribute belongs to. My problem is that I do not understand how I can specify this given that I have two potential tables that the name attribute, for example, may come from.

This is the example I am referring to: Mapping a Class against Multiple Tables

How can I achieve this? Thank you.

Was it helpful?

Solution

You have discovered why "dual purpose foreign key", is an antipattern.

There is a related problem to this that you haven't quite pointed out; there's no way to use a foreign key constraint to enforce the data be in a valid state. You want to be sure that there's exactly one of something for each row in UserTypeMapper, but that 'something' is not any one table. formally you want a functional dependance on

user_type_mapper → (system_admin× 1) ∪ (user× 0)

But most sql databses won't allow you to write a foreign key constraint expressing that.

It looks complicated because it is complicated.

instead, lets consider what we really want to say; "every system_admin should be a user; or

system_adminuser

In sql, that would be written:

CREATE TABLE user (
    id INTEGER PRIMARY KEY,
    name VARCHAR,
    email VARCHAR
);

CREATE TABLE system_admin (
    user_id INTEGER PRIMARY KEY REFERENCES user(id)
);

Or, in sqlalchemy declarative style

class User(Base):
    __tablename__ = 'user'
    id = Column(Integer, primary_key=True)
    name = Column(String)
    email = Column(String)

class SystemAdmin(Base):
    __tablename__ = 'system_admin'
    user_id = Column(ForeignKey(User.id), primary_key=True)

What sort of questions does this schema allow us to ask?

  • "Is there a SystemAdmin by the name of 'john doe'"?
>>> print session.query(User).join(SystemAdmin).filter(User.name == 'john doe').exists()

EXISTS (SELECT 1 
FROM "user" JOIN system_admin ON "user".id = system_admin.user_id 
WHERE "user".name = :name_1)
  • "How many users are there? How many sysadmins?"
>>> print session.query(func.count(User.id), func.count(SystemAdmin.user_id)).outerjoin(SystemAdmin)

SELECT count("user".id) AS count_1, count(system_admin.user_id) AS count_2 
FROM "user" LEFT OUTER JOIN system_admin ON "user".id = system_admin.user_id

I hope you can see why the above is prefereable to the design you describe in your question; but in the off chance you don't have a choice (and only in that case, if you still feel what you've got is better, please refine your question), you can still cram that data into a single python object, which will be very difficult to work with, by providing an alternate mapping to the tables; specifically one which follows the rough structure in the first equation.

We need to mention UserTypeMapper twice, once for each side of the union, for that, we need to give aliases.

>>> from sqlalchemy.orm import aliased
>>> utm1 = aliased(UserTypeMapper)
>>> utm2 = aliased(UserTypeMapper)

For the union bodies join each alias to the appropriate table: Since SystemAdmin and User have the same columns in the same order, we don't need to describe them in detail, but if they are at all different, we need to make them "union compatible", by mentioning each column explicitly; this is left as an exercise.

>>> utm_sa = Query([utm1, SystemAdmin]).join(SystemAdmin, (utm1.user_id == SystemAdmin.id) & (utm1.is_admin == True))
>>> utm_u = Query([utm2, User]).join(User, (utm2.user_id == User.id) & (utm2.is_admin == False))

And then we join them together...

>>> print utm_sa.union(utm_u)
SELECT anon_1.user_type_mapper_1_id AS anon_1_user_type_mapper_1_id, anon_1.user_type_mapper_1_is_admin AS anon_1_user_type_mapper_1_is_admin, anon_1.user_type_mapper_1_user_id AS anon_1_user_type_mapper_1_user_id, anon_1.system_admin_id AS anon_1_system_admin_id, anon_1.system_admin_name AS anon_1_system_admin_name, anon_1.system_admin_email AS anon_1_system_admin_email 
FROM (SELECT user_type_mapper_1.id AS user_type_mapper_1_id, user_type_mapper_1.is_admin AS user_type_mapper_1_is_admin, user_type_mapper_1.user_id AS user_type_mapper_1_user_id, system_admin.id AS system_admin_id, system_admin.name AS system_admin_name, system_admin.email AS system_admin_email 
FROM user_type_mapper AS user_type_mapper_1 JOIN system_admin ON user_type_mapper_1.user_id = system_admin.id AND user_type_mapper_1.is_admin = 1 UNION SELECT user_type_mapper_2.id AS user_type_mapper_2_id, user_type_mapper_2.is_admin AS user_type_mapper_2_is_admin, user_type_mapper_2.user_id AS user_type_mapper_2_user_id, "user".id AS user_id, "user".name AS user_name, "user".email AS user_email 
FROM user_type_mapper AS user_type_mapper_2 JOIN "user" ON user_type_mapper_2.user_id = "user".id AND user_type_mapper_2.is_admin = 0) AS anon_1

While it's theoretically possible to wrap this all up into a python class that looks a bit like standard sqlalchemy orm stuff, I would certainly not do that. working with non-table mappings, especially when they are more than simple joins (this is a union), is lots of work for zero payoff.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top